哈基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则直接插入到队尾。

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

相关推荐
H5开发新纪元5 分钟前
Vite 项目打包分析完整指南:从配置到优化
前端·vue.js
嘻嘻嘻嘻嘻嘻ys6 分钟前
《Vue 3.3响应式革新与TypeScript高效开发实战指南》
前端·后端
恋猫de小郭21 分钟前
腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
android·前端·ios
2301_7994049123 分钟前
如何修改npm的全局安装路径?
前端·npm·node.js
(❁´◡双辞`❁)*✲゚*29 分钟前
node入门和npm
前端·npm·node.js
韩明君33 分钟前
前端学习笔记(四)自定义组件控制自己的css
前端·笔记·学习
tianchang44 分钟前
TS入门教程
前端·typescript
吃瓜群众i44 分钟前
初识javascript
前端
前端练习生1 小时前
vue2如何二次封装表单控件如input, select等
前端·javascript·vue.js