diff算法原理解析+vue中key的作用

您所在的位置:网站首页 vue中key的作用源码解析 diff算法原理解析+vue中key的作用

diff算法原理解析+vue中key的作用

2023-11-10 06:51| 来源: 网络整理| 查看: 265

diff算法可以看作是一种对比算法,diff算法可以找到新旧虚拟Dom之间的差异,并根据对比后的结果更新真实Dom。

一、虚拟Dom

虚拟Dom是用来描述真实Dom的对象。

它有六个属性,sel表示当前节点标签名,data内是节点的属性,children表示当前节点的其他子标签节点,elm表示当前虚拟节点对应的真实节点(这里暂时没有),key即为当前节点的key,text表示当前节点下的文本,结构类似这样。

let vnode = { sel: 'ul', data: {}, children: [ { sel: 'li', data: { class: 'item' }, text: 'son1' }, { sel: 'li', data: { class: 'item' }, text: 'son2' }, ], elm: undefined, key: undefined, text: undefined }

虚拟Dom的作用:

其实虚拟Dom是真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以提供这个真实Dom变化之前和变化之后的状态,通过对比这两个状态,即可得出真实Dom真正需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom的效率高,这也就是虚拟Dom和diff算法真正存在的意义。

二、h函数

了解diff算法原理前需要先了解一下h函数,因为是靠h函数生成虚拟Dom。

这个h函数就是render函数里面传入的那个h函数。

h函数可以接受多种类型的参数,但其实它只干了一件事,就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。

vnode函数又是干什么的呢?其实它也只干了一件事,就是把传入h函数的参数转化为一个对象,即虚拟Dom。

// vnode.js export default function (sel, data, children, text, elm) { const key = data.key return {sel, data, children, text, elm, key} }

执行h函数后,内部会通过vnode函数生成虚拟Dom,h函数把这个虚拟Dom再return出去。

三、diff算法对比规则

简单用h函数生成两个不同的虚拟Dom节点,通过一个简易版的diff算法代码介绍diff对比的具体流程。

// 第一个参数是sel 第二个参数是data 第三个参数是children const myVnode1 = h("h1", {}, [ h("p", {key: "a"}, "a"), h("p", {key: "b"}, "b"), ]); ​ const myVnode2 = h("h1", {}, [ h("p", {key: "c"}, "c"), h("p", {key: "d"}, "d"), ]); 1、patch

比较的第一步就是执行patch,它相当于对比的入口。既然是对比两个虚拟Dom,那么就将两个虚拟Dom作为参数传入patch中。patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。

patch函数的核心代码如下

// patch.js ​ import vnode from "./vnode" import patchDetails from "./patchVnode" import createEle from "./createEle" ​ /** * @description 用来对比两个虚拟dom的根节点,并根据对比结果操作真实Dom * @param {*} oldVnode * @param {*} newVnode */ export function patch(oldVnode, newVnode) { // 1.判断oldVnode是否为虚拟节点,不是的话转化为虚拟节点 if(!oldVnode.sel) { // 转化为虚拟节点 oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode) } ​ // 2.判断oldVnode和newVnode是否为同一个节点 if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) { console.log('是同一个节点') // 比较子节点 patchDetails(oldVnode, newVnode) }else { console.log('不是同一个节点') // 插入newVnode const newNode = createEle(newVnode) // 插入之前需要先将newVnode转化为dom oldVnode.elm.parentNode.insertBefore(newNode, oldVnode.elm) // 插入操作 // 删除oldVnode oldVnode.elm.parentNode.removeChild(oldVnode.elm) } } ​ // createEle.js ​ /** * @description 根据传入的虚拟Dom生成真实Dom * @param {*} vnode * @returns real node */ export default function createEle (vnode) { const realNode = document.createElement(vnode.sel) ​ // 子节点转换 if(vnode.text && (vnode.children == undefined || (vnode.children && vnode.children.length == 0)) ) { // 子节点只含有文本 realNode.innerText = vnode.text }else if(Array.isArray(vnode.children) && vnode.children.length > 0) { // 子节点为其他虚拟节点 递归添加node for(let i = 0; i < vnode.children.length; i++) { const childNode = createEle(vnode.children[i]) realNode.appendChild(childNode) } } ​ // 补充vnode的elm属性 vnode.elm = realNode ​ return vnode.elm } 2、patchVnode

patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点。

