Vue 的渲染过程:
上一篇写了 render 函数生成 VNode 的过程,这一篇就来看看 VNode 是怎么渲染成真实 DOM 的;
在初次渲染和更新的时候 Vue 都会调用 _update
方法来将 VNode 渲染成真实的 DOM。_update
方法是 Vue 的一个私有方法,在初始化的时候将这个方法添加到 Vue 的原型当中,在 src/core/instance/lifecycle.js
文件中:
//_update 方法用来更新组件内容、也是 patch 的入口
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
//缓存更新之前元素的 $el 和 _vnode
const prevEl = vm.$el
const prevVnode = vm._vnode
//当前的实例设置为活跃实例
const restoreActiveInstance = setActiveInstance(vm)
//传入最新的vnode
vm._vnode = vnode
// prevVnode不存在说明是第一次渲染
// __patch__主要是将vnode转换成dom,渲染在视图中
if (!prevVnode) {
// 最初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 再次渲染(更新)
vm.$el = vm.__patch__(prevVnode, vnode)
}
//调用return里面的方法返回原来的活跃实例
restoreActiveInstance()
// 之前的el添加 _vue_属性并设置为null
if (prevEl) {
prevEl.__vue__ = null
}
//__vue__ 指向更新时接收到的 vue 实例
if (vm.$el) {
vm.$el.__vue__ = vm
}
// 如果当前实例的父级 $parent 是 高阶组件,那么也更新其 $el
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
_update
的核心就是调用 _patch_
方法,首次渲染和更新调用 _patch_
方法的参数是不一样的,首次渲染提供的根节点是真实的 DOM,更新的时候提供的是一个 vnode;在不同的平台 _patch_
方法定义是不一样的:
Vue.prototype.__patch__ = inBrowser ? patch : noop
不是浏览器环境,不需要把 VNode 转换成 DOM,所以给了一个空函数;是浏览器环境则指向 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: Function = createPatchFunction({ nodeOps, modules })
patch
方法是 createPatchFunction
方法的返回值,参数 nodeOps 封装了一些 DOM 操作方法,在 /srcplatforms/web/runtime/node-ops.js
文件中
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// False或null将删除该属性,但undefined不会
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
export function parentNode (node: Node): ?Node {
return node.parentNode
}
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
export function tagName (node: Element): string {
return node.tagName
}
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
export function setStyleScope (node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
node-ops.js
文件中封装的方法,实际上就是对真实 DOM 操作的一层封装,传递 nodeOps 的目的是为了在 VNode 转成真实 DOM 节点的过程中提供便利;
modules
是 platformModules
和 baseModules
两个数组合并的结果,其中baseModules
是对模板标签上 ref 和 directives 各种操作的封装;platformModules
是对模板标签上class、style、attr 以及 events 等操作的封装;
下面看一下createPatchFunction
方法的实现,在 src/core/vdom/patch.js
文件中:
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
//取出 modules 中和 hooks 数组里面的同名的钩子函数
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]])
}
}
}
//....方法太多隐藏....
return function patch (oldVnode, vnode, hydrating, removeOnly) {
//vnode不存在调用删除钩子删除节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
//oldVnode不存在,直接创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
//vnode和oldVnode都存在
//判断是否为真实DOM元素
const isRealElement = isDef(oldVnode.nodeType)
//不是第一次渲染,并且两个节点是同一个节点
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 是同一个节点的时候直接修改现有的节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// vnode和oldVnode不是同一个的情况
// 是真实dom
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
//当旧的VNode是服务端渲染的元素,hydrating记为true
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
//需要用hydrate函数将虚拟DOM和真实DOM进行映射
if (isTrue(hydrating)) {
//需要合并到真实Dom上
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
//调用insert钩子
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.'
)
}
}
//如果不是服务端渲染或者合并到真实Dom失败,则创建一个空的VNode节点替换它
oldVnode = emptyNodeAt(oldVnode)
}
// 获取父节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 根据vnode创建一个真实DOM节点并挂载至oldVnode的父节点下
createElm(
vnode,
insertedVnodeQueue,
// 如果旧元素处于离开过渡,则不要插入;只有在结合过渡+keep-alive + hoc时才会发生
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
//组件根节点被替换,遍历更新父节点element
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)
}
// 调用可能已被create钩子合并的插入钩子。
const insert = ancestor.data.hook.insert
if (insert.merged) {
// 从索引1开始,以避免重新调用组件挂载钩子
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 销毁老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
//调用insert钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
createPatchFunction
内部定义大量的辅助方法,最终返回 patch
方法,这个方法有四个参数:
oldVnode:_update 时从 vm 上获取 prevVnode 或者直接是 vm.$el(真实dom);
vnode:_render 函数返回;
hydrating :布尔值是否服务端渲染;
removeOnly: transition-group用的参数;
这个 patch
方法就是在 src/platforms/web/runtime/index.js
文件中赋值给 Vue.prototype.__patch__
的方法,patch
方法的流程:
1、vnode 不存在,oldVnode 存在,调用
invokeDestroyHook
方法销毁 oldVnode;
2、oldVnode 不存在,说明是最初挂载,将 isInitialPatch 标记置为 true,调用了createElm(vnode, insertedVnodeQueue)
方法创建一个新的根元素;
3、oldVnode 、vnode 都存在:
①、不是第一次渲染 并且是相同节点,这里有个判断是同一个节点的方法sameVnode
然后调用patchVnode
方法对节点进行比对;
②、如果 vnode 和 oldVnode 不是同一个节点,那么根据 vnode 创建新的元素并挂载至 oldVnode 父元素下。如果组件根节点被替换,遍历更新父节点element。然后移除旧节点。如果 oldVnode 是服务端渲染元素节点,需要用hydrate
函数将虚拟dom和真是dom进行映射;
下面详细介绍一下 patch 过程中用到的几个重要的方法:
invokeDestroyHook
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
//节点上data存在,并且
if (isDef(data)) {
//data中有hook,hook有destory方法,则直接调用这个方法删除当前节点
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
//否则循环cbs里面的destroy数组,来删除当前节点
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
//有子节点,递归调用invokeDestroyHook来删除子节点
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
invokeDestroyHook
调用 destory
方法来销毁当前节点;节点的 data 里面有 destory
直接调用,没有则借助 cbs 里面的 destory
;
sameVnode
//入参 oldVnode, vnode
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
key 必须一致,然后( tag 、isComment、data 相同、是 input 标签时 type 相同 )或者( 老节点的 isAsyncPlaceholder 为 true、两个的 asyncFactory 相同、新节点的 asyncFactory 的 error 不存在) 则为同一个节点;这也看出来 key 在节点比对的过程中的重要性,所以在我们给节点设置 key 值的时候一定要确保唯一性和稳定性。
patchVnode
sameVnode
判断是同一节点之后, 对节点进行比对,看看是否复用当前元素;
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
//新旧节点相同不做处理
if (oldVnode === vnode) {
return
}
//updateChildren方法中递归调用patchVnode时用于克隆节点
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 克隆复用节点
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
//ssr渲染
if (isTrue(oldVnode.isAsyncPlaceholder)) {
//调用hydrate将虚拟dom和真是dom进行映射
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 两个节点都是静态节点、key相同、新节点是被克隆或者是v-once节点,则新节点替换老节点的组件实例
//如果新节点没有被克隆,这意味着渲染函数已经被热加载api重置
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
//如果存在data.hook.prepatch则要先执⾏
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
//获取子节点
const oldCh = oldVnode.children
const ch = vnode.children
//属性更新
if (isDef(data) && isPatchable(vnode)) {
//遍历调用 cbs.update 钩子函数,更新oldVnode所有属性
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
//如果这个VNode节点没有text文本时
if (isUndef(vnode.text)) {
//都有子节点并且子节点不相同调用 updateChildren
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
//vnode有child,oldVnode没有child
//检查
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
//oldVnoe是文本节点,清空文本内容;并为当前节点添加子节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
//只有oldVode有child 则直接删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
//oldVode是文本节点,vnode没有则清空文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
//两个节点都有文本但是不相同则用新的替换老的
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
//执行data.hook.postpatch钩子
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
(isAsyncPlaceholder 是 ssr 渲染时才会赋值给“异步占位符)
patchVnode
的流程:
1、如果 oldVnode 和 vnode 是同一个对象,直接返回,不需要更新;
2、如果 oldVnode 是 ssr渲染,调用hydrate
将虚拟dom和真是dom进行映射然后返回;
3、如果 oldVnode 和 vnode 都是静态节点,key 相同(同一节点),那么只需要替换 componentInstance 即可;
4、用 vnode 来更新 oldVnode 的属性,包括attrs、class、domProps、events、style、ref、directive;
5、如果 vnode 没有文本节点:
①、oldVnode 和vnode 都有子节点,但是是两个的子节点不相同时调用updateChildren
方法比对;
②、如果只有 vnode 有子节点则清空 oldVnode 的文本并以addVnodes
创建子节点;
③、如果只有 oldVnode 有子节点则删除 oldVnode 的子节点;
④、如果 oldVnode 是文本节点则清空文本;
6、 如果 oldVnode 和 vnode 的文本不相等,用 vnode 替换 oldVnode ;
updateChildren
如果 vnode 和 oldVnode 的 children 不相等会调用这个方法对子节点进行更新;
//diff核心方法,比较优化
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly是使用<transition-group>的一个特殊标志,以确保在离开转换期间被移除的元素保持在正确的相对位置
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
//开始循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
//oldStartVnode 不存在了则右移一位
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
//oldEndVnode 不存在了则左移一位
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
//oldVnode 和 vnode 开始节点相同,调用patchVnode处理,同时右移一位
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
//oldVnode 和 vnode 结束节点相同,调用patchVnode处理,同时左移一位
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
//oldVnode 的开始和 vnode 的结束相同,oldVnode右移,vnode左移一位
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 把oldStart节点放到oldEnd节点后面
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
//oldVnode 的结束和 vnode 的开始相同,oldVnode左移,vnode右移一位
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 把oldEnd节点放到oldStart节点前面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
//根据key值来复用元素
//找出所有 oldVnode 有 key 的节点做一个映射
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
//vnode第一个节点有key则在 oldKeyToIdx 找到相同的key并返回
//找不到则循环 oldVnode 比对每一项和 vnode的第一个是否相同相同则返回节点下标
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
//没有找到vnode相同的则创建新的子节点
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
//获取同key的老节点
vnodeToMove = oldCh[idxInOld]
//是相同节点
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
//去除老节点
oldCh[idxInOld] = undefined
//将oldStartVnode放到oldCh[idxInOld]前面
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 相同的key,但不同的元素。视为新元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
//newStartVnode 右移
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
//全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多
//所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
//如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点
//这个时候需要将多余的老节点从真实Dom中移除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
updateChildren
方法的流程:
1、首先是循环终止条件确定:oldCh 或 newCh 遍历结束后停止循环;
2、找不到 oldStartVnode 则右移一位,找不到 oldEndVnode 则左移一位;
3、oldCh 开始对比 newCh 开始;
4、oldCh 结尾对比 newCh 结尾;
5、oldCh 的开始和 newCh 的结尾;
6、oldCh 的结尾和 newCh 的开始;
// 相同则patchVnode
比对,移动节点位置(以 newCh 节点下标位置为准),移动下标
7、遍历 oldCh 将有 key 的节点的 key 值取出来维护一个对应的表 oldKeyToIdx ;
8、如果newStartVnode.key
存在则在 oldKeyToIdx 中寻找相同 key 值的节点返回给 idxInOld ;不存在则遍历 oldCh 每一项和 newStartVnode 通过sameVnode
比较是否是相同节点并返回给 idxInOld ;
9、idxInOld 没值说明 newStartVnode 没有找到相同节点则创建一个新的子节点;
10、idxInOld 有值则取出相同key的老节点,然后再次sameVnode
比较是否是相同节点,防止 key 相同但是 element 不相同,是相同节点则调用patchVnode
比对,将老节点对应值置为 undefined ,移动新节点位置;不相同则创建新节点
11、移动 newStartVnode 位置;
12、全部比较完成以后,发现oldStartIdx > oldEndIdx
,说明老节点已经遍历完了,新节点比老节点多所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中;
13、如果全部比较完成以后发现newStartIdx > newEndIdx
,则说明新节点已经遍历完了,老节点多余新节点这个时候需要将多余的老节点从真实Dom中移除;
问题:为什么会有头对尾、尾对头的操作?
可以快速检测出reverse操作,加快diff效率。
位置的变换:
1、头对头、尾对尾的比较,相同位置不变
2、头对尾,尾对头的比较,相同则根据 vnode 节点的位置下标来移动 oldVnode 节点
3、然后以 vnode 的 newStartVnode 节点开始,在 oldVnode 里面找相同的节点,有相同的则将节点移动到oldStartVnode.elm
前面;否则在oldStartVnode.elm
前面位置创建新节点;
这里有一个 oldStartVnode.elm
位置有点不好理解,我们以下图为例:
开始的时候 oldStartVnode.elm
是 A ,循环之后看到 A 都在开始位置,并且相同,所以 A 节点不用动,这个时候 oldStartVnode.elm
变成了 D ,newStartVnode 变为 E;
再次循环,用 newStartVnode 去 oldVnode 中找相同的节点,oldVnode 中没有 E ,所以在 oldStartVnode.elm
前面新建一个子节点 F,这个时候的 oldStartVnode.elm
是 D,newStartVnode 变为 C;
再次循环,oldVnode 中有 C,则将 oldVnode 中的 C 设置为 undefined ,将缓存的 C 放到 D 前面,这个时候的 oldStartVnode.elm
是 D,newStartVnode 变为 B;
再次循环,oldVnode 中有 B,则将 oldVnode 中的 B 设置为 undefined ,将缓存的 B 放到 D 前面,这个时候的 oldStartVnode.elm
是 D,newStartVnode 变为 G;
再次循环,oldVnode 中没有 G,在 D 之前新建一个 G;然后 vnode 循环结束,整个循环终止;得到的结果:
按照 vnode 里面子节点把 oldVnode 里面子节点重新排列,能复用的直接复用,不能复用的创建新的;可以看到比对后的 oldVnode 里面很多是 vnode 里面没有的节点,这个时候会判断 newStartIdx > newEndIdx
表示老节点有多余的,然后要除这些多余的节点;
注意:Vue 的 diff 都是同级进行比较。
createElm
创建真实 DOM 元素的方法
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
//这个vnode在之前的渲染中使用过,现在它被用作一个新节点,当它被用作插入参考节点时,覆盖它的elm会导致潜在的补丁错误。
//所以,我们在为节点创建相关的DOM元素之前按需克隆节点
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // 过渡进入检查
//如果是组件节点则创建,并return
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
//是否存在tag
const data = vnode.data
const children = vnode.children
const tag = vnode.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
)
}
}
//是否有命名空间,主要是 svg,创建对应的element
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 设置样式的作用域
setScope(vnode)
//weex相关
if (__WEEX__) {
// 在Weex中,默认的插入顺序是parent-first。通过使用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 {
//非weex 循环创建子节点
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 触发 create 钩子函数
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 将创建好的 vnode 插入到 parent 中,如果 refElm 存在的话,就插入到 refElm 元素之前
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
//注释节点
} else if (isTrue(vnode.isComment)) {
// 创建注释的文本节点,并插入parent 中
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
//文本节点,创建文本节点并插入到 parent 中
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
createElm
的作⽤是通过虚拟节点创建真实的 DOM 并插⼊到它的⽗节点中;流程如下:
1、首先判断是不是组件节点是则直接调用
createComponent
创建组件节点,并 return ;
2、如果 tag 存在说明是普通节点,在判断是不是未知标签,是则报错;
3、不是未知标签,根据命名空间创建对应的元素,然后设置样式的作用域,创建子节点,触发creat
钩子函数;
4、tag 不存在则判断是不是注释节点或者文本节点,然后创建对应的节点并调用insert
插入到 parent 中;
在完成组件的整个 patch 过程后,最后执⾏ insert(parentElm, vnode.elm, refElm)
完成组件 的 DOM 插⼊,如果组件 patch 过程中⼜创建了⼦组件,那么DOM 的插⼊顺序是先⼦后⽗。
问题:可以看到代码中全程维护一个 insertedVnodeQueue 数组,那么这个数组是做什么的呢?
insertedVnodeQueue 就是在一次 patch 过程中维护的插入的 vnode 的队列,它会收集待执行 mounted 钩子函数的 vnode 节点,在一次 patch 过程中,可能会有子组件,出现递归渲染子组件的过程,但是整个过程是同步的,子组件的 vnode 仍然会收集到 insertedVnodeQueue。而且过程中子组件的 vnode 会先添加进 insertedVnodeQueue,所以最终先执行子组件的 mounted,再执行父组件的 monted。
以上就是 Vnode 到真实 DOM 的具体过程了,简单复述一下:在首次渲染或者更新的时候,通过_render
拿到 Vnode 通过 _update
来触发对 vnode 的比对 patch,这里也可以说是 diff 算法;patch 对同级的 vnode 进行比对,然后对节点进行复用、替换、删除等操作,让最新的 vnode 节点以最少的消耗渲染到真实 DOM 中去;