Vue之虚拟DOM和diff算法

Vue之虚拟DOM和diff算法

snabbdom简介

  • snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom
  • 官方git:https://github.com/snabbdom/snabbdom
  • 在git上的snabbdom源码是用TypeScirpt写的,git上并不提供编译好的JavaScript版本
  • 如果要直接使用build出来的JavaScript版本的snabbdom库,可以从npm上下载

虚拟DOM和h函数

  • 虚拟DOM:用JavaScript对象描述DOM的层次结构,DOM中的一切属性都在虚拟DOM中有相对应的属性

  • diff发生在虚拟DOM上的

  • 新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上

  • h函数用来产生虚拟节点(vnode)

    // 1.调用h函数
    h('a', {props: {href: 'https://www.baidu.com' }}, '百度')
    // 2.得到这样的虚拟dom节点
    {
      "sel": "a",
      "data": {
         props: {
           href: 'https://www.baidu.com'
         } 
       },
      "text": "百度"
    }
    
    // 3.他表示的真正的DOM节点
    <a href="https://www.baidu.com">百度</a>
    
  • 虚拟节点的属性

    {
        children: undefined // 子元素
        data: {} // 属性
        elm: undefined // 对应的真正的dom节点
        key: undefined // 该dom节点的唯一标识
        sel: "dev" // 选择器
        text: "我是一个盒子" // 文字
     }
    
  • h函数可以嵌套使用,从而得到虚拟DOM树(重要)

    // 1.比如这样使用h函数
    h('ul', {}, [
      h('li', {}, '牛奶'),
      h('li', {}, '咖啡'),
      h('li', {}, '可乐'),
    ])
    
    // 2.将得到这样的虚拟DOM树
    {
    	"sel": "ul",
    	"data": {},
    	"children": [
    		{
    			"sel": "li",
    			"text": "牛奶"
    		},
    		{
    			"sel": "li",
    			"text": "咖啡"
    		},
    		{
    			"sel": "li",
    			"text": "可乐"
    		}
    	]
    }
    

模拟演示 ( 需要npm install -D snabbdom )

import {
    init
} from 'snabbdom/build/init'
import {
    classModule
} from 'snabbdom/build/modules/class'
import {
    propsModule
} from 'snabbdom/build/modules/props'
import {
    styleModule
} from 'snabbdom/build/modules/style'
import {
    eventListenersModule
} from 'snabbdom/build/modules/eventlisteners'
import {
    h
} from 'snabbdom/build/h'


// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule])

// 创建虚拟节点
const myVnode1 = h('a', {
    props: {
        href: 'https://www.baidu.com',
        target: '_blank'
    }
}, '百度')
console.log(myVnode1)

const myVnode2 = h('div', {
    class: {
        'box': true
    }
}, '我是一个盒子')
console.log(myVnode2)

const myVnode3 = h('ul', [
    h('li', '苹果'),
    h('li', '香蕉'),
    h('li', [
        h('div', [
            h('p', '一个p'),
            h('p', '两个p')
        ])
    ]),
    h('li', '葡萄')
])
console.log(myVnode3)

// 让虚拟节点上树
const app = document.getElementById('app')
patch(app, myVnode3)

手写h函数

// vnode.js
// 函数功能就是把传入的5个参数组合成对象返回
export default function vnode(sel, data, children, text, elm) {
    return {
        sel,
        data,
        children,
        text,
        elm
    }
}
// h.js
import vnode from './vnode.js'
// 编写一个简单的h函数,只能接受3个参数
export default function h(sel, data, c) {
    // 检查参数的个数
    if (arguments.length !== 3) {
        throw new Error('这个手写的简单h函数只能穿入三参数!')
    }
    // 检查c的类型
    if (typeof c == 'string' || typeof c == 'number') {
        // 说明现在调用h函数是这样的 h('div', {}, '文字')
        return vnode(sel, data, undefined, c, undefined)
    } else if (Array.isArray(c)) {
        // 说明现在调用h函数是这样的 h('div', {}, [])
        let children = []
        // 遍历c
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个对象, 如果不满足
            if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
                throw new Error('传入的数组参数中,有项的结果不是h函数')
            }
            // 这里不用执行c[i] 因为测试语句中已经有了执行
            // 只需要收集好就行
            children.push(c[i])
        }
        // 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,他有children属性的
        return vnode(sel, data, children, undefined, undefined)
    } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
        // 说明现在调用h函数是这样的 h('div', {}, h())
        // 即,传入的c是唯一的children, 不用执行c,测试语句中已经执行cl 
        let children = [c]
        return vnode(sel, data, children, undefined, undefined)
    } else {
        throw new Error('传入的第三个参数类型不正确')
    }
}

