鄢栋,微医云服务团队前端工程师。有志成为一名全栈开发工程师甚至架构师,路漫漫,吾求索。 生活中通过健身释放压力,思考问题。
目前 Vue3.0 打的很火热,都已经出了很多 Vue3.0 源码解析系列的博客, 但是 Vue2.0 的源码我觉得还是有必要细品一下, 掌握了原有通用的源码原理,才能知道新版本的 Vue3.0 到底做了哪些更改。如果已经很熟悉了,可跳过~
首先整体看一下整个页面渲染的流程图, 顺着这张图我们再带着问题深入研究, 相信很快就能攻克阅读 Vue 源码的困难。
初始化及挂载
new Vue() -> $mount
从文件夹 core/index.js 入口,看到 import Vue from './instance/index'这句话;
接着我们定位到 instance/index.js 文件,看到
import { initMixin } from './init'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue); // 这个 Vue 参数在当前文件定义了
可以看到: 上面 Vue 构造函数中,执行了this._init(options)。
this 是指当前的 Vue 实例,是从initMixin()函数中定义的。
我们定位到 instance/init.js 可以看到:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
... // 省略中间的处理
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化 render
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 props、 methods、 data、 computed 与 watch 等
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
... // 省略一部分处理
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 初始化之后调用 $mount 挂载组件
}
}
}
- 在 new Vue() 之后, Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会:
- 初始化生命周期:
initLifecycle(vm) - 初始化事件:
initEvents(vm) - 初始化 props、 methods、 data、 computed 与 watch 等选项:
initState(vm)
- 初始化生命周期:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // props
if (opts.methods) initMethods(vm, opts.methods) // methods
if (opts.data) {
initData(vm) // data
} else {
observe(vm._data = {}, true)
}
if (opts.computed) initComputed(vm, opts.computed) // computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch) // watch
}
}
- 其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」,后面会详细讲到,这里只要有一个印象即可。
(Q1: Vue 的响应式以及依赖收集是如何实现的?)
-
初始化之后调用 $mount 会挂载组件。
-
如果是运行时编译,即不存在 render function 但是采用 template 进行渲染 的情况,需要进行「编译」步骤。
(Q2: Vue 的模板编译过程?)
编译
compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。
查看 compiler/index.js 文件:
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
可以看到,先创建了一个编译器, 创建成功后:
- 调用
parse函数生成 AST 抽象语法树; - 如果选项中的 optimize 为 true, 则需要进行优化, 调用
optimize函数; - 接着, 根据生成的 AST,通过调用
generate函数生成代码段对象; - 最后将
AST、code 对象中的render 字符串(VNode 渲染所需要的) 、code 中的staticRenderFns 字符串包裹成一个对象返回。
parse
parse 会用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST。(如何解析?)
optimize
optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
(Q3: Vue 是如何区分静态节点的?Vue 的 patch 过程?diff 算法做了哪些事情?)
generate
generate 是将 AST 转化成 render function 字符串的过程(如何转换?),得到结果是 render 的字符串以及 staticRenderFns 字符串。
在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。
(Q4: VNode 是什么?)
响应式
当 render function 被渲染的时候,会读取对象中的值, 从而触发getter 函数进行依赖收集。依赖收集的目的是将观察者 Watcher 对象放到订阅者 Dep 中的 subs 中。
当修改对象中的值时,会触发setter 函数通知之前收集的 Dep 中的每一个 Watcher 重新渲染视图,Watcher 收到通知后, 调用 update 函数来更新视图。当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略,这个我们后面再讲。
(Q5: Vue2.0 的响应式原理?)
Virtual DOM
虚拟 DOM 其实是 render function 执行后的产物,是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
更新视图
-
前面说到:在修改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来修改对象对应的值。
-
当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些浪费。
-
因此我们可以只修改有修改的部分,这个时候就会通过 patch 去比较了。将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异」。最后我们只需要将这些「差异」的对应 DOM 进行修改即可。
总结
回过头来,我们再来看第一张图:
对于 Vue 整体上的执行机制是否有了一些概念?
- 页面渲染:
new Vue() -> init() -> $mount() - 数据更新:
用户操作导致数据需要更新,视图需要更新 -> getter 收集依赖 -> dep.depend() -> dep.subs.push(watcher) -> setter 通知 watcher 更新视图 -> dep.notify() -> dep.subs[i].update() -> render fucntion() -> VNode -> patch -> DOM - 模板更新:
模板编译:parse -> optimize -> generator
具体的有些机制细节,本系列会一一更新, 共同学习, 共同进步! 具体有些细节, 可能笔者在理解上存在误解,如果有问题,欢迎在评论或者留言区进行反馈交流~
参考资料
- 染陌掘金小册《剖析 Vue.js 内部运行机制》
- Vue2.0 源码