前言
前面用了两篇文章,讲虚拟 DOM
是如何生成的。终于到了如何将虚拟 DOM
渲染成真实 DOM
的部分了。
回顾下之前的mountComponent
函数,中间有行代码如下:
vm._update(vm._render(), hydrating)
vm._render()
,我们已经知道是如何生成虚拟 DOM
的了。接下来,我们看看vm._update
是如何将虚拟 DOM
渲染成真实的 DOM
的。
_update
_update
的主要代码如下:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
...
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
可以看到,_update
主要调用了__patch__
方法。暂时还没有看过diff
算法,就先分析首次的渲染吧。
_update
主要是调用了__patch__
方法,并将该方法所返回的结果,覆盖之前的vm.$el
。
__patch__
先找到__patch__
方法在哪里定义的。找到之后,就是下面这样:
Vue.prototype.__patch__ = inBrowser ? patch : noop
这段很好理解,当前环境是在浏览器中时,将patch
赋值给 __patch__
。
这时,我们不仅又要问 patch
又是什么呢?
export const patch: Function = createPatchFunction({ nodeOps, modules })
可以看到,patch
就是 createPatchFunction
方法。
所以,_update
实际上相当于是调用了createPatchFunction
方法来生成真实的DOM
。需要注意的地方是{ nodeOps, modules }
分别是什么。
-
nodeOps:封装了一些原生
DOM
操作方法。像DOM
的创建、插入、移除等都是封装在这里面的。 -
modules:封装了对原生
DOM
的特性进行操作的方法。如class/styles
的添加、更新等等。 -
createPatchFunction
export function createPatchFunction(backend) {
...
return function patch(oldVnode, vnode, hydrating, removeOnly) {
const isRealElement = isDef(oldVnode.nodeType)
// 是真实的 DOM 时,则将真实的 DOM 转为虚拟 DOM
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
// 获取旧节点的父元素
const parentElm = nodeOps.parentNode(oldElm)
// 创建真实 DOM
createElm(
vnode, // 新的虚拟节点
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm, // 要插入的父节点
nodeOps.nextSibling(oldElm) // 下一个节点
)
// 返回真实的 DOM,代替之前的 vm.$el
return vnode.elm
}
}
在第一次的渲染时,旧的虚拟节点oldVnode
就是vm.$el
,是真实的DOM
,会被转为对应的虚拟DOM
。然后在获取到oldVnode
的父元素之后,将新的虚拟节点vnode
转为真实的DOM
。
createElm
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
// 创建组件节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 是元素节点
if (isDef(tag)) {
// 先创建最外层的元素
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
// 创建最外层元素内部的节点
createChildren(vnode, children, insertedVnodeQueue)
// 将创建好的元素插入到父节点
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)
}
...
}
生成真实的DOM
主要是这个函数,分为创建组件节点和创建普通节点。
-
创建普通节点
创建普通节点又分为创建元素节点、注释节点和文本节点。
- 元素节点
// 创建元素节点
if (isDef(tag)) {
// 先创建最外层的元素
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
// 创建最外层元素内部的节点
createChildren(vnode, children, insertedVnodeQueue)
// 将创建好的元素插入到父节点
insert(parentElm, vnode.elm, refElm)
}
----------------------------------------------------
// createChildren 方法
function createChildren(vnode, children, insertedVnodeQueue) {
// 数组的话,遍历 children,然后递归调用 createElm 方法
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)) {
// 文本节点类型, 如 string/number/symbol/boolean,创建并插入到父节点
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
-----------------------------------------------------
// 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)
}
}
}
在创建时,先创建最外层的元素,而后创建外层元素内部的元素,如果内部元素是数组组成的虚拟DOM,则递归遍历数组。如果内部元素是文本元素类型(如:string/number/boolean/symbol),则创建文本节点,然后插入到外层元素下面。最后在将外层元素插入到父节点下。
- 注释节点
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)
}
其他情况下,均是创建文本节点,然后插入到父元素下。
整个流程如下:
还有一部分是创建组件节点。这个我们放到下一节将。
总结
- 在首次渲染时,将
虚拟DOM
转为真实的DOM
,vm._update
会调用__patch__
方法,而patch
方法实际上则是封装了createPatchFunction
方法。 - 在
createPatchFunction
方法中,先获取旧节点的父元素,然后将虚拟DOM转为真实DOM,插入到旧节点的父元素下。 - 在将虚拟DOM转为真实DOM时,又将虚拟DOM分为组件类型的虚拟DOM、和普通的虚拟DOM。不同类型的DOM,处理方式也不一样。组件类型的,我们放在下节单独讲。
- 而普通的虚拟DOM,处理方式如上面的流程图所示。