写在前面
本篇是从零实现vue2系列第五篇,将 YourVue 实例的 render 函数转换成真实 dom 和更新算法。
文章会最先更新在公众号:BUPPT。
正文
上篇文章我们把 render 函数挂在了 options 属性上,执行 render() 就可以得到 template 对应的虚拟 dom 树了。
export default class YourVue{
update(){
if(this.$options.template){
if(this._isMounted){
const vnode = this.$options.render()
patch(this.vnode, vnode)
this.vnode = vnode
}else{
this.vnode = this.$options.render()
let el = this.$options.el
this.el = el && query(el)
patch(this.vnode, null, this.el)
this._isMounted = true
}
}
}
}
Vue 将虚拟 dom 转换成真实 dom 有两种阶段,一个是 mount,一个是 update。都是通过 patch 函数来操作 dom 的。
export function patch (oldVnode, vnode, el) {
if(isUndef(vnode)){
createElm(oldVnode, el)
return
}
if (oldVnode === vnode) {
return
}
if(sameVnode(oldVnode, vnode)){
patchVnode(oldVnode, vnode)
}else{
const parentElm = oldVnode.elm.parentNode;
createElm(vnode,parentElm,oldVnode.elm)
removeVnodes(parentElm,[oldVnode],0,0)
}
}
如果是 mount 阶段,会执行 createElm,如果是 update 阶段,先判断两个根节点的 vnode 是否相同,如果不同则直接创建新的 dom,如果相同则执行 patchVnode。
先看 mount 阶段的 createElm,就是createElement 和 setAttribute,updateListeners 就是第一篇文章中事件绑定到 dom 的方法。最后将生成的 dom 插入到指定的位置。
function createElm (vnode, parentElm, afterElm = undefined) {
let element
if(!vnode.tag && vnode.text){
element = document.createTextNode(vnode.text);
}else{
element = document.createElement(vnode.tag)
if(vnode.props.attrs){
const attrs = vnode.props.attrs
for(let key in attrs){
element.setAttribute(key, attrs[key])
}
}
if(vnode.props.on){
const on = vnode.props.on
const oldOn = {}
updateListeners(element, on, oldOn, vnode.context)
}
for(let child of vnode.children){
if(child instanceof VNode){
createElm(child, element)
}else if(Array.isArray(child)){
for (let i = 0; i < child.length; ++i) {
createElm(child[i], element)
}
}
}
}
vnode.elm = element;
if(isDef(afterElm)){
insertBefore(parentElm, element, afterElm)
}else if(parentElm){
parentElm.appendChild(element)
}
return element;
}
update 阶段,就是对比两棵虚拟 dom 树的阶段。Vue 对比两棵虚拟 dom 树时是按层对比的,如果根节点相同,判断 children 是否相同:
- 如果新树有 child 旧树没有,则新建 child
- 如果新树没有 child,旧树没有,则删掉 child
- 如果都有 children,就到了虚拟 dom 中非常有名的 diff 算法。
function patchVnode(oldVnode, vnode){
if (oldVnode === vnode) {
return
}
const ch = vnode.children
const oldCh = oldVnode.children
const elm = vnode.elm = oldVnode.elm
if(isUndef(vnode.text)){
if(isDef(ch) && isDef(oldCh)){
updateChildren(elm,oldCh,ch)
}else if(isDef(ch)){
if (isDef(oldVnode.text)) setTextContent(elm, '')
addVnodes(oldVnode, ch, 0, ch.length - 1)
}else if(isDef(oldCh)){
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
}else{
setTextContent(elm, vnode.text);
}
}
diff 算法步骤比较多,但是也都不复杂,核心思想就是使用四个指针分别指向新 children 和旧 children 数组的头和尾,尽量找到和新树相同的节点,通过移动进行元素复用,将旧树变换成新树的结构,减少新建 dom 节点的操作。
当头和头,尾和尾都不同时,先比较旧树的头和新树的尾,如果相同,就把旧树的头指针指向的节点移动到尾指针指向节点的后面。旧头指针后移,新尾指针前移。
当前面都不同,旧树的尾和新树的头相同时,把旧树的尾移动到旧树的头前面,旧尾指针前移,新头指针后移。
当头尾指针都不同的时候,vue 还会遍历旧树剩余节点的 key 与新树的头节点的 key 进行比较,也就是 v-for 时必须要写的 key 的值,如果有相同的 key,就将旧树的节点移到旧头前面。
如果都没有,就在旧树的头前面新建新树的头节点,新树头指针后移。
最后当旧树头尾指针相遇,新树头尾指针之间仍有元素节点时,新建这些节点。
当新树头尾指针相遇,旧树头尾指针之间还有元素时,删除这些节点。
这样就通过元素节点的移动和新建,将旧的 dom 结构转换成新的 dom 树结构啦!理解思路后,再看代码就清晰了。
function updateChildren(parentElm, oldCh, newCh,){
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
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
createElm(newStartVnode, parentElm, oldStartVnode.elm)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
insertBefore(parentElm,vnodeToMove.elm, oldStartVnode.elm)
} else {
createElm(newStartVnode, parentElm, oldStartVnode.elm)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, newCh, newStartIdx, newEndIdx, refElm)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
虚拟 dom 完成实现。综合本篇和上篇文章的代码:
求关注~ 求star~