在上一篇文章《render 渲染原理》,讲解了如何把 Vue 实例渲染成虚拟 DOM。沿着这一思路,将讲解如何把虚拟 DOM 渲染成真实 DOM。而对于虚拟 DOM,又分为普通 VNode 和组件 VNode,本文将详细分析普通 VNode 的渲染原理。
update 内部实现
按照惯例,沿着主线将普通节点 patch 过程整理成一张图,如下:
对于普通节点 patch 过程,其实现逻辑是定义在 Vue 原型上的方法:_update ,位于 src/core/instance/lifecycle.js ,其实现逻辑如下:
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:虚拟 DOM,即render函数渲染生成的虚拟节点hydrating:是否为服务端渲染
从代码实现中可以看出,_update 作用有两个:首次渲染和更新渲染,即数据驱动视图变更;而本文只分析初始化渲染的逻辑。
先定义变量来保存数据,比如 prevEl、prevVnode 及保存当前激活实例 vm 。其实现逻辑最核心的是 patch,代码片段如下:
// 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)
}
由于首次渲染,prevVnode 为 null,则会进入 if 逻辑,即执行首次渲染逻辑。那么 vm.__patch__ 又是如何定义的呢?
__patch__ 是定义在 Vue 原型上,位于 src/platforms/web/runtime/index.js,即
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
__patch__ 方法在平台 web 和 weex 其实现逻辑是不一样的;与此同时,可以看到,在 web 平台其实现还跟环境有关。如果所处环境是浏览器,则指向 patch 方法;如果所处环境是服务器,则指向空函数,因为服务器端没有真实 DOM,无需将虚拟 DOM 渲染成真实 DOM。
pathc 方法的实现位于 src/platforms/web/runtime/patch.js,即
/* @flow */
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 })
patch 方法的定义是调用 createPatchFunction 的返回值,这里传入一个参数,是一个对象,包含两个属性,分别为:nodeOps 和 modules。
nodeOps 封装了操作真实 DOM 的一系列方法,比如 createElement 、createElementNS 、createTextNode、createComment、insertBefore、appendChild 等方法;而 modules 定义一些模块的钩子函数实现,比如设置属性 attrs、事件 event、样式 style 等。
createPatchFunction 方法具体实现如下:
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
function emptyNodeAt (elm) { ... }
function createRmCb (childElm, listeners) { ... }
function removeNode (el) { ... }
function isUnknownElement (vnode, inVPre) { ... }
let creatingElmInVPre = 0
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) { ... }
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { ... }
function initComponent (vnode, insertedVnodeQueue) { ... }
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { ... }
function insert (parent, elm, ref) { ... }
function createChildren (vnode, children, insertedVnodeQueue) { ... }
function isPatchable (vnode) { ... }
function invokeCreateHooks (vnode, insertedVnodeQueue) { ... }
// set scope id attribute for scoped CSS.
// this is implemented as a special case to avoid the overhead
// of going through the normal attribute patching process.
function setScope (vnode) { ... }
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { ... }
function invokeDestroyHook (vnode) { ... }
function removeVnodes (vnodes, startIdx, endIdx) { ... }
function removeAndInvokeRemoveHook (vnode, rm) { ... }
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { ... }
function checkDuplicateKeys (children) { ... }
function findIdxInOld (node, oldCh, start, end) { ... }
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) { ... }
let hydrationBailed = false
// list of modules that can skip create hook during hydration because they
// are already rendered on the client or has no need for initialization
// Note: style is excluded because it relies on initial clone for future
// deep updates (#7063).
const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')
// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) { ... }
function assertNodeMatch (node, vnode, inVPre) { ... }
return function patch (oldVnode, vnode, hydrating, removeOnly) { ... }
}
createPatchFunction 定义一系列辅助方法,最终返回 patch 方法。也就是说,vm._update 函数里调用的 vm.__patch__,实际上调用此处的方法 patch,即 patch 过程的核心逻辑实现。
patch 内部实现
function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 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)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
patch 方法接收四个参数:
oldVnode:表示旧的 VNode,它可以是一个 DOM 对象,也可以是不存在的vnode:表示执行vm._render返回的 VNode 节点hydrating:表示是否为服务端渲染removeOnly:表示给transition-group使用
patch 的执行逻辑相对复杂,不同的场景有不同的处理方法。下面结合例子来分析首次渲染调用时的逻辑实现
new Vue({
render: h => h('div', {
attrs: {
id: 'app'
}
}, [
h('h1', '我是标题1'),
h('h2', '我是标题2')
]),
}).$mount('#app')
回顾下在 _update 方法调用 __patch__
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
在执行 patch 时,传入的参数 vm.$el 是 id 为 app 的 DOM 对象,而 vm.$el 的赋值是在 mountComponent 函数里面发生的;vnode 是执行 vm._render 函数渲染生成 VNode;hydrating 在非服务端情况下为 false,removeOnly 为 false。
确定入参后,我们就可以来看 patch 内部是如何实现的?
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
由于传入的参数 vnode 是上一步 vm._render 渲染生成的 VNode,此处不为空,跳过此逻辑。
传入的 oldVnode 是不为空,if 分支执行后为 false,则进入到 else 分支。此时 oldVnode 是一个真实 DOM,isRealElement 则为 true ,进入 else 分支。
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm // 保存真实 DOM 节点 div#app
const parentElm = nodeOps.parentNode(oldElm) // 保存当前节的父节点,此处指的是 body
// 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)
)
通过方法 emptyNodeAt 把 oldVnode 转换为一个 VNode 对象;声明变量 oldElm、parentElm 保存值;调用方法 createElm 创建新节点,此方法为核心方法,稍后分析。
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
由于创建了新节点,需要将旧节点删除。根据 createElm 递归生成的 vnode 插入顺序队列,执行相关的 insert 函数。
createElm 内部实现
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) { ... }
该方法接收 7 个参数:
vnode:表示执行vm._render返回的 VNode 节点insertedVnodeQueue:用来保存 vnode 已经转换为真实 DOM 的 vnode 队列parentElm:表示父节点refElm表示参考节点nested:表示是否嵌套ownerArray:数组节点,用于保存节点index:索引
作用是把虚拟节点 VNode 转换为真实的 DOM 节点,并且插入它的父节点。
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
在方法 patch 调用 createElm 时,没有传入参数 ownerArray,因此不满足条件,跳过此逻辑。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
尝试创建子组件,而在此场景下其值为 false,因此跳过此逻辑。
接着有三个判断逻辑,分别为:
- 判断
vnode是否包含tag,如果包含,则在开发环境中简单对其做合法性校验,且调用平台 DOM 的操作方法去创建一个占位符元素 - 判断
vnode是否为注释节点,如果是的话,则调用平台 DOM 操作方法创建注释节点,且将其插入到父元素中 - 如果以上两种都不满足的话,那么
vnode是一个文本节点,则调用平台 DOM 操作方法创建文本节点,并将其插入到父元素中
在此 case 中,tag 为 div,则进入到 tag 逻辑,代码实现如下:
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
}
在开发环境中,对 tag 做一些合法性校验,比如使用的组件是否注册,不符合的话则会在控制台抛出告警。接着调用平台操作 DOM 的方法创建一个占位符元素,赋值给 vnode.elm 。由于分析的平台是 web ,因此跳过 __WEEX__ 逻辑,直接进入 else 分支逻辑。
首先调用的方法是 createChildren ,具体实现如下:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(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)))
}
}
该方法接收三个参数:
vnode:表示执行vm._render返回的 VNode 节点children:vnode子节点insertedVnodeQueue:用来保存 vnode 已经转换为真实 DOM 的 vnode 队列
作用是遍历 vnode 子节点,递归调用方法 createElm 创建真实的 DOM,采用的是深度优先遍历算法。
这里有一处简单的校验,即在开发环境中,遍历检查节点设置的 key(如使用指令 v-for,需要对每个元素设置 key)是否重复。如果重复的话,则在控制台抛出告警。
调用 invokeCreateHooks 方法执行所有的 create 的钩子函数,并将已经转换为真实 DOM 的 vnode 保存到 insertedVnodeQueue 队列中,具体实现如下:
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
最后调用方法 insert 将真实 DOM 插入到父节点中,由于是递归调用,子节点会优先于父节点 insert,所以整个 vnode 树的插入顺序是先子后父。 看下方法 insert 的具体实现:
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
该方法接收三个参数:
parent:父节点elm:vnode转换成真实的 DOMref:参考节点
该方法的实现逻辑挺简单的,即在满足判断条件时,调用 nodeOps 的辅助方法将子节点插入到父节点中,辅助方法位于 src/platform/web/runtime/node-ops.js,具体实现如下:
export function parentNode (node: Node): ?Node {
return node.parentNode
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
从源码实现可以看出,创建真实 DOM 的过程中,其实调用的是原生 DOM 的 API 来操作的。
至此,我们就简单地分析了普通节点 patch 过程;同时数据和模板如何渲染成最终 DOM 也分析完了,借助 update 里提供的图片更直观地来描述从初始化 Vue 到整个渲染完成的过程: