【vue源码】虚拟DOM和diff算法
虚拟DOM 与 AST抽象语法树的区别
虚拟DOM是表示页面状态的JavaScript对象,(虚拟DOM只有实现diff算法)
而AST是表示代码语法结构的树形数据结构。
虚拟DOM通常用于优化页面渲染性能,
而AST通常用于进行代码静态分析、代码转换等操作。(AST主要执行compiler编译)
什么是虚拟DOM?
用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。
本质:真实dom的一个js对象
。
vue源码借鉴了snabbdom
render() 实现虚拟dom,h函数渲染虚拟dom的函数,返回的是一个 vnode对象
diff算法
一、 什么是diff算法?
diff算法本质:就是比较2个JS对象的差异
。找到2个js对象的差异
,最小化更新视图
。
二、 diff算法比较:
1.只比较同层级的节点。
2.同层比较时,如果类型不同,会把该节点和该节点的所有子节点全部销毁。
3.类型相同时,使用key合理性能优化
三、 diff算法主要通过patch(oldVnode,newVnode)方法实现
patch函数
- patch函数比较新旧虚拟节点,进行diff算法,更新。
- patch判断是不是首次渲染?
是首次渲染直接createElement。否则sameVnode
判断元素类型是否相同,不同则直接替换, 类型相同则执行核心函数patchVnode
.
// 用于 比较 新老节点的不同,然后更新的 函数
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 当新节点不存在的时候,销毁旧节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
// 用来存储 insert 钩子函数,在 插入节点之后调用
const insertedVnodeQueue = []
// 如果旧节点 是未定义的,直接创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 当老节点不是真实的 dom 节点, 当两个节点是相同节点的时候,进入 patctVnode 的过程
// 而 patchVnode 也是 传说中 diff updateChildren 的调用者
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 当老节点是真实存在的 dom 节点的时候
if (isRealElement) {
// 当 老节点是 真实节点,而是在 ssr 环境的时候,修改 SSR_ATTR 属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
....
// 设置 oldVnode 为一个包含 oldVnode 的无属性节点
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
// 获取父亲节点,这样方便 删除或者增加节点
const parentElm = nodeOps.parentNode(oldElm)
// 在 dom 中插入新节点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 递归 更新父占位符元素
// 就是执行一遍 父节点的 destory 和 create 、insert 的 钩子函数
// 类似于 style 组件,事件组件,这些 钩子函数
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)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
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)) {
// 触发老节点 的 destory 钩子
invokeDestroyHook(oldVnode)
}
}
}
// 执行 虚拟 dom 的 insert 钩子函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回最新 vnode 的 elm ,也就是真实的 dom节点
return vnode.elm
}
patchVnode
patchVnode函数对比 两个虚拟dom不同
的地方, 同时 也是 updateChildren
调用递归
的 函数
function patchVnode (
oldVnode, // 旧节点
vnode, // 新节点
insertedVnodeQueue, // 插入节点的队列
ownerArray, // 节点 数组
index, // 当前 节点的
removeOnly // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
) {
// 如果新节点和旧节点 相等(使用了 同一个地址,直接返回不进行修改)
// 这里就是 当 props 没有改变的时候,子组件不会做渲染,而是直接复用
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// 当 当前节点 是 注释节点(被 v-if )了,或者是一个 异步函数节点,那不执行
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 当前节点 是一个静态节点的时候,或者 标记了 once 的时候,那不执行
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 调用 prepatch 的钩子函数
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
// 调用 update 钩子函数
if (isDef(data) && isPatchable(vnode)) {
// 这里 的 update 钩子函数式 vnode 本身的钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 这里的 update 钩子函数 是 用户传过来的 钩子函数
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 新节点 没有 text 属性
if (isUndef(vnode.text)) {
// 如果都有子节点,对比更新子节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 新节点存在,但是老节点不存在
// 如果老节点是 text, 清空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 增加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 老节点存在,但是新节点不存在,执行删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) { // 如果老节点是 text, 清空
nodeOps.setTextContent(elm, '')
}
// 新旧节点 text 属性不一样
} else if (oldVnode.text !== vnode.text) {
// 将 text 设置为 新节点的 text
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 执行 postpatch 钩子函数
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
updateChildren
采用双端比较
,4个指针来实现 。旧节点的2个头尾指针, 新节点的2个头尾指针。
头头 、 尾尾 、头尾 、尾头
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 旧节点头指针
let oldStartIdx = 0
// 新节点头指针
let newStartIdx = 0
// 旧节点尾指针
let oldEndIdx = oldCh.length - 1
// 新节点尾指针
let newEndIdx = newCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 遇到空,指针移动,开始指针向后移动,结束指针向前移动
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// 【头头】新前和旧前是相同节点,指针后移
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 继续深度patch
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 【尾尾】新后和旧后是相同节点,指针前移
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 继续深度patch
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 【头尾】旧前和新后相等,将旧前指针对应的节点移动到新后指向节点的后面,且旧前指针向后移动,新后指针向前移动
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
//【尾头】 旧后和新前相等,将旧后指针对应的节点移到新前指针对应节点的前面,且旧后指针向前移动,新前指针向后移动
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 若上述条件都不符合
} else {
// 首先将旧数组转换成 节点 key-下标 的map (key值是唯一的)
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 判断当前新前指针指向节点是否存在key,若存在直接在旧数组对应的map中找到该key对应的下标,若不存在遍历旧数组中剩余的节点,找到新节点对应匹配的下标
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果下标为空,代表该新节点是一个新节点,需要新增节点
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 若不为空则把对应的旧节点和新节点比较,看两个节点是否是相同节点
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 若相同,将当前旧节点位置置空,并把旧节点移动到旧前的前一位
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 若不同,则新建一个节点
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 向后移动新前指针
newStartVnode = newCh[++newStartIdx]
}
}
// 如果旧前>旧后,而新前和新后还没重叠,则新增新前到新后之间的节点
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 如果新前>新后,而旧前和旧后还没重叠,则删除旧前到旧后之间的节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
createElm,将 vnode 转换为 真实的 dom 节点
- 判断当前的 vnode
tag
标签是否是存在的 - 如果存在,创建对应的节点,然后 设置
样式的 作用域
- 遍历子元素,并
插入
节点之中 - 触发
create
钩子函数 - 如果tag 标签不存在,判断是否是
注释节点
,然后创建
- 如果tag 标签不存在,且
不是 注释节点
,直接创建文本节点
// 把 vnode 转换为 真实的 dom,挂载到 节点上
function createElm (
vnode, // vnode
insertedVnodeQueue, // inserted 钩子函数
parentElm,
refElm, // 如果这个存在的话,就插到这个节点之前
nested,
ownerArray,
index
) {
// 如果存在子节点的话,就会克隆一遍
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)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
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
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode) // 设置样式的作用域
/* istanbul ignore if */
if (__WEEX__) {
。。。
} else {
// 把子元素设置为 vnode 的对象
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)) { // 是否是注释节点
vnode.elm = nodeOps.createComment(vnode.text) // 创建 注释的文本 节点
insert(parentElm, vnode.elm, refElm)
} else { // Text 标签
vnode.elm = nodeOps.createTextNode(vnode.text) // 创建 文本节点
insert(parentElm, vnode.elm, refElm)
}
}
一步一叩首,今天的自己比昨天好一点就行,明天的自己需追寻