// patchVnode.js ​ import updateChildren from "./updateChildren" import createEle from "./createEle" ​ /** * @description 比较两个虚拟节点的子节点(children or text) 并更新其子节点对应的真实dom节点 * @param {*} oldVnode * @param {*} newVnode * @returns */ export function patchDetails(oldVnode, newVnode) { // 判断oldVnode和newVnode是否为同一个对象, 是的话直接不用比了 if(oldVnode == newVnode) return ​ // 默认newVnode和oldVnode只有text和children其中之一,真实的源码这里的情况会更多一些,不过大同小异。 ​ if(hasText(newVnode)) { // newVnode有text但没有children ​ /** * newVnode.text !== oldVnode.text 直接囊括了两种情况 * 1.oldVnode有text无children 但是text和newVnode的text内容不同 * 2.oldVnode无text有children 此时oldVnode.text为undefined * 两种情况都可以通过innerText属性直接完成dom更新 * 情况1直接更新text 情况2相当于去掉了children后加了新的text */ if(newVnode.text !== oldVnode.text) { oldVnode.elm.innerText = newVnode.text } ​ }else if(hasChildren(newVnode)) { // newVnode有children但是没有text if(hasText(oldVnode)) { // oldVnode有text但是没有children oldVnode.elm.innerText = '' // 删除oldVnode的text // 添加newVnode的children for(let i = 0; i < newVnode.children.length; i++) { oldVnode.elm.appendChild(createEle(newVnode.children[i])) } ​ }else if(hasChildren(oldVnode)) { // oldVnode有children但是没有text ​ // 对比两个节点的children 并更新对应的真实dom节点 updateChildren(oldVnode.children, newVnode.children, oldVnode.elm) } } } ​ // 有children没有text function hasChildren(node) { return !node.text && (node.children && node.children.length > 0) } ​ // 有text没有children function hasText(node) { return node.text && (node.children == undefined || (node.children && node.children.length == 0)) } 3、updateChildren

该方法是diff算法中最复杂的方法。对应上面patchVnode中oldVnode和newVnode都有children的情况。

首先介绍一下对比规则。

对比过程中会引入四个指针,分别指向oldVnode子节点列表中的第一个节点和最后一个节点(简称为旧前和旧后)以及指向newVnode子节点列表中的第一个节点和最后一个节点(简称为新前和新后)

对比时,每一次对比按照以下顺序进行命中查找

旧前与新前节点对比(1)

旧后与新后节点对比(2)

旧前与新后节点对比(3)

旧后与新前节点对比(4)

上述四种情况,如果某一种情况两个指针对应的虚拟Dom相同,那么称之为命中。命中后就不会接着查找了,指针会移动,(还有可能会操作真实Dom,3或者4命中时会操作真实Dom移动节点)之后开始下一次对比。如果都没有命中,则去oldVnode子节点列表循环查找当前新前指针所指向的节点,如果查到了,那么操作真实Dom移动节点,没查到则新增真实Dom节点插入。

这种模式的对比会一直进行,直到满足了终止条件。即旧前指针移动到了旧后指针的后面或者新前指针移动到了新后指针的后面,可以理解为旧子节点先处理完毕和新子节点处理完毕。那么可以预想到新旧子节点中总会有其一先处理完,对比结束后,会根据没有处理完子节点的那一对前后指针决定是要插入真实Dom还是删除真实Dom。

如果旧子节点先处理完了,新子节点有剩余,说明有要新增的节点。将根据最终新前和新后之间的虚拟节点执行插入操作

如果新子节点先处理完了,旧子节点有剩余,说明有要删除的节点。将根据最终旧前和旧后之间的虚拟节点执行删除操作

下面将呈现代码:

// updateChildren.js ​ import patchDetails from "./patchVnode" import createEle from "./createEle"; ​ /** * @description 对比子节点列表并更新真实Dom * @param {*} oldCh 旧虚拟Dom子节点列表 * @param {*} newCh 新虚拟Dom子节点列表 * @param {*} parent 新旧虚拟节点对应的真实Dom * @returns */ ​ export default function updateChildren(oldCh, newCh, parent) { // 定义四个指针 旧前 旧后 新前 新后 (四个指针两两一对,每一对前后指针所指向的节点以及其之间的节点为未处理的子节点) let oldStartIndex = 0; let oldEndIndex = oldCh.length - 1; let newStartIndex = 0; let newEndIndex = newCh.length - 1; ​ // 四个指针对应的节点 let oldStartNode = oldCh[oldStartIndex]; let oldEndNode = oldCh[oldEndIndex]; let newStartNode = newCh[newStartIndex]; let newEndNode = newCh[newEndIndex]; ​ // oldCh中每个子节点 key 与 index的哈希表 用于四种对比规则都不匹配的情况下在oldCh中寻找节点 const keyMap = new Map(); ​ /** * 开始遍历两个children数组进行细节对比 * 对比规则:旧前-新前 旧后-新后 旧前-新后 旧后-新前 * 对比之后指针进行移动 * 直到指针不满足以下条件 意味着有一对前后指针之间再无未处理的子节点 则停止对比 直接操作DOM */ ​ while (oldStartIndex


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3