结果演示

// index.js
import h from './h'
var myVnode1 = h('a', {props: {href: 'https://www.baidu.com' }}, '百度')
console.log(myVnode1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P5udyxjt-1619620817608)(/Users/yinlu/Documents/截屏/截屏2021-04-25 下午8.31.31.png)]

// index.js
import h from './h'
var myVnode2 = h('div', {}, [
    h('span', {}, '东北🐯'),
    h('span', {}, '华南🐯'),
    h('span', {}, '巴厘🐯'),
    h('span', {}, h('span', {}, '🐯儿子'))
])
console.log(myVnode2)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iS9VwQQV-1619620817611)(/Users/yinlu/Documents/截屏/截屏2021-04-25 下午8.40.46.png)]

// index.js
import h from './h'
var myVnode3 = h('div',{props: {class: 'myDiv'}},h('span',{},'mySpan'))
console.log(myVnode3)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y5qNyHIb-1619620817613)(/Users/yinlu/Documents/截屏/截屏2021-04-25 下午8.49.22.png)]

diff算法

最小量更新,key:节点的唯一标示,告诉diff算法,在更改前后它们是同一个DOM节点。

只有是同一个虚拟节点,才能进行精细化比较,否则就会删除旧的,插入新的。所谓同一个虚拟节点就是指,选择器相同且key的值相同

diff算法只会进行同层比较,不会进行跨层比较。

手写patch函数

// index.js
//patch函数的目的就是让我们的虚拟node渲染到domTree上从而在页面显示

import h from './h.js'
import patch from './patch.js'

const myVnode1 = h('h1', {}, '你好')
const app = document.getElementById('app')
patch(app, myVnode1)
// patch.js
import vnode from './vnode.js'
import createElement from './createElement'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点')
    } else {
        console.log('不是同一个节点,插入新的,删除旧的  ')
        createElement(newVnode, oldVnode.elm)
    }
}
// createElement.js
// 真正创建节点,将vnode创建为DOM,插入到标杆pivot这个元素之前
export default function createElement(vnode, pivot) {
    let domNode = document.createElement(vnode.sel)
    // 有子节点还是有文本 这是针对我们的内部是文字的情况简单版本,并没有像源码那样实现全部
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 它内部是文字
        domNode.innerText = vnode.text
        // 将孤儿节点上树,让标杆节点的父元素调用insertfore方法,将新的孤儿节点上树
        pivot.parentNode.insertBefore(domNode, pivot)
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iLY9uqsu-1619620817614)(/Users/yinlu/Documents/截屏/截屏2021-04-26 下午8.04.55.png)]

// 那么如果我们的myVnode是这样呢
const myVnode2 =  h('ul', {}, [
  h('li', {}, 'A'),
  h('li', {}, 'B'),
  h('li', {}, 'C'),
  h('li', {}, 'D'),
])
// 这样的话就不行了,所以继续修改
// patch.js
import vnode from './vnode.js'
import createElement from './createElement'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点')
    } else {
        console.log('不是同一个节点,插入新的,删除旧的  ')
        let newVnodeElm = createElement(newVnode)
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}
// createElement.js
// 真正创建节点,将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
    let domNode = document.createElement(vnode.sel)
    // 有子节点还是有文本 这是针对我们的简单版本,并没有像源码那样实现所有细节
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 它内部是文字
        domNode.innerText = vnode.text
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 内部是子节点时,就要递归创建节点
        for (let i = 0; i < vnode.children.length; i++) {
            // 得到当前这个children
            let ch = vnode.children[i]
            // 创建出它的DOM,一旦调用cerateElement意味着,创建出了DOM,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点
            let chDom = createElement(ch)
            // 上树
            domNode.appendChild(chDom)
        }
    }
    // 补充elm属性
    vnode.elm = domNode
    // 返回elm,elm属性是一个纯DOM对象
    return vnode.elm
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eb9WXtpy-1619620817616)(/Users/yinlu/Documents/截屏/截屏2021-04-26 下午10.12.52.png)]

