上次我们说到 vue
的初始化渲染会实例化一个渲染watcher,内部会执行**updateComponent
方法,执行updateComponent
方法的时候,会先执行实例上的_render
方法生成vnode
,在执行_update
方法patch
成真实dom
**,下面我们来分析一下这两块的原理:
updateComponent = () => { vm._update(vm._render(), hydrating)}
Virtual Dom(VNode)
Vue2.0引入了**virtual dom
,用一个原生的 JS 对象去描述一个 DOM节点,相比于操作真实DOM 代价更小。其实VNode
**是对真实 DOM 的一种抽象描述,我们看一下它的组成:
代码位置: src/core/vdom/vnode.js
export default class VNode {
constructor (tag, data, children, text, elm, context, componentOptions) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.context = context
this.componentOptions = componentOptions
this.componentInstance = undefined
}
}
VNode
核心定义无非就几个关键属性,标签名、数据、子节点、文本、真实元素、组件属性等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
tip: Vue.js 的**virtual dom
** 借鉴了开源组件库**snabbdom**的实现,然后加入了一些Vue.js自身需要的属性。
_render方法实现:
代码位置: src/core/instance/render.js
1. **_render
方法是在renderMixin
函数中添加到Vue Prototype
上的,内部会执行编译好的render
**函数。
Vue.prototype._render = function () {
const vm = this
const { render } = vm.$option
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement) // 执行render函数
}
}
执行**render
函数的时候,会传入vm.$createElement
**函数(以下简称h函数)。
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
2.用户调用h函数,会传入一系列配置项,如下:
new Vue({
el: '#app',
render(h) {
return h('div', {
attrs: {
id: 'app'
}
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
3. **Vue
拿到用户传递进来的配置项,会执行内部的createElement
**方法。
代码位置: src/core/vdom/dom/create-element.js
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize)
{
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
函数,序列化用户传入的参数,然后执行__createElement
**内部方法。
function _createElement ( context, tag, data, children, normalizationType){
// 1.规范化children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 2. 生成不同类型的VNode(普通标签、组件类型)
let vnode
if (typeof tag === 'string') {
let Ctor
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
}
手绘流程图如下:
到此为止**_render
**函数执行完成,此时会生成不同类型的VNode。
_update方法实现:
**_update
**入口: src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode, hydrating) {
const vm = this if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
}
**__patch__
**函数位置: src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
**patch
**函数位置: src/platforms/web/runtime/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'
const modules = platformModules.concat(baseModules)export
const patch = createPatchFunction({ nodeOps, modules })
**createPatchFunction
**函数位置: src/core/vdom/patch.js
function createPatchFunction (backend) {
const { modules, nodeOps } = backend
return function patch (oldVnode, vnode, hydrating, removeOnly) {
const isRealElement = isDef(oldVnode.nodeType)
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
}
}
**`createElm`**函数位置: **`src/core/vdom/patch.js`**
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode)
createChildren(vnode, children, insertedVnodeQueue) // 递归调用createElm方法挂载子VNode
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
**createChildren
**函数实现: **`src/core/vdom/patch.js`**
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
手绘流程图如下:
总结: 执行**patch
函数的过程中,会执行createElm
**创建节点并挂载(根据vnode的tag来区分,如果vnode有children,会遍历children递归调用createElm挂载子节点)。这是一个深度优先的过程,所以节点的挂载顺序是 子 -> 父