深入浅出 Vue 数据驱动(一)

数据驱动开发,与传统的 jQuery 开发相比,有很多优势。最明显的两点是:

  1. 不需要关注 dom。不仅不需要关注如何初始化dom,也不需要关心状态变更时如何处理dom。整个流程围绕着如何操作数据。
  2. 可以方便做优化。因为整个流程都是数据,加上配合 vdom 对底层的抽象,我们可以做类似于 diff patch 算法的优化。多了层抽象意味着有了很多优化空间。

在做UI 编程时,通常有两个流程需要考虑:

  • 第一次进来时如何展示。
  • 当后续有变化时如何展示。

这是一个动态的时间序的考量。对应在 Vue 的流程中:

  • 从 new Vue 到 dom。
  • 数据变化时更新 dom(很多人称之为响应式)。

image

本节主要分析从 new Vue 到最终 dom 的过程。

从 new Vue 开始

我们以最简单的 Hello world 示例:

1
2
3
4
5
6
7
8
9
import Vue from 'vue/dist/vue.esm';

new Vue({
template: '<div>{{message}}</div>',
el: `#app`,
data: {
message: 'Flyyang say hello to Vue.js!',
},
});

深入浅出 Vue 实例化 可知,构造函数 Vue 位于 src/core/instance/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

export default Vue

当我们调用 new Vue 的时候,会使用内部方法 _init 。定义在 initMixin 内。注意由于 js 是动态语言,我们可以先使用,后定义。在 src/core/instance/init.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

initMixin 定义了一个原型方法。做了一些初始化操作,然后调用 $mount 方法。

平台无关的 $mount 方法

$mount 方法是一个平台无关的方法。无论是 weex 运行时,还是 web 的运行时,都需要有 $mount 方法。相当于一个运行时接口。

我们研究 entry-runtime-with-compiler.js 时,发现有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from './runtime/index'

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
}
return mount.call(this, el, hydrating)
}

import Vue from './runtime/index' 时,已经定义了 $mount 方法,为什么在这里要缓存并重写呢?

  1. runtime 版本的mount 可以被多个入口使用。比如 entry-runtime.js
  2. 这里重写是要加自己的逻辑,对于带compiler版本的 Vue 来说,需要编译 template 到 render function。最终还是会调用 runtime 内的 $mount 方法。

compileToFunctions 会将模板 <div></div> 编译成 render function。 Vue2.0 之后都会变成 render fucntion。细节在后续章节详述。

image

mountComponent 到 dom

由上图可知,$mount 会调用 mountComponent 方法。找到 mountComponent 其实就已经找到终点了。

什么?这么简单?也不是,还有一些细节需要补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

return vm
}

mountComponent 定义了一个 updateComponent 方法,然后新建了一个 Watcher 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {

if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.lazy
? undefined
: this.get()
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
value = this.getter.call(vm, vm)
return value
}

}

Watcher 的 构造函数将第二个参数 updateComponent 赋值给 getter。然后有调用其 get 方法,触发 getter。执行了 updateComponent 方法。

1
2
3
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

vm._render

vm._render 定义在 src/core/instance/render.js:

1
2
3
4
5
6
7
8
9
10
11
12
export function renderMixin (Vue: Class<Component>) {
Vue.prototype._render = function (): VNode {
const { render, _parentVnode } = vm.$options
// There's no need to maintain a stack becaues all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)

return vnode
}
}

我们关注其核心部分。 _render 函数调用编译阶段生成的 render 函数。执行生成,vnode。最后返回 vnode。

vm._update

update 方法定义于 src/core/instance/lifecycle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
}

update 第一个参数接受 vnode。第二个参数是服务端渲染标记,暂不考虑。由上面可知,vm._render 返回的就是 vnode。而 update 内部又调用了 patch 方法。

vm.__patch__

__patch__ 是一个平台无关的方法。和 $mount 一样,每个平台有不同的 __patch__ 方法,定义在 src/platforms/(web/weex)/runtime/index.js内。我们只看 web 部分:

1
2
3
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
1
2
3
4
5
6
7
8
9
10
11
// patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules }

nodeOps 定义了平台相关的节点操作方法。modules 定义了一些平台相关的属性事件操作。

image

我们注意到虽然平台相关,但是对外仍是接口的形式。不同平台需要实现相同的方法

image

1
2
3
4
5
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
....
}
}

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 一起讨论。