// 现在我们继续构造
const myVnode3 = h('ul', {}, [
    h('li', {}, [
        h('span', {}, 'spanA'),
        h('span', {}, 'spanB'),
    ]),
    h('li', {}, [
        h('span', {}, 'spanA'),
        h('span', {}, [
            h('a', {}, 'a标签')
        ]),
    ])
])
// 这样也可以了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6HvtQjW-1619620817617)(/Users/yinlu/Documents/截屏/截屏2021-04-26 下午10.09.36.png)]

// 当oldVnode 和 newVnode是同一个节点的时候
// 如果oldVnode和newVnode就是内存中的同一个对象 -> 什么都不用做
// 再看newVnode有没有text属性,有 -> newVnode中的text和oldVnode是否相同 -> 相同就什么也不做、不相同就把elm中的innerText改变为newVnode中的text
// newVnode没有text属性(意味着newVnode有children) -> oldVnode有没有children -> 没有就意味着oldVnode有text -> 清空oldVnode中的text,并且把newVnode的children添加到DOM中
// oldVnode也有children(最复杂的情况--newVnode和oldVnode都有children)
// index.js
// 现在我们实现这样的需求,就是点击后将我们的DOM节点变为文字
const myVnode4 = h('section', {}, [
    h('p', {}, 'A'),
    h('p', {}, 'B'),
    h('p', {}, 'C'),
    h('p', {}, 'D'),
])

const myVnode5 = h('section', {}, 'hello')

const app = document.getElementById('app')
patch(app, myVnode4)
console.log(myVnode4)

const btn = document.querySelector('button')
btn.onclick = function () {
    patch(myVnode4, myVnode5)
}
// patch.js
import vnode from './vnode.js'
import createElement from './createElement'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点')
        // 判断newVnode和oldVnode是不是同一个对象
        if (oldVnode === newVnode) return
        // 判断newVnode有没有text属性
        if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) {
            console.log('newVnode有text属性')
          	if(newVnode.text !== oldVnode.text) {
              // 如果newVnode中的text和oldVnode中的text不一样,直接让newVnode的text写入oldVnode的elm中即可,如果oldVnode中是children,那么也会立即消失掉了
              oldVnode.elm.innerText = newVnode.text
            }
        } else {
            console.log('newVnode没有text属性')
        }
    } else {
        console.log('不是同一个节点,插入新的,删除旧的  ')
        let newVnodeElm = createElement(newVnode)
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}
// 这样就完成了点击后将我们的DOM节点变为文字
// 然后就可以完成newVnode有children的情况
// 就像这样
const myVnode6 = h('section', {}, 'hello')
const myVnode7 = h('section', {}, [
    h('p', {}, 'A'),
    h('p', {}, 'B'),
    h('p', {}, 'C'),
    h('p', {}, 'D')
])
const app = document.getElementById('app')
patch(app, myVnode6)

