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