好家伙,这是diff的最后一节了

 

0.暴力比对的使用场景

没有可复用的节点:当新旧虚拟 DOM 的结构完全不同,或者某个节点不能被复用时,需要通过暴力比对来创建新的节点,并在真实 DOM 上进行相应的插入操作。

0.1.例子一:

// 创建vnode
let vm1 = new Vue({
data: {
name: '张三'
}
})
let render1 = compileToFunction(`<ul>
<li style="background:red" key="c">c</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="a">a</li>
</ul>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1)) //数据更新
let vm2 = new Vue({
data: {
name: '李四'
}
})
let render2 = compileToFunction(`<ul>
<li style="background:red" key="f">f</li>
<li style="background:pink" key="g">g</li>
<li style="background:blue" key="e">e</li>
</ul>`)
let vnode2 = render2.call(vm2) setTimeout(() => {
patch(vnode1, vnode2)
}, 2000)

 

0.2.例子二:

let vm1 = new Vue({
data: {
name: '张三'
}
})
let render1 = compileToFunction(`<ul>
<li style="background:red" key="c">c</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="a">a</li>
</ul>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1)) //数据更新
let vm2 = new Vue({
data: {
name: '李四'
}
})
let render2 = compileToFunction(`<ul>
<li style="background:red" key="f">f</li>
<li style="background:pink" key="g">g</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="e">e</li> </ul>`)
let vnode2 = render2.call(vm2) setTimeout(() => {
patch(vnode1, vnode2)
}, 2000)

 

 

依旧是这个例子,但我们分开两种情况讨论

情况一:render1和render2中没有相同的key值

情况二:render1和render2中只有一个节点的key值是相同的

以上两种情况上一张的方法

Vue源码学习(十五):diff算法(二)交叉比对(双指针)

无法处理

于是我们使用暴力比对

 

 

1.分析

来看逻辑图:

1.1.情况一:

例子:c b a 与 f g e 比对

1.1.1比对新旧vnode中的首个节点

不匹配,将新vnode中的元素添加到旧vnode首个元素前

 

 

1.1.2.新vnode指针++

添加逻辑同上,依旧是添加到添加到旧vnode首个元素前

 

1.1.3.新vnode指针++

添加逻辑同上,依旧是添加到添加到旧vnode首个元素前

 

1.1.4.匹配完成,删除旧vnode

 

1.2.情况二:

例子: c b a 与 f g b e比对

前两步一致

但,到相同的元素b时有些许的不同

此处我们会引入一个旧vnode的关系映射表

 

1.2.2.新vnode节点中的每一个子节点都将与这个映射表进行匹配,寻找相同的元素

 

 1.2.3.继续

 

将旧vnode中的"相同节点"打上一个"删去标记"

后续步骤同情况一

2.代码实现

2.1.建立映射表

    function makeIndexBykey(child){
let map = {}
child.forEach((item,index)=>{
if(item.key){
map[item.key] =index
}
})
return map
}
//创建映射表
let map =makeIndexBykey(oldChildren)

在暴力比对(也称为全量更新)期间,旧 vnode 映射表的作用是存储旧 vnode 子元素的键值对(key-value pairs)。

这个映射表的目的是在新旧 vnode 的比对中,可以通过键(key)快速查找旧 vnode 中对应的索引位置。

在给定的代码中,makeIndexBykey 函数接收一个 child 数组作为输入参数,遍历每个 child 元素,并且如果该元素存在 key 属性,

则将其在 child 数组中的索引值存储到 map 对象中,以 item.key 作为键,index 作为值。

这样做的目的是为了在后续的比对过程中,可以通过 key 值快速找到旧 vnode 中对应的索引值。

通过查找 map 对象,可以在遇到新的 vnode 元素时,快速判断是否存在对应的 key 值,并且获取旧 vnode 中的索引值。

这对于减少比对时间和优化更新性能非常有帮助,尤其在大型应用程序或具有复杂数据结构的页面中。

2.2.暴力比对算法

