哈基Diff算法?!,有点意思

当听说或学习diff算法时第一印象就是它是一个比较虚拟dom的算法。欸,这个时候又不得不提虚拟dom这个小子是甚么。

虚拟dom是一个模拟真实dom的js对象,因为真实dom这小子身上的方法很多操作其不便,其次是操作js比操作dom性能更高更快。

虚拟dom的创建

js 复制代码
<ul id="list">   //非常真实的dom
    <li class="item">哈哈</li>
    <li class="item">呵呵</li>
    <li class="item">han</li>
</ul>

转化后->

js 复制代码
let oldVDOM = { // 旧虚拟DOM
        tagName: 'ul', // 标签名
        props: { // 标签属性
            id: 'list'
        },
        children: [ // 标签子节点
            {
                tagName: 'li', props: { class: 'item' }, children: ['哈哈']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['呵呵']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['han']
            },
        ]
    }

这个时候又很疑惑了,dom如何转化为虚拟dom。之前一些框架是使用h函数 我们来简单实现一下加深下印象:(代码可直接运行,大家不想看文本格式直接放到vscode兄弟身上运行)

js 复制代码
/**
 *
 * @param {string} a tagName 选择器
 * @param {object} b data
 * @param {any} c 是子节点 可以是文本,数组
 */
export default function h(a, b, c) {
    if (arguments.length < 3) throw new Error('请检查参数个数');
    if (typeof c === 'string' || typeof c === 'number') {
        return vnode(a, b, undefined, c, undefined)
    }
    else if (Array.isArray(c)) {
        let children = []
        for (let i = 0; i < c.length; i++) {
            if (!(typeof c[i] === 'object' && c[i].sel))
                throw new Error('第三个参数为数组时只能传递h()函数')
            children.push(c[i])
        }
        return vnode(a, b, children, undefined, undefined)
    }
    else if (typeof c === 'object' && c.sel) {
        let child = [c]
        return vnode(a, b, child, undefined, undefined)
    }
}
/**
 * 把传入的 参数 作为 对象返回
 * @param {string} tagName 选择器
 * @param {object} data 数据
 * @param {array} children 子节点
 * @param {string} text 文本
 * @param {dom} elm DOM
 * @returns object
 */
export default function (sel, data, children, text, elm) {
    return { sel, data, children, text, elm }
}
//简单使用
vnode = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'E' }, 'E'),
])

这里大家也可以清楚的看到h函数转化主要是对子节点进行添加。将js对象中的对应属性名添加上。

由上图,一看就能看到第二种方式更加快,因为第一种方式中间还加着虚拟dom的步骤。所以虚拟dom比真实dom快是不够严谨的。那该怎么说嘞,虚拟dom算法操作真实dom,性能高于直接操作真实dom,虚拟dom和虚拟dom算法是两个概念。

比较流程

虚拟dom算法=虚拟dom+diff算法(终于来了) 为了让刚学习的童鞋理解大概的diff算法,这简单介绍一下流程:

js 复制代码
 patch(vnode, vnodeList)//刚开始使用patch方法比较新旧虚拟dom  pacth(oldvdom,newvdom)
 //patch函数流程
  if (!oldnode.tagname) {  //先判断old的是不是虚拟dom,不是则进行转化格式
        oldnode = changeVirtualnode(oldnode)
    }
    if (samenode(oldnode, newnode)) {         //如果他们是同一节点则比较他们的子节点
        patchnode(oldnode, newnode)
    } else {                            //如果不是着直接将新的给强行插到老的里面就行
        let newNode = createlm(newnode)
        if (oldnode.elm.parentNode) {            
            let parent = oldnode.elm.parentNode
            parent.insertBefore(newNode, oldnode.elm)
            parent.removeChild(oldnode.elm)
        }
    }

//patchnode函数
  if (oldnode === newnode) return  //如果完全一样就return
    if (newnode.text && !newnode.children) {    //如果新dom节点没有子节点
        if (newnode.text !== oldnode.text) {
            oldnode.elm.textContent = newnode.text    
        }
    } else {
        if (oldnode.children) {     //如果双方都有子节点开始细节比较,updateChildren里面还会使用patchnode递归调用直到没有子节点比较为主
            updateChildren(oldnode.elm, oldnode.children, newnode.children)
        } else {                       //不用多说旧dom没有子节点新dom由子节点
            oldnode.elm.innerHTML = ''
            for (let i = 0; i < newnode.children.length; i++) {
                let node = createlm(newnode.children[i])
                console.log('awdawd', node)
                oldnode.elm.appendChild(node)
            }
        }
    }