const btn = document.querySelector('button')
btn.onclick = function () {
    patch(myVnode6, myVnode7)
}
// patch.js
import vnode from './vnode.js'
import createElement from './createElement'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
        console.log('是同一个节点')
        // 判断newVnode和oldVnode是不是同一个对象
        if (oldVnode === newVnode) return
        // 判断newVnode有没有text属性
        if (newVnode.text !==undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
            console.log('newVnode有text属性')
          	if(newVnode.text !== oldVnode.text) {
              // 如果newVnode中的text和oldVnode中的text不一样,直接让newVnode的text写入oldVnode的elm中即可,如果oldVnode中是children,那么也会立即消失掉了
              oldVnode.elm.innerText = newVnode.text
            }
        } else {
            console.log('newVnode没有text属性')
          	// 判断oldVnode有没有children
          	if(oldVnode.children !== undefined && oldVnode.children.length > 0) {
              // oldVnode有children,此时就是newVnode和oldVnode都有children的情况,最复杂的情况
            } else {
              // oldVnode没有children,而newVnode有children
              // 清空oldVnode节点的内容
              oldVnode.elm.innerHTML = ''
              // 遍历newVnode的子节点,创建DOOM并且上树
              for(let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i])
              	oldVnode.elm.appendChild(dom)
              }
              
            }
        }
    } else {
        console.log('不是同一个节点,插入新的,删除旧的  ')
        let newVnodeElm = createElement(newVnode)
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}
// 这样就完成了点击后将我们的文字DOM节点变成带有children的DOM节点
// 然后就只剩oldVnode和newVnode都有children的情况了
// 接下来先试着单独写这部分代码
// 所有的未处理的节点的开头
let un = 0
for(let i  = 0; i < newVnode.children.length; i++) {
  let ch = newVnode.children[i]
	// 再次遍历,看看oldVnode中有没有节点和newVnode是一样的
  let isExist = false
  for(let j = 0; j < oldVnode.children.length; i++) {
    if(oldVnode.children[j].sel == ch.sel && oldVnode.children[j].key == ch.key) {
			isExist = true
    }
  }
  if(!isExist) {
    console.log(ch, i)
    let dom = createElement(ch)
    ch.elm = dom
    if(un < oldVnode.children.length) {
      oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm)
    } else {
      oldVnode.elm.appendChild(dom)
    }
    
  } else {
    // 让处理的节点指针下移
    un++
  }
}
// 这样做就只能把新增的节点的情况完成,但如果是删除节点呢,就不行,也起不到算法的更新策略优势,下面就介绍Vue中的最小量更新算法

四种命中查找:(命中一种就不再进行命中判断了,如果都没有命中,就需要用循环来寻找了)

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前(发生的话,新后(旧前)指向的节点,移动到旧后之后)
  4. 新前与旧后(发生的话,新前(旧后)指向的节点,移动到旧前之前)

新增的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-COXKkGrm-1619620817618)(/Users/yinlu/Documents/截屏/截屏2021-04-27 下午11.54.58.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6aUhemP-1619620817619)(/Users/yinlu/Documents/截屏/截屏2021-04-27 下午11.56.59.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihTni0iG-1619620817619)(/Users/yinlu/Documents/截屏/截屏2021-04-27 下午11.59.13.png)]

while(新前 <= 新后 && 旧前 <= 旧后){
	// 如果是旧节点先循环完毕,说明新节点中有要插入的节点
}

删除的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-35eLHiT1-1619620817620)(/Users/yinlu/Documents/截屏/截屏2021-04-28 上午12.06.24.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7pd2qyu-1619620817621)(/Users/yinlu/Documents/截屏/截屏2021-04-28 上午12.07.14.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Bz1m3b9-1619620817622)(/Users/yinlu/Documents/截屏/截屏2021-04-28 上午12.10.31.png)]

while(新前 <= 新后 && 旧前 <= 旧后){
	// 如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前后旧后中间的节点),说明他们是要被删除的
}

都没有命中(遍历oldVnode,找到了就将其插入到旧前之前,newVnode指针下移)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUfhbfYW-1619620817622)(/Users/yinlu/Documents/截屏/截屏2021-04-28 上午12.16.13.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g0PqsS39-1619620817624)(/Users/yinlu/Documents/截屏/截屏2021-04-28 上午12.16.49.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pn6onP9i-1619620817624)(/Users/yinlu/Documents/截屏/截屏2021-04-28 上午12.28.50.png)]

while(新前 <= 新后 && 旧前 <= 旧后){
	// 如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和旧后中间的节点),说明他们是要被删除的
}

