源码版本:v2.1.10javascript
经过阅读源码,对 Vue2 的基础运行机制有所了解,主要是:vue
Vue2 中数据绑定的实现方式java
Vue2 中对 Virtual DOM 机制的使用方式node
项目构建配置文件为 build/config.js
,定位 vue.js 对应的入口文件为 src/entries/web-runtime-with-compiler.js
,基于 rollup 进行模块打包。git
代码中使用 flow 进行接口类型标记和检查,在打包过程当中移除这些标记。为了阅读代码方便,在 VS Code 中安装了插件 Flow Language Support,而后关闭工做区 JS 代码检查,这样界面就清爽不少了。github
Vue 应用启动通常是经过 new Vue({...})
,因此,先从该构造函数着手。web
注:本文只关注 Vue 在浏览器端的应用,不涉及服务器端代码。数组
文件:src/core/instance/index.js
浏览器
该文件只是构造函数,Vue 原型对象的声明分散在当前目录的多个文件中:服务器
init.js:._init()
state.js:.$data
.$set()
.$delete()
.$watch()
render.js:._render()
...
events.js:.$on()
.$once()
.$off()
.$emit()
lifecycle.js:._mount()
._update()
.$forceUpdate()
.$destroy()
构造函数接收参数 options
,而后调用 this._init(options)
。
._init()
中进行初始化,其中会依次调用 lifecycle、events、render、state 模块中的初始化函数。
Vue2 中应该是为了代码更易管理,Vue 类的定义分散到了上面的多个文件中。
其中,对于 Vue.prototype
对象的定义,经过 mixin 的方式在入口文件 core/index.js
中依次调用。对于实例对象(代码中一般称为 vm
)则经过 init 函数在 vm._init()
中依次调用。
文件:src/core/index.js
这里调用了 initGlobalAPI()
来初始化 Vue 的公共接口,包括:
Vue.util
Vue.set
Vue.delete
Vue.nextTick
Vue.options
Vue.use
Vue.mixin
Vue.extend
asset相关接口:配置在 src/core/config.js
中
调用 new Vue({...})
后,在内部的 ._init()
的最后,是调用 .$mount()
方法来“启动”。
在 web-runtime-with-compiler.js
和 web-runtime.js
中,定义了 Vue.prototype.$mount()
。不过两个文件中的 $mount()
最终调用的是 ._mount()
内部方法,定义在文件 src/core/instance/lifecycle.js
中。
Vue.prototype._mount(el, hydrating)
简化逻辑后的伪代码:
vm = this vm._watcher = new Watcher(vm, updateComponent)
接下来看 Watcher
。
文件:src/core/observer/watcher.js
先看构造函数的简化逻辑:
// 参数:vm, expOrFn, cb, options this.vm = vm vm._watchers.push(this) // 解析 options,略.... // 属性初始化,略.... this.getter = expOrFn // if `function` this.value = this.lazy ? undefined : this.get()
因为缺省的 lazy
属性值为 false
,接着看 .get()
的逻辑:
pushTarget(this) // ! value = this.getter.call(this.vm, this.vm) popTarget() this.cleanupDeps() return value
先看这里对 getter
的调用,返回到 ._mount()
中,能够看到,是调用了 vm._update(vm._render(), hydrating)
,涉及两个方法:
vm._render():返回虚拟节点(VNode)
vm._update()
来看 _update()
的逻辑,这里应该是进行 Virtual DOM 的更新:
// 参数:vnode, hydrating vm = this prevEl = vm.$el prevVnode = vm._vnode prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode if (!prevVnode) { // 初次加载 vm.$el = vm.__patch__(vm.$el, vnode, ...) } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // 后续属性配置,略....
参考 Virtual DOM 的通常逻辑,这里是差很少的处理过程,再也不赘述。
综上,这里的 watcher 主要做用应该是在数据发生变动时,触发从新渲染和更新视图的处理:vm._update(vm._render())
。
接下来,咱们看下 watcher 是如何发挥做用的,参考 Vue 1.0 的经验,下面应该是关于依赖收集、数据绑定方面的细节了,而这一部分,和 Vue 1.0 差异不大。
watcher.get()
中调用的 pushTarget()
和 popTarget()
来自文件:src/core/observer/dep.js
。
pushTarget()
和 popTarget()
两个方法,用于处理 Dep.target
,显然 Dep.target
在 wather.getter
的调用过程当中会用到,调用时会涉及到依赖收集,从而创建起数据绑定的关系。
在 Dep
类的 .dep()
方法中用到了 Dep.target
,调用方式为:
Dep.target.addDep(this)
能够想见,在使用数据进行渲染的过程当中,会对数据属性进行“读”操做,从而触发 dep.depend()
,进而收集到这个依赖关系。下面来找一下这样的调用的位置。
在 state.js
中找到一处,makeComputedGetter()
函数中经过 watcher.depend()
间接调用了 dep.depend()
。不过 computedGetter 应该不是最主要的地方,根据 Vue 1.0 的经验,仍是要找对数据进行“数据劫持”的地方,应该是defineReactive()
。
defineReactive()
定义在文件 src/core/observer/index.js
。
// 参数:obj, key, val, customSetter? dep = new Dep() childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { // 略,调用了 dep.depend() }, set: function () { // 略,调用 dep.notify() } })
结合 Vue 1.0 经验,这里应该就是数据劫持的关键了。数据原有的属性被从新定义,属性的 get()
被调用时,会经过 dep.depend()
收集依赖关系,记录到 vm 中;而在 set()
被调用时,则会判断属性值是否发生变动,若是发生变动,则经过 dep.notify()
来通知 vm,从而触发 vm 的更新操做,实现 UI 与数据的同步,这也就是数据绑定后的效果了。
回过头来看 state.js
,是在 initProps()
中调用了 defineReactive()
。而 initProps()
在 initState()
中调用,后者则是在 Vue.prototype._init()
中被调用。
不过最经常使用的实际上是在 initData()
中,对初始传入的 data
进行劫持,不过里面的过程稍微绕一些,是将这里的 data 赋值到 vm._data
而且代理到了 vm
上,进一步的处理还涉及 observe()
和 Observer
类。这里不展开了。
综上,数据绑定的实现过程为:
初始化:new Vue() -> vm._init()
数据劫持:initState(vm) -> initProps(), initData() -> dep.depend()
依赖收集:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()
首先来看 initRender()
,这里在 vm 上初始化了两个与建立虚拟元素相关的方法:
vm._c()
vm.$createElement()
其内部实现都是调用 createElement()
,来自文件:src/core/vdom/create-element.js
。
而在 renderMixin()
中初始化了 Vue.prototype._render()
方法,其中建立 vnode 的逻辑为:
render = vm.$options.render try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // ... }
这里传入 render()
是一个会返回 vnode 的函数。
接下来看 vm._update()
的逻辑,这部分在前面有介绍,初次渲染时是经过调用 vm.__patch__()
来实现。那么 vm.__patch__()
是在哪里实现的呢?在 _update()
代码中有句注释,提到:
// Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used.
在文件 web-runtime.js
中,找到了:
Vue.prototype.__patch__ = inBrowser ? patch : noop
显然示在浏览器环境下使用 patch()
,来自:src/platforms/web/runtime/patch.js
,其实现是经过 createPatchFunction()
,来自文件 src/core/vdom/patch
。
OK,以上线索都指向了 vdom 相关的模块,也就是说,显然是 vdom 也就是 Virtual DOM 参与了渲染和更新。
不过还有个问题没有解决,那就是原始的字符串模块,是如何转成用于 Virtual DOM 建立的函数调用的呢?这里会有一个解析的过程。
回到入口文件 web-runtime-with-compiler.js
,在 Vue.prototype.$mount()
中,有一个关键的调用:compileToFunctions(template, ...)
,template
变量值为传入的参数解析获得的模板内容。
文件:src/platforms/web/compiler/index.js
函数 compileToFunctions()
的基本逻辑:
// 参数:template, options?, vm? res = {} compiled = compile(template, options) res.render = makeFunction(compiled.render) // 拷贝数组元素: // res.staticRenderFns <= compiled.staticRenderFns return res
这里对模板进行了编译(compile()
),最终返回了根据编译结果获得的 render()、staticRenderFns
。再看 web-runtime-with-compiler.js
中 Vue.prototype.$mount()
的逻辑,则是将这里获得的结果写入了 vm.$options
中,也就是说,后面 vm._render()
中会使用这里的 render()
。
再来看 compile()
函数,这里是实现模板解析的核心,来作文件 src/compiler/index.js
,基本逻辑为:
// 参数:template, options ast = parse(template.trim(), options) optimize(ast, options) code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns }
逻辑很清晰,首先从模板进行解析获得抽象语法树(ast),进行优化,最后生成结果代码。整个过程当中确定会涉及到 Vue 的语法,包括指令、组件嵌套等等,不只仅是获得构建 Virtual DOM 的代码。
须要注意的是,编译获得 render 实际上是代码文本,经过 new Function(code)
的方式转为函数。
Vue2 相比 Vue1 一个主要的区别在于引入了 Virtual DOM,但其 MVVM 的特性还在,也就是说仍有一套数据绑定的机制。
此外,Virtual DOM 的存在,使得原有的视图模板须要转变为函数调用的模式,从而在每次有更新时能够从新调用获得新的 vnode,从而应用 Virtual DOM 的更新机制。为此,Vue2 实现了编译器(compiler),这也意味着 Vue2 的模板能够是纯文本,而没必要是 DOM 元素。
Vue2 基本运行机制总结为:
文本模板,编译获得生成 vnode 的函数(render),该过程当中会识别并记录 Vue 的指令和其余语法
new Vue() 获得 vm 对象,其中传入的数据会进行数据劫持处理,从而能够收集依赖,实现数据绑定
渲染过程是将全部数据交由渲染函数(render)进行调用获得 vnode,应该 Virtual DOM 的机制实现初始渲染和更新
对 Vue2 的源码分析,是基于我以前对 Vue1 的分析和对 Virtual DOM 的了解,见【连接】中以前的文章。
水平有限,错漏不免,欢迎指正。
感谢阅读!