console.log(5)
//1 创建 旧元素映射表
//2 从旧的中寻找新的中有的元素
let moveIndex = map[newStartVnode.key]
//没有相应key值的元素
if(moveIndex == undefined){
parent.insertBefore(createELm(newStartVnode),oldStartVnode.el)
}//有
else{
let moveVnode = oldChildren[moveIndex] //获取到有的元素
oldChildren[moveIndex]=null
//a b f c 和 d f e
parent.insertBefore(moveVnode.el,oldStartVnode.el) patch(moveVnode,newEndVnode)
}
newStartVnode = newChildren[++newStartIndex]

--1--如果旧 vnode 中不存在相同键(key)的元素,即 moveIndex 为 undefined,则说明这是一个新元素,需要将新元素插入到旧 vnode 开始位置元素之前。

这里调用 createELm(newStartVnode) 创建新元素的 DOM 节点,并通过 parent.insertBefore 方法将其插入到旧 vnode 开始位置元素之前。

--2--如果旧 vnode 中存在相同键(key)的元素,则说明这是一个相同元素(一个需要移动的元素,事实上,代码的逻辑为,将在旧vnode中该"相同元素"移动)

通过 let moveVnode = oldChildren[moveIndex] 将该元素赋值给 moveVnode。然后将 oldChildren[moveIndex] 设为 null,标记该元素已经被处理。

然后,通过 parent.insertBefore(moveVnode.el, oldStartVnode.el),将该元素的 DOM 节点插入到旧 vnode 开始位置元素之前。

2.3.删除旧vnode中节点

//将老的多余的元素删去
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
//注意null
let child = oldChildren[i]
if(child !=null ){
parent.removeChild(child.el) //删除元素
}
}
}

3.patch.js完整代码

