【手写 Vue2.x 源码】第二十九篇 - diff算法-节点比对

news/2023/6/9 18:40:51

一,前言

上篇,diff 算法问题分析与 patch 方法改造,主要涉及以下几点:

  • 初始化与更新流程分析;
  • 问题分析与优化思路;
  • 新老虚拟节点比对模拟;
  • patch 方法改造;

下篇,diff 算法-节点比对


二,diff 算法

上一篇完成了 patch 方法的改造,

接下来,开始编写视图更新时新老虚拟节点比对的 diff 算法;

在这之前,先介绍一下 diff 算法:

1,diff 算法的简单介绍

diff 算法也叫做同层比较算法;

首先,dom 是一个树型结构:

image.png

在日常开发中,很少会将 B 和 A 或是 D 和 A 的位置进行调换,即:很少将父亲和儿子节点进行交换

而且跨层的节点比对会非常麻烦,所以,diff 算法考虑到应用场景与性能,只会进行同层节点的比较;

2,diff 算法的比较方式

diff 算法将新老虚拟节点,"两棵树"进行比对

从树的根节点,即 LV1 层开始比较:

image.png

A 比较完成后,查看 A 节点是否有儿子节点,即 B 和 C,优先比较 B:

image.png

B 比较完成后,查看 B 节点是否有儿子节点,即 D 和 E,优先比较 D

D 比较完成后,没有儿子;继续比较 E,当前层处理完成,返回上层处理;

继续比较 C,C 有儿子 F,继续比较 F,最后全部比较完成,结束;

所以,diff 比对是深度优先遍历的递归比对;

备注:递归比对是 vue2 的性能瓶颈,当组件树庞大时会有性能问题;

3,diff 算法的节点复用

如何确定两个节点为复用,一般来说,相同标签的元素即可进行复用;
但也有标签相同,实际场景并不希望复用的情况,这时可使用 key 属性进行标记;
如果 key 不相同,即便标签名相同的两个元素,也不会进行复用;

所以,在编写代码时,相同节点的复用标准如下:

  1. 标签名和 key 均相同,是相同节点;
  2. 如果标签名和 key 不完全相同,不是相同节点;

isSameVnode 方法:用于判断是否为相同节点:

// src/vdom/index.js/*** 判断两个虚拟节点是否是同一个虚拟节点*  逻辑:标签名 和 key 都相同* @param {*} newVnode 新虚拟节点* @param {*} oldVnode 老虚拟节点* @returns */
export function isSameVnode(newVnode, oldVnode){return (newVnode.tag === oldVnode.tag)&&(newVnode.key === oldVnode.key); 
}

当新老虚拟节点的标签和 key 均相同时,即 isSameVnode 返回 true,复用节点仅做属性更新即可;


三,虚拟节点比对

1,不是相同节点的情况

创建两个虚拟节点,模拟视图的更新:

// 模拟初渲染
let vm1 = new Vue({data() {return { name: 'Brave' }}
})
let render1 = compileToFunction('<div>{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);// 模拟新的虚拟节点 newVnode
let vm2 = new Vue({data() {return { name: 'BraveWang' }}
})
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);// diff:新老虚拟节点对比
setTimeout(() => {patch(oldVnode, newVnode); 
}, 1000);

由于新老虚拟节点的标签名 tag 不同(模拟 v-if 和 v-else 的情况),

所以不是相同节点,不考虑复用(放弃跨层复用),直接使用新的替换掉旧的真实节点

在 patch 方法中,打印新老虚拟节点:

image.png

image.png

如何替换节点

由于父节点的标签名不同,导致节点不复用,
需根据新的虚拟节点生成真实节点,并替换掉老节点
  1. 使用新的虚拟节点创建真实节点:

    createElm(vnode);

  2. 替换老节点,如果获取到老的真实节点?

    根据vnode生成真实节点时,通过 vnode.el 将真实节点与虚拟节点进行了映射
    所以,此时可以通过 oldVnode.el 获取到老的真实节点;

    备注:$el是指整棵树,这里不可用;

结论:

  • 新的真实节点:createElm(vnode);
  • 老的真实节点:oldVnode.el
