这里写目录标题
- 概览
- 1. 变化侦查
- 1.1 Observer流程图
- 2. vdom虚拟DOM
- 2.1 创建节点createElm
- 2.2 更新节点patchVnode
- 2.3 更新子节点 updateChildren
- 3. 模板编译
- 3.1 抽象语法树AST
- 3.2 模板编译具体流程
- 3.3 HTML解析器
- 3.3.1 解析不同的内容
- 3.3.2 AST节点层级
- 3.3.3 小结
- 3.4 文本解析器
- 3.5 优化阶段
- 3.6 代码生成阶段
- 3.7 总结
- 4. 生命周期
- 4.1Vue实例的生命周期流程图
- 4.2 _init()函数
- 5. 实例方法
- 6. 全局Api
概览
├─src # 项目源代码
│ ├─complier # 与模板编译相关的代码
│ ├─core # 通用的、与运行平台无关的运行时代码
│ │ ├─observe # 实现变化侦测的代码
│ │ ├─vdom # 实现virtual dom的代码
│ │ ├─instance # Vue.js实例的构造函数和原型方法
│ │ ├─global-api # 全局api的代码
│ │ └─components # 内置组件的代码
参考:学习方式:对照源码阅读流程图
1. 变化侦查
1.1 Observer流程图
其整个流程大致如下:
- Data通过observer转换成了getter/setter的形式来追踪变化。
- 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖管理器中。
- 当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。
- Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
例如:vue一次set流程
2. vdom虚拟DOM
虚拟DOM就是用JS来描述一个真实的DOM节点。而在Vue中就存在了一个VNode类,通过这个类,我们就可以实例化出不同类型的虚拟DOM节点。
2.1 创建节点createElm
2.2 更新节点patchVnode
DOM-Diff即patch 打补丁
2.3 更新子节点 updateChildren
3. 模板编译
3.1 抽象语法树AST
抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
如下图
3.2 模板编译具体流程
三个阶段:
- 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
- 解析器——源码路径:src/compiler/parser/index.js;
- 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
- 优化器——源码路径:src/compiler/optimizer.js;
- 代码生成阶段:将AST转换成渲染函数;
- 代码生成器——源码路径:src/compiler/codegen/index.js
src\compiler\index.js => baseCompile作为核心调用,包含三个阶段
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 模板解析阶段:用正则等方式解析 template 模板中的指令、class、style等数据,形成AST
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化阶段:遍历AST,找出其中的静态节点,并打上标记
optimize(ast, options)
}
// 代码生成阶段:将AST转换成渲染函数
const code = generate(ast, options)
// 最终返回了抽象语法树( ast ),渲染函数( render ),静态渲染函数( staticRenderFns )
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
解析器
3.3 HTML解析器
parse内的parseHTML有两个参数分别是:
- template:待转换的模板字符串;
- options:转换时所需的选项;
第二个参数提供了一些解析HTML模板时的一些参数,同时还定义了4个钩子函数,作用:把模板字符串中不同的内容出来之后,把提取出来的内容生成对应的AST。
内部定义四个方法:
start:解析到开始标签时调用
end:解析到结束标签时调用
chars:解析到文本时调用
comment当解析到注释时调用
3.3.1 解析不同的内容
通过编写不同的正则表达式将下面这些内容从模板字符串中一一解析出来,然后再把不同的内容做不同的处理。以下内容对应源码位置:src\compiler\parser\html-parser.js
1. HTML注释,例如<!-- 我是注释 -->
2. 条件注释,例如<!-- [if !IE]> -->我是注释<!--< ![endif] -->
3. DOCTYPE,例如<!DOCTYPE html>
4. 文本,例如“难凉热血”
5. 开始标签,例如<div>
6. 结束标签,例如</div>
1. HTML注释,2. 条件注释,3. DOCTYPE,解析流程如图:
4. 文本解析流程如图:
5. 开始标签,6. 结束标签,解析流程图如下:
开始标签内部两个方法parseStartTag(),handleStartTag()的详细流程:
parseStartTag()的解析例子:
结束标签内部parseEndTag的详细流程:
涉及AST节点层级,可先看3.3.2AST节点层级
函数接收三个参数,分别是结束标签名tagName、结束标签在html字符串中的起始和结束位置start和end。
这三个参数其实都是可选的,根据传参的不同其功能也不同。
- 第一种是三个参数都传递,用于处理普通的结束标签
- 第二种是只传递tagName
- 第三种是三个参数都不传递,用于处理栈中剩余未处理的标签
3.3.2 AST节点层级
Vue在HTML解析器的开头定义了一个栈stack,
作用
- 用来维护AST节点层级的。
- 检测模板字符串中是否有未正确闭合的标签。(展现在上面parseEndTag函数的抛出警告)
HTML解析器在从前向后解析模板字符串时,每当遇到开始标签时就会调用start钩子函数,那么在start钩子函数内部我们可以将解析得到的开始标签推入栈中,
而每当遇到结束标签时就会调用end钩子函数,那么我们也可以在end钩子函数内部将解析得到的结束标签所对应的开始标签从栈中弹出。
3.3.3 小结
HTML解析器的工作流程就是:一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点,最终完成将整个模板字符串转化成AST,在解析器内维护了一个栈,用来保证构建的AST节点层级与真正DOM层级一致。
3.4 文本解析器
接3.3.1文本标签解析器在options.chars()钩子被使用。
src\compiler\parser\index.js
chars (text) {
if(res = parseText(text)){
let element = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else {
let element = {
type: 3,
text
}
}
}
实例:
let res = parseText(text)
res = {
expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
tokens:[
"我叫",
{'@binding': name },
",我今年"
{'@binding': age },
"岁了"
]
}
文本解析器内部就干了三件事:
- 判断传入的文本是否包含变量
- 构造expression
- 构造tokens
- expression属性就是把文本中的变量和非变量提取出来,然后把变量用_s()包裹,最后按照文本里的顺序把它们用+连接起来。
- tokens是个数组,数组内容也是文本中的变量和非变量,不一样的是把变量构造成{’@binding’: xxx}。
文本解析器的作用就是将HTML解析器解析得到的文本内容进行二次解析,解析文本内容中是否包含变量,如果包含变量,则将变量提取出来进行加工,为后续生产render函数做准备。
文本解析源码:src/compiler/parser/text-parsre.js
3.5 优化阶段
在模板编译的时候就先找出模板中所有的静态节点和静态根节点,然后给它们打上标记,用于告诉后面patch过程打了标记的这些节点是不需要对比的,只要把它们克隆一份去用就好啦。
优化阶段:在AST中找出所有静态节点和所有静态根节点并打上标记;
好处:
- 把它们变成常量,这样我们就不需要在每次重新渲染时为它们创建新的节点;
- 在打补丁的过程中完全跳过它们。
src/compiler/optimizer.js
isStatic方法源码:
function isStatic(node: ASTNode): boolean {
if (node.type === 2) { // expression 包含变量的动态文本节点
return false
}
if (node.type === 3) { // text 不包含变量的纯文本节点
return true
}
return !!(node.pre // 如果节点使用了v-pre指令,那就断定它是静态节点;
||
// 如果节点没有使用v-pre指令,那它要成为静态节点必须满足:
(!node.hasBindings && //不能使用动态绑定语法,即标签上不能有v-、@、:开头的属性;
!node.if && !node.for && //不能使用v-if、v-else、v-for指令;
!isBuiltInTag(node.tag) && //不能是内置组件,即标签名不能是slot和component;
isPlatformReservedTag(node.tag) && //标签名必须是平台保留标签,即不能是组件;
!isDirectChildOfTemplateFor(node) && //当前节点的父节点不能是带有 v-for 的 template 标签;
Object.keys(node).every(isStaticKey) //节点的所有属性的 key 都必须是静态节点才有的 key
//静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一;
))
}
3.6 代码生成阶段
代码生成阶段主要的工作就是根据已有的AST生成对应的render函数供组件挂载时调用,组件只要调用的这个render函数就可以得到AST对应的虚拟DOM的VNode。
生成render函数的过程其实就是一个递归的过程,从顶向下依次递归AST中的每一个节点,根据不同的AST节点类型创建不同的VNode类型。
src/compiler/codegen/index.js
3.7 总结
4. 生命周期
4.1Vue实例的生命周期流程图
Vue实例的生命周期大致可分为4个阶段:
初始化阶段:为Vue实例上初始化一些属性,事件以及响应式数据;
模板编译阶段:将模板编译成渲染函数;
挂载阶段:将实例挂载到指定的DOM上,即将模板渲染到真实DOM中;
销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器
4.2 _init()函数
src/core/instance/index.js
Vue类的定义
function Vue (options) {
this._init(options)
}
initMixin(Vue)
src/core/instance/init.js
initMixin 类:只是给Vue类的原型上绑定_init方法
关键_init方法
new Vue()会执行Vue类的构造函数,构造函数内部会执行_init方法,所以new Vue()所做的就是_init方法所做的
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this //把Vue实例赋值给变量vm
//把用户传递的options与当前构造函数的options属性及其父级构造函数的options属性进行合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数beforeCreate
initInjections(vm) //初始化injections
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数created
//判断用户是否传入了el选项,如果传入了则调用$mount函数进入模板编译与挂载阶段,
//如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
合并属性
mergeOptions函数
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),//parent
options || {},//child
vm
)
把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,可简单理解为返回 vm.constructor.options,相当于 Vue.options
src/core/global-api/index.js
Vue.options
initGlobalAPI(Vue)
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
extend(Vue.options.components, builtInComponents)
// ...
}
ASSET_TYPES 的定义在 src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
遍历 ASSET_TYPES 后的代码相当于:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前 有<keep-alive>
、<transition>
和<transition-group>
组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。
mergeOptions 这个函数,它的定义在 src/core/util/options.js
mergeOptions函数的主要功能是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。
5. 实例方法
6. 全局Api