export function patch(oldVnode, Vnode) {
//原则 将虚拟节点转换成真实的节点
console.log(oldVnode, Vnode)
// console.log(oldVnode.nodeType)
// console.log(Vnode.nodeType)
//第一次渲染 oldVnode 是一个真实的DOM
//判断ldVnode.nodeType是否为一,意思就是判断oldVnode是否为属性节点
if (oldVnode.nodeType === 1) {
console.log(oldVnode, Vnode) //注意oldVnode 需要在加载 mount 添加上去 vm.$el= el let el = createELm(Vnode) // 产生一个新的DOM
let parentElm = oldVnode.parentNode //获取老元素(app) 父亲 ,body
// console.log(oldVnode)
// console.log(parentElm) parentElm.insertBefore(el, oldVnode.nextSibling) //当前真实的元素插入到app 的后面
parentElm.removeChild(oldVnode) //删除老节点
//重新赋值
return el
} else { // diff
// console.log(oldVnode.nodeType)
// console.log(oldVnode, Vnode)
//1 元素不是一样
if (oldVnode.tag !== Vnode.tag) {
//旧的元素 直接替换为新的元素
return oldVnode.el.parentNode.replaceChild(createELm(Vnode), oldVnode.el)
}
//2 标签一样 text 属性 <div>1</div> <div>2</div> tag:undefined
if (!oldVnode.tag) {
if (oldVnode.text !== Vnode.text) {
return oldVnode.el.textContent = Vnode.text
}
}
//2.1属性 (标签一样) <div id='a'>1</div> <div style>2</div>
//在updataRpors方法中处理
//方法 1直接复制
let el = Vnode.el = oldVnode.el
updataRpors(Vnode, oldVnode.data)
//diff子元素 <div>1</div> <div></div>
console.log(oldVnode,Vnode)
let oldChildren = oldVnode.children || []
let newChildren = Vnode.children || []
if (oldChildren.length > 0 && newChildren.length > 0) { //老的有儿子 新有儿子
//创建方法 updataChild(oldChildren, newChildren, el)
} else if (oldChildren.length > 0 && newChildren.length <= 0) { //老的元素 有儿子 新的没有儿子
el.innerHTML = ''
} else if (newChildren.length > 0 && oldChildren.length <= 0) { //老没有儿子 新的有儿子
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i]
//添加到真实DOM
el.appendChild(createELm(child))
}
} }
} function updataChild(oldChildren, newChildren, parent) {
//diff算法 做了很多优化 例子<div>11</div> 更新为 <div>22</div>
//dom中操作元素 常用的 思想 尾部添加 头部添加 倒叙和正序的方式
//双指针 遍历
console.log(oldChildren, newChildren)
let oldStartIndex = 0 //老的开头索引
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndIndex = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIndex] let newStartIndex = 0 //新的开头索引
let newStartVnode = newChildren[newStartIndex];
let newEndIndex = newChildren.length - 1
let newEndVnode = newChildren[newEndIndex]
// console.log(oldEndIndex,newEndIndex)
// console.log(oldEndVnode,newEndVnode) function makeIndexBykey(child){
let map = {}
child.forEach((item,index)=>{
if(item.key){
map[item.key] =index
}
})
return map
}
//创建映射表
let map =makeIndexBykey(oldChildren) while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
//比对子元素
console.log(666)
if (isSomeVnode(oldStartVnode, newStartVnode)) {
//递归
//1 从头部开始
console.log(1)
patch(oldStartVnode, newStartVnode);
//移动指针
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
console.log(oldStartVnode,newStartVnode)
}//2 从尾部开始
else if(isSomeVnode(oldEndVnode, newEndVnode)){
//
console.log(2)
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}//3 交叉比对 从头
else if(isSomeVnode(oldStartVnode,newEndVnode)){
console.log(3)
patch(oldStartVnode, newEndVnode);
oldStartVnode =oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex];
}//4 交叉比对 从尾
else if(isSomeVnode(oldEndVnode,newStartVnode)){
console.log(4)
patch(oldEndVnode, newStartVnode);
oldEndVnode =oldChildren[--oldStartIndex]
newStartVnode = newChildren[++newStartIndex];
}//5 暴力比对 儿子之间没有任何关系
else{
console.log(5)
//1 创建 旧元素映射表
//2 从旧的中寻找新的中有的元素
let moveIndex = map[newStartVnode.key]
//没有相应key值的元素
if(moveIndex == undefined){
parent.insertBefore(createELm(newStartVnode),oldStartVnode.el)
}//有
else{
let moveVnode = oldChildren[moveIndex] //获取到有的元素
oldChildren[moveIndex]=null
//a b f c 和 d f e
parent.insertBefore(moveVnode.el,oldStartVnode.el) patch(moveVnode,newEndVnode)
}
newStartVnode = newChildren[++newStartIndex]
}
}
//判断完毕,添加多余的子儿子 a b c 新的 a b c d
console.log(newEndIndex)
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.appendChild(createELm(newChildren[i]))
}
}
//将老的多余的元素删去
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
//注意null
let child = oldChildren[i]
if(child !=null ){
parent.removeChild(child.el) //删除元素
}
}
} }
function isSomeVnode(oldContext, newContext) {
// return true
return (oldContext.tag == newContext.tag) && (oldContext.key === newContext.key);
} //添加属性
function updataRpors(vnode, oldProps = {}) { //第一次
let newProps = vnode.data || {} //获取当前新节点 的属性
let el = vnode.el //获取当前真实节点 {}
//1老的有属性,新没有属性
for (let key in oldProps) {
if (!newProps[key]) {
//删除属性
el.removeAttribute[key] //
}
}
//2演示 老的 style={color:red} 新的 style="{background:red}"
let newStyle = newProps.style || {} //获取新的样式
let oldStyle = oldProps.style || {} //老的
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style = ''
}
}
//新的
for (let key in newProps) {
if (key === "style") {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
el.setAttribute(key, newProps[key])
}
}
}
//vnode 变成真实的Dom
export function createELm(vnode) {
let {
tag,
children,
key,
data,
text
} = vnode
//注意
if (typeof tag === 'string') { //创建元素 放到 vnode.el上
vnode.el = document.createElement(tag) //创建元素
updataRpors(vnode)
//有儿子
children.forEach(child => {
// 递归 儿子 将儿子渲染后的结果放到 父亲中
vnode.el.appendChild(createELm(child))
})
} else { //文本
vnode.el = document.createTextNode(text)
}
return vnode.el //新的dom
}