新后与旧前命中(移动新后(旧前)指向的节点到旧后之后)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-scQMBOrV-1619620817625)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.11.22.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWcysW7K-1619620817626)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.14.51.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oTln3Eie-1619620817627)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.43.24.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utNgujDB-1619620817628)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.45.16.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-04VpRKS0-1619620817628)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.47.43.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TmS6t3fw-1619620817630)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.49.36.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JLiKmF7Q-1619620817631)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.50.46.png)]

while(新前 <= 新后 && 旧前 <= 旧后){
	// 如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和旧后中间的节点),说明他们是要被删除的
}

新前与旧后命中 (移动新前(旧后)指向的节点到旧前之前)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tw7BvdHF-1619620817632)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午12.58.47.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B4y0LTw6-1619620817632)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午1.01.06.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GOtNoNZ6-1619620817634)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午1.01.55.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nnzQBy5R-1619620817635)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午1.09.30.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Ib6NZ7Y-1619620817636)(/Users/yinlu/Documents/截屏/截屏2021-04-28 下午1.12.06.png)]

// 将以上图示用代码实现
// updateChildren.js

import patchVnode from "./patchVnode"
import createElement from './createElement'
// 判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
    return a.sel == b.sel && a.key == b.key
}
export default function updateChildren(parentElm, oldCh, newCh) {
    console.log(oldCh, newCh)
    // 旧前
    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 keyMap = null
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 首先判断不是四种情况是否命中,而是要掠过已经加undefined标记的节点
        if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
            newStartVnode = newCh[++newStartIdx]
        } else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
            newEndVnode = newCh[++newEndIdx]
        }
        // 判断新旧是否是同一个虚拟节点 
        else if (checkSameVnode(oldStartVnode, newStartVnode)) {
            // 新前与旧前
            console.log("新前与旧前 命中");
            // 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
            patchVnode(oldStartVnode, newStartVnode);
            // 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        } else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            // 新后与旧后
            console.log("新后与旧后 命中");
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            // 新后与旧前
            console.log("新后与旧前 命中");
            patchVnode(oldStartVnode, newEndVnode);
            // 当新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面 也就是旧后下一个兄弟之前
            // 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            // 新前与旧后
            console.log("新前与旧后 命中");
            patchVnode(oldEndVnode, newStartVnode);
            // 当新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面
            // 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            // 四种都没有命中的情况
            // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了
            if (!keyMap) {
                keyMap = {}
              	// 从oldStartInx开始,到oldEndIdx结束,创建keyMap映射的对象
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key
                    if (key !== undefined) {
                        keyMap[key] = i
                    }
                }
            }
            console.log(keyMap)
            // 寻找当前这项(newStartIdx) 这项在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key]
            console.log(idxInOld)
            if (idxInOld == undefined) {
                // 判断,如果idxInOld是undefined表示他是全新的项
                // 被加入项(就是newStartVnode这项)现不是真正的DOM节点
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
            } else {
                // 如果不是undefined,不是全新的项,而是要移动
                const elmToMove = oldCh[idxInOld]
                patchVnode(elmToMove, newStartVnode)
                // 把这项设置为undefined,表示我已经处理完了这项了
                oldCh[idxInOld] = undefined
                // 移动,调用insertBefore也可以实现移动
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)

            }
            // 指针指移新的头
            newStartVnode = newCh[++newStartIdx]
        }
    }
    // 看看有没有剩余的, 循环结束了start还是比old小
    if (newStartIdx <= newEndIdx) {
        console.log('new还有剩余节点没有处理,要添加项')
        // 插入的标杆
        // const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        // 遍历新的newCh,添加到老的没有处理的之前
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾,和appendChild一致
            // newch[i] 现在还没有变成真正的DOM,所以要调用createElement使其变为DOM
            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm)
        }
    } else if (oldStartIdx <= oldEndIdx) {
        console.log('old还有剩余节点没有处理,要删除项')
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm)
            }
        }
    }
}

学废了没?这里是视频链接

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Y shǔ shǔ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值