Vue.js源码学习四 —— 渲染 Render 初始化过程学习

今天我们来学习下Vue的渲染 Render 源码~

还是从初始化方法开始找代码,在 src/core/instance/index.js 中,先执行了 renderMixin 方法,然后在Vue实例化的时候执行了 vm._init 方法,在这个 vm._init 方法中执行了 initRender 方法。renderMixininitRender 都在 src/core/instance/render.js 中,我们来看看代码:

renderMixin

首先来跟一下 renderMixin 的代码:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)

Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}

Vue.prototype._render = function (): VNode {
const vm: Component = this
// vm.$options.render & vm.$options._parentVnode
const { render, _parentVnode } = vm.$options

if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}

vm.$vnode = _parentVnode
let vnode
try {
// 执行 vue 实例的 render 方法
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
if (process.env.NODE_ENV !== 'production') {
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
}
// 返回空vnode避免render方法报错退出
if (!(vnode instanceof VNode)) {
vnode = createEmptyVNode()
}
// 父级Vnode
vnode.parent = _parentVnode
return vnode
}
}

源码执行了 installRenderHelpers 方法,然后定义了 Vue 的 $nextTick_render 方法。
先来看看 installRenderHelpers 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber // 数字
target._s = toString // 字符串
target._l = renderList // 列表
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}

这就是 Vue 的各类渲染方法了,从字面意思中可以知道一些方法的用途,这些方法用在Vue生成的渲染函数中。具体各个渲染函数的实现先不提~之后会专门写博客学习。
$nextTick 函数中执行了 nextTick 函数,找到该函数源码:

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
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

现在来说关键的 _render 方法,关键在这个 try…catch 方法中,执行了Vue实例中的 render 方法生成一个vnode。如果生成失败,会试着生成 renderError 方法。如果vnode为空,则为vnode传一个空的VNode,最后返回vnode对象。

initRender

接下来看下 render 的初始化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 将 createElement 方法绑定到这个实例,这样我们就可以在其中得到适当的 render context。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 规范化一直应用于公共版本,用于用户编写的 render 函数。
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// 父级组件数据
const parentData = parentVnode && parentVnode.data
// 监听事件
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

在 initRender 方法中,为Vue的实例方法添加了几个属性值,最后定义了 $attrs$listeners 的监听方法。
看下 createElement 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}

这里执行了 _createElement 方法,由于该方法太长,就不贴出来费篇幅了,代码看这里。最终返回一个 VNode 对象,VNode 对象由 createEmptyVNodecreateComponent 方法得到的。
createEmptyVNode 创建了一个空的 VNode

1
2
3
4
5
6
7
// src/core/vdom/vnode.js
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}

createComponent 创建了一个组件,最终也将返回一个 VNode 对象。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') {
return
}

let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

data = data || {}
resolveConstructorOptions(Ctor)
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}

const listeners = data.on
data.on = data.nativeOn

if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}

mergeHooks(data)
// 创建组件的 VNode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)

return vnode
}

初次渲染过程

既然是初次渲染,肯定会触发 mounted 生命周期钩子。所以我们从 mount 找起。在源码中定义了两次 $mount 方法,第一次返回了 mountComponent 方法;第二次定义了 Vue 实例的 $options 选项中的一些数据,然后再执行第一次的 $mount 方法,即执行 mountComponent 方法。

1
2
3
4
5
6
7
8
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

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
34
35
36
37
38
39
40
41
42
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)

if (el === document.body || el === document.documentElement) {
return this
}

const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}

这里需要注意的是 compileToFunctions 方法,该方法的作用是将 template 编译为 render 函数。
compileToFunctions 方法是一个编译的过程,暂且不论。抓住主线,看渲染。所以去看看 mountComponent 方法:

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
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')

let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
hydrating = false

if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

可以看到,在 beforeMount 和 mounted 生命周期之间的代码:创建一个更新方法,然后创建一个Watcher监听该方法。

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

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)

new Watcher 监听了 updateComponent 方法后,会立即执行 updateComponent 方法。在 updateComponent 方法中,我们之前提到 _render 方法最终返回一个编译过的 VNode 对象,即虚拟 DOM,这里我们就看看 _update 方法。

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
34
35
36
// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode

if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}

从注释可以看出,初次渲染会走到 vm.__patch__ 方法中,这个方法就是比对虚拟 DOM ,局部更新 DOM 的方法,关于虚拟 DOM 和 VNode 节点,之后再聊。

小结一下

  • 通过 renderMixin 方法来定义一些渲染属性。
  • initRender 定义了各类渲染选项,并且对一些属性进行监听。
  • $mount 方法执行了 mountComponent 方法,监听
    updateComponent 方法并执行 _update 方法。
  • _update 方法中执行 __patch__ 方法渲染 VNode。

最后

这里简单理了理 render 渲染的代码流程,更深入的关于虚拟 DOM 的内容在下一篇中继续研究~
这里再提出几个问题,之后学习和解决:

  • template 的具体编译细节
  • 已知 data 数据监测,如何在改变数据后对改变界面的显示。
  • 深入理解虚拟 DOM 的原理
  • 学习全局 API 的源码
  • 了解各类工具类
  • 了解 AST 语法树是什么~

计划3月底完成Vue源码的系统学习,之后转战vue-router、vuex、vuxt、 devtools、webpack、vue-loader,今年目标把Vue全家老小、亲戚朋友都学习一遍!加油!

Vue.js学习系列

鉴于前端知识碎片化严重,我希望能够系统化的整理出一套关于Vue的学习系列博客。

Vue.js学习系列项目地址

本文源码已收入到GitHub中,以供参考,当然能留下一个star更好啦^-^。
https://github.com/violetjack/VueStudyDemos

关于作者

VioletJack,高效学习前端工程师,喜欢研究提高效率的方法,也专注于Vue前端相关知识的学习、整理。
欢迎关注、点赞、评论留言~我将持续产出Vue相关优质内容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571d953d39b0570068145cd1
CSDN: http://blog.csdn.net/violetjack0808
简书: http://www.jianshu.com/users/54ae4af3a98d/latest_articles
Github: https://github.com/violetjack