Vue源码学习(十六):diff算法(三)暴力比对的更多相关文章

  1. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  2. 最新 Vue 源码学习笔记

    最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...

  3. Vue源码学习三 ———— Vue构造函数包装

    Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...

  4. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  5. 【Vue源码学习】依赖收集

    前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...

  6. VUE 源码学习01 源码入口

    VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...

  7. Vue源码学习(一):调试环境搭建

    最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...

  8. Vue 源码学习(1)

    概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...

  9. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...

  10. 【iScroll源码学习02】分解iScroll三个核心事件点

    前言 最近两天看到很多的总结性发言,我想想今年好像我的变化挺大的,是不是该晚上来水一发呢?嗯,决定了,晚上来水一发! 上周六,我们简单模拟了下iScroll的实现,周日我们开始了学习iScroll的源 ...

随机推荐

  1. 与AI对话 -- 20230221 -- linux 性能分析相关的软件包

    linux 性能分析相关的软件包有哪些,各自包含了哪些命令 sysstat:sysstat 包含了丰富的监控系统性能的工具,例如 sar(系统性能分析器).iostat(设备 IO 状态分析器).mp ...

  2. 【技术实战】Vue技术实战【三】

    需求实战一 效果展示 代码展示 <template> <div style="display: flex;"> <div style="di ...

  3. PXE服务器搭建--ARM

    PXE服务搭建 一. 什么是PXE PXE是由Intel公司开发的最新技术,工作于Client/Server的网络模式,支持工作站通过网络从远端服务器下载映像,并由此支持通过网络启动操作系统,在启动过 ...

  4. 【Nacos篇】Nacos基本操作及配置

    官方文档:https://nacos.io/zh-cn/docs/v2/ecology/use-nacos-with-spring-cloud.html 前置条件:SpringCloud脚手架 单机模 ...

  5. 基于C#的消息处理的应用程序 - 开源研究系列文章

    今天讲讲基于C#里的基于消息处理的应用程序的一个例子. 我们知道,Windows操作系统的程序是基于消息处理的.也就是说,程序接收到消息代码定义,然后根据消息代码定义去处理对应的操作.前面有一个博文例 ...

  6. [刺客伍六七&黑客] 魔刀千刃

    魔刀千刃的特写 诞生之日:2023.7.29 此后会在此记录如何自己写一个自己的python库以及魔刀千刃的维护过程. 魔刀千刃(evilblade) **只攻不防,天下无双** 实战 (和堆攻击帖子 ...

  7. java与es8实战之三:Java API Client有关的知识点串讲

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本篇是<java与es8实战>系 ...

  8. Jmeter+Ant+Jenkins接口自动化框架(续)

    前段时间给公司内部项目搭建了一套接口自动化框架,基于实际使用,需要配置自动发送邮件功能,将 执行结果发送给相关负责人.Jenkins本身也提供了一个邮件通知功能,但在提供详细的邮件内容.自定义邮 件格 ...

  9. 商品详情api接口的应用方向有哪些?

    ​ 商品详情API接口的应用方向非常广泛,可以应用于以下领域: 电子商务平台:商品详情API接口可以提供商品的基本信息,如名称.描述.价格.图片等,帮助电子商务平台展示和推荐商品.此外,还可以提供商品 ...

  10. 性能调优 session 1 - 计算机体系结构 量化研究方法

    近期本人参与的存储系统项目进入到性能调优阶段,当前系统的性能指标离项目预期目标还有较大差距.本人一直奉行"理论指导下的实践",尤其在调试初期,更要抓住主要矛盾,投入最少的资源来获取 ...