export function patch(oldVnode, vnode) {const isRealElement = oldVnode.nodeType;if(isRealElement){// 真实节点const elm = createElm(vnode);const parentNode = oldVnode.parentNode;parentNode.insertBefore(elm, oldVnode.nextSibling); parentNode.removeChild(oldVnode);return elm;}else{ // diff:新老虚拟节点比对console.log(oldVnode, vnode)if(!isSameVnode(oldVnode, vnode)){// 不是相同节点,不考虑复用直接替换return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);}}
}
当包含子组件时,每个组件都有一个 watcher,
将会通过 diff 进行局部更新,并不会做整个树的更新
所以,只要组件拆分合理,一般不会有性能问题

2,是相同节点的情况

如果元素的标签名和 key 都相同,即判定为相同节点,即isSameVnode返回 true;

此时,只需要更新属性即可(文本、样式等)

2-1 文本的处理

  • 文本节点没有标签名
  • 文本节点没有有儿子
// 文本的处理:文本可以直接更新,因为文本没有儿子
// 组件中 Vue.component(‘xxx’);xxx 就是组件的 tag
let el = vnode.el = oldVnode.el;  // 节点复用:将老节点 el 赋值给新节点 el
if(!oldVnode.tag){// 文本:没有标签名if(oldVnode.text !== vnode.text){// 内容变更:更新文本内容return el.textContent = vnode.text;// 新内容替换老内容} else{return; }
}

2-2 元素的处理

相同节点且新老节点不都是文本时,会对元素进行处理

需要对 updateProperties 方法进行重构调整:

重构前:直接传入真实元素vnode.el 和 data 属性,进行替换,仅具有渲染功能

// src/vdom/patch.jsexport function createElm(vnode) {let{tag, data, children, text, vm} = vnode;if(typeof tag === 'string'){vnode.el = document.createElement(tag)updateProperties(vnode.el, data)children.forEach(child => {vnode.el.appendChild(createElm(child))});} else {vnode.el = document.createTextNode(text)}return vnode.el;
}function updateProperties(el, props = {} ) { for(let key in props){el.setAttribute(key, props[key])}
}
updateProperties 方法的重构方式:
第一个参数是:新的虚拟节点
第二个参数是:老的数据,因为需要对新老数据做 diff 比对

重构后:updateProperties方法,既有渲染功能,又有更新功能

// src/vdom/patch.jsexport function createElm(vnode) {let{tag, data, children, text, vm} = vnode;if(typeof tag === 'string'){vnode.el = document.createElement(tag)updateProperties(vnode, data) // 修改。。。children.forEach(child => {vnode.el.appendChild(createElm(child))});} else {vnode.el = document.createTextNode(text)}return vnode.el;
}// 1,初次渲染,用oldProps给vnode的 el 赋值即可
// 2,更新逻辑,拿到老的props和vnode中的 data 进行比对
function updateProperties(vnode, oldProps = {} ) { let el = vnode.el; // dom上的真实节点(上边复用老节点时已经赋值了)let newProps = vnode.data || {};  // 拿到新的数据// 新旧比对:两个对象比对差异for(let key in newProps){ // 直接用新的盖掉老的,但还要注意:老的里面有,可能新的里面没有了el.setAttribute(key, newProps[key])}// 处理老的里面有,可能新的里面没有的情况,需要再删掉for(let key in oldProps){if(!newProps[key]){el.removeAttribute(key)}}
}

测试:节点元素名相同,属性不同

// 调用 updateProperties 属性更新
updateProperties(vnode, oldVnode.data);
let vm1 = new Vue({data() {return { name: 'Brave' }}
})
let render1 = compileToFunction('<div id="a">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);let vm2 = new Vue({data() {return { name: 'BraveWang' }}
})
let render2 = compileToFunction('<div class="b">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {patch(oldVnode, newVnode); 
}, 1000);

测试结果:

image.png

image.png

2-3 style的处理

除了属性需要更新外,还有其他特殊属性也需要更新,如:style 样式

let vm1 = new Vue({data() {return { name: 'Brave' }}
})
let render1 = compileToFunction('<div style="color:blue">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);let vm2 = new Vue({data() {return { name: 'BraveWang' }}
})
let render2 = compileToFunction('<div style="color:red">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {patch(oldVnode, newVnode); 
}, 1000);

新老元素都有 style,不能用当前逻辑el.setAttribute(key, newProps[key])来处理

style 中是字符串类型,不能直接做替换,需要对样式属性进行收集,再进行比较和更新

