数据驱动开发,与传统的 jQuery 开发相比,有很多优势。最明显的两点是:
- 不需要关注 dom。不仅不需要关注如何初始化dom,也不需要关心状态变更时如何处理dom。整个流程围绕着如何操作数据。
- 可以方便做优化。因为整个流程都是数据,加上配合 vdom 对底层的抽象,我们可以做类似于 diff patch 算法的优化。多了层抽象意味着有了很多优化空间。
在做UI 编程时,通常有两个流程需要考虑:
- 第一次进来时如何展示。
- 当后续有变化时如何展示。
这是一个动态的时间序的考量。对应在 Vue 的流程中:
- 从 new Vue 到 dom。
- 数据变化时更新 dom(很多人称之为响应式)。
本节主要分析从 new Vue 到最终 dom 的过程。
从 new Vue 开始
我们以最简单的 Hello world 示例:
1 | import Vue from 'vue/dist/vue.esm'; |
由深入浅出 Vue 实例化 可知,构造函数 Vue 位于 src/core/instance/index.js
:
1 | import { initMixin } from './init' |
当我们调用 new Vue 的时候,会使用内部方法 _init 。定义在 initMixin 内。注意由于 js 是动态语言,我们可以先使用,后定义。在 src/core/instance/init.js
1 | export function initMixin (Vue: Class<Component>) { |
initMixin 定义了一个原型方法。做了一些初始化操作,然后调用 $mount 方法。
平台无关的 $mount 方法
$mount 方法是一个平台无关的方法。无论是 weex 运行时,还是 web 的运行时,都需要有 $mount 方法。相当于一个运行时接口。
我们研究 entry-runtime-with-compiler.js
时,发现有如下代码:
1 | import Vue from './runtime/index' |
在 import Vue from './runtime/index'
时,已经定义了 $mount 方法,为什么在这里要缓存并重写呢?
- runtime 版本的mount 可以被多个入口使用。比如
entry-runtime.js
。 - 这里重写是要加自己的逻辑,对于带compiler版本的 Vue 来说,需要编译 template 到 render function。最终还是会调用 runtime 内的 $mount 方法。
compileToFunctions
会将模板 <div></div>
编译成 render function。 Vue2.0 之后都会变成 render fucntion。细节在后续章节详述。
mountComponent 到 dom
由上图可知,$mount 会调用 mountComponent 方法。找到 mountComponent 其实就已经找到终点了。
什么?这么简单?也不是,还有一些细节需要补充。
1 | export function mountComponent ( |
mountComponent 定义了一个 updateComponent 方法,然后新建了一个 Watcher 实例。
1 |
|
Watcher 的 构造函数将第二个参数 updateComponent
赋值给 getter。然后有调用其 get 方法,触发 getter。执行了 updateComponent 方法。
1 | updateComponent = () => { |
vm._render
vm._render 定义在 src/core/instance/render.js
:
1 | export function renderMixin (Vue: Class<Component>) { |
我们关注其核心部分。 _render 函数调用编译阶段生成的 render 函数。执行生成,vnode。最后返回 vnode。
vm._update
update 方法定义于 src/core/instance/lifecycle.js
。
1 |
|
update 第一个参数接受 vnode。第二个参数是服务端渲染标记,暂不考虑。由上面可知,vm._render 返回的就是 vnode。而 update 内部又调用了 patch 方法。
vm.__patch__
__patch__ 是一个平台无关的方法。和 $mount 一样,每个平台有不同的 __patch__ 方法,定义在 src/platforms/(web/weex)/runtime/index.js
内。我们只看 web 部分:
1 | import { patch } from './patch' |
1 | // patch.js |
nodeOps 定义了平台相关的节点操作方法。modules 定义了一些平台相关的属性事件操作。
我们注意到虽然平台相关,但是对外仍是接口的形式。不同平台需要实现相同的方法。
1 | export function createPatchFunction (backend) { |
createPatchFunction 是一个高阶函数。通过传入不同的 node_ops 和 modules,生成不同的 patch function。这里用到了一个函数柯里化的技巧,通过 createPatchFunction 把差异化参数提前固化,这样不用每次调用 patch 的时候都传递 nodeOps 和 modules 了。
在 返回的 patch 函数中:1
2
3
4
5
6
7
8
9
10// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
将会实际创建 dom。至此,我们就看到了从 new Vue 到 dom 的整个过程。
ISSUE
有问题?来 GitHub 一起讨论。