现在童鞋们知道了比较虚拟dom中diff算法出现的位置,现在介绍下具体的diff算法。

diff算法原理

众所周知diff算法是同级比较

这里主要将一下vue的diff(代码展示)

Vue2.X Diff ------ 双端比较

所谓双端比较就是新列表旧列表两个列表的头与尾互相对比,,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。

js 复制代码
function vue2Diff(prevChildren, nextChildren, parent) {
  //...声明属性大家应该都会滴
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (oldStartNode.key === newStartNode.key) {
    //...    不做操作  指针变化
    } else if (oldEndNode.key === newEndNode.key) {
    //...    不做操作指针变化
    } else if (oldStartNode.key === newEndNode.key) {
    //**原本旧列表中是头节点,然后在新列表中是尾节点。那么只要在旧列表中把当前的节点移动到原本尾节点的后面,就可以了**
    } else if (oldEndNode.key === newStartNode.key) {
    //**原本旧列表中是尾节点,然后在新列表中是头节点。那么只要在旧列表中把当前的节点移动到原本头节点点的前面,就可以了**
    } else {
      // 在旧列表中找到 和新列表头节点key 相同的节点
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child.key === newKey);  
      if (oldIndex > -1) {   //如果找到了,直接塞到旧头节点前面
        let oldNode = prevChildren[oldIndex];
        patch(oldNode, newStartNode, parent)
        parent.insertBefore(oldNode.el, oldStartNode.el)//将该节点插入到oldStartIndex节点前
        prevChildren[oldIndex] = undefined
      }else {
      	mount(newStartNode, parent, oldStartNode.el)   //如果没找到就创建一个节点塞到前面
      }
      newStartNode = nextChildren[++newStartIndex]
    }
  }
      if (oldEndIndex < oldStartIndex) {         //需要添加节点时
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      mount(nextChildren[i], parent, prevStartNode.el)  应判断插入到末尾还是开图位置
    }
  } else if (newEndIndex < newStartIndex) { 如果需要删除节点时
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      if (prevChildren[i]) {  判断是否为undefinded
        partent.removeChild(prevChildren[i].el)
      }
    }
  }
}

到此完成。 vue3diff借鉴于inferno (opens new window),该算法其中有两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最长递增子序列,此思想与Reactdiff类似又不尽相同。下面我来一一介绍

js 复制代码
function vue3Diff(prevChildren, nextChildren, parent) {
  let j = 0,
    prevEnd = prevChildren.length - 1,
    nextEnd = nextChildren.length - 1,
    prevNode = prevChildren[j],
    nextNode = nextChildren[j];
  // label语法
  outer: {
    while (prevNode.key === nextNode.key) {
      patch(prevNode, nextNode, parent)
      j++
      // 循环中如果触发边界情况,直接break,执行outer之后的判断
      if (j > prevEnd || j > nextEnd) break outer
      prevNode = prevChildren[j]
      nextNode = nextChildren[j]
    }

    prevNode = prevChildren[prevEnd]
    nextNode = prevChildren[nextEnd]

    while (prevNode.key === nextNode.key) {
      patch(prevNode, nextNode, parent)
      prevEnd--
      nextEnd--
      // 循环中如果触发边界情况,直接break,执行outer之后的判断
      if (j > prevEnd || j > nextEnd) break outer
      prevNode = prevChildren[prevEnd]
      nextNode = prevChildren[nextEnd]
    }
  }  //双端比较
  
  // 边界情况的判断 如果j<=nextend则添加上节点,j>nextend则
  if (j > prevEnd && j <= nextEnd) {
    let nextpos = nextEnd + 1,
      refNode = nextpos >= nextChildren.length
                ? null
                : nextChildren[nextpos].el;
    while(j <= nextEnd) mount(nextChildren[j++], parent, refNode)
    
  } else if (j > nextEnd && j <= prevEnd) {
    while(j <= prevEnd) parent.removeChild(prevChildren[j++].el)
  }
}

它会先比较两个节点前置和后值相同的地方直接复用到dom。

然后处理中间的元素先回创建一个和剩余新child长度一样的列表,然后会根据旧列表映射到新列表元素的值的下标改变列表中的值,之后再去寻找最长字符串,从后先向遍历,如果不是最长最序列中的值就会需要移动,如果是-1则直接插入到队尾。

感谢阅读文章由问题请指正

相关推荐
GIS之路7 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug11 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213813 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中34 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路38 分钟前
GDAL 实现矢量合并
前端
hxjhnct40 分钟前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全
前端工作日常1 小时前
我学习到的AG-UI的概念
前端
韩师傅1 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端