function updateProperties(vnode, oldProps = {} ) {let el = vnode.el;let newProps = vnode.data || {};let newStyly = newProps.style || {};  // 新样式对象let oldStyly = oldProps.style || {};  // 老样式对象// 老样式对象中有,新样式对象中没有,删掉多余样式for(let key in oldStyly){if(!newStyly[key]){el.style[key] = ''}}// 新样式对象中有,覆盖到老样式对象中for(let key in newProps){if(key == 'style'){ // 处理style样式for(let key in newStyly){el.style[key] = newStyly[key]}}else{el.setAttribute(key, newProps[key])}}for(let key in oldProps){if(!newProps[key]){el.removeAttribute(key)}}
}

更新前:

image1.png

更新后:

image2.png

至此,外层的 div 已经实现了 diff 更新,但内层 name 属性还并没有更新

接下来继续对比儿子节点


四,结尾

本篇,介绍了diff算法-节点比对,主要涉及以下几点:

  • 介绍了 diff 算法、对比方式、节点复用
  • 实现了外层节点的 diff 算法
  • 不同节点如何做替换更新
  • 相同节点如何做复用更新:文本、元素、样式处理

下篇,diff算法-比对优化


维护日志:

20210806:调整文章的排版布局;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.exyb.cn/news/show-4562356.html

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

实现树莓派homeassistant OS远程控制内网穿透--ddnsto教程

一、两种穿透服务 1、molohub 一种是论坛提供的molohub&#xff0c;但是可能因为自己这边的网络原因在配置过程中一直无法连接到服务器&#xff0c;无法绑定github&#xff0c;因此放弃molohub的方法。如下图所示。molohub教程如下&#xff0c;不再赘述。 molohub的Github教程…

以太坊白皮书[中文]

当中本聪在2009年1月启动比特币区块链时&#xff0c;他同时向世界引入了两种未经测试的革命性的新概念。第一种就是比特币&#xff08;bitcoin&#xff09;&#xff0c;一种去中心化的点对点的网上货币&#xff0c;在没有任何资产担保、内在价值或者中心发行者的情况下维持着价…

sel4白皮书翻译 | sel4 whitepaper | sel4简介

首发地址&#xff1a;http://trialley.top/pages/53ac44/ CSDN地址&#xff1a;https://blog.csdn.net/lgfx21/article/details/117606097 翻译与转发许可&#xff1a; 作者&#xff1a;Gernot Heiser gernotsel4.systems 翻译&#xff1a;TriAlley lg139139.com 翻译版本&…

【算法基础】快速排序

目录 一、快速排序核心思想 二、快速排序步骤 (1)暴力做法 (2)双指针做法 三、代码模板 四、边界问题 五、总结 一、快速排序核心思想 分治&#xff0c;即将一个序列划分成左部分小于等于x,右部分大于等于x 二、快速排序步骤 ①确定一个分界点x。分界点可以是左端 a[l]、右…

27部优秀的黑客纪录片

在多数人眼中&#xff0c;黑客通常是一群无聊至极没有什么趣味的人&#xff0c;在他们的世界里仿佛只有计算机和那敲不完的代码。但事实真的如此吗&#xff1f;让我们回味一下看《黑客帝国》、《幽灵》等黑客题材电影时的场景。有木有种热血澎湃&#xff0c;瞬间变成小迷妹的冲…

《Ajax实战》三部曲之“王者归来”

rel"File-List" href"file:///C:%5CWindows%5CTEMP%5Cmsohtmlclip1%5C01%5Cclip_filelist.xml"> rel"Preview" href"file:///C:%5CWindows%5CTEMP%5Cmsohtmlclip1%5C01%5Cclip_preview.wmf">rel"themeData" href&quo

0910期即将上市:优秀产品三部曲

本期封面报道&#xff1a; 优秀产品三部曲 一 个优秀的产品&#xff0c;完全有可能决定一家公司的兴衰成败。然而如何才能诞生一个优秀的产品&#xff1f;一般来说&#xff0c;都要经历产品规划和设计开发、产品营销、产品运营三个阶 段。在互联网大行其道的今天&#xff0…

财务费控--企业信息化三部曲(财务方向)

话不多说&#xff0c;这篇文章直接贴上干货&#xff01; 财务信息化建设的“三步走”是指&#xff1a;从无到有、连接协作、融合共享&#xff0c;大白话是&#xff1a;将线下基础工作搬上线、部门间/上下级公司业务协作、集团数据大融合。企业信息化建设是任重道远的、是循环渐…