Diff 算法源码(结合源码写的简易版本)

备注:文章后面有详细解析,先简单浏览一遍整体代码,更容易阅读

// Vue3 中的 diff 算法
// 模拟节点
const { oldVirtualDom, virtualDom } = require('./dom') // 这是节点的类型(源码中还有更多的类型,这里只使用了两种类型作为示例)
const ShapeFlags = {
TEXT_CHILDREN: 1 << 3,
ARRAY_CHILDREN: 1 << 4
}
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key
} /**
* Vue render模块函数入口函数
* @param {*} n1 老节点(若老节点为 null, 则为初始化新节点)
* @param {*} n2 新节点
* @param {*} container 容器
*/
function patch(n1, n2, container) {
// 源码中这里会根据 n2 节点的类型进行不同处理
// 这里只考虑 Element 类型,因为diff算法的核心逻辑在 patchKeyedChildren 这个方法中
processElement(n1, n2, container)
} function processElement(n1, n2, container) {
if (!n1) {
// 如果 n1 为空,那么就初始化节点
// mountElement(n2, container);
} else {
// 更新节点
updateElement(n1, n2, container);
}
} function updateElement(n1, n2, container) {
const oldProps = (n1 && n1.props) || {}
const newProps = (n2 && n2.props) || {}
// 这一句代码非常重要,将 老节点的 Element元素 挂载到 新节点上
// 每次对下一级进行对比的时候,会使用这个 el 作为容器
const el = (n2.el = n1.el); // patchProps 这个函数是对新老节点的 props 进行对比
// patchProps(el, oldProps, newProps)
patchChildren(n1, n2, el)
} function patchChildren(n1, n2, container) {
const { children: c1, shapeFlag: prevShapeFlag } = n1
const { children: c2, shapeFlag } = n2 if(ShapeFlags.TEXT_CHILDREN & shapeFlag) {
// 如果 新节点n2 是 文本类型
console.log('c1, c2', n1, n2)
if(c1 !== c2) {
// hostSetElementText(container, c2)
console.log('更新节点文本即可!', container, c2)
}
} else if(
(ShapeFlags.ARRAY_CHILDREN & prevShapeFlag)
&& (ShapeFlags.ARRAY_CHILDREN & shapeFlag)
) {
// 如果 旧节点n1 与 新节点n2 是数组类型
// (h('div', {}, [h('p', {}, 'p1'), h('p', {}, 'p2')]))
patchKeyedChildren(c1, c2, container)
}
} function patchKeyedChildren(c1, c2, container) {
let i = 0; // 当前索引
let e1 = c1.length - 1;
let e2 = c2.length - 1; // 这里进行预处理可以过滤掉一些简单的dom变化,比如 头尾节点 的 新增 和 卸载 等 // 从头部进行对比
while(i <= e1 && i <= e2) {
const prevChild = c1[i]
const nextChild = c2[i]
if(!isSameVNodeType(prevChild, nextChild)) {
break
} // 相等,继续进行对比
patch(prevChild, nextChild, container)
i++
} // 从尾部进行对比
while(i <= e1 && i <= e2) {
const prevChild = c1[e1]
const nextChild = c2[e2]
if(!isSameVNodeType(prevChild, nextChild)) {
break
} // 相等,继续进行对比
patch(prevChild, nextChild, container)
e1--
e2--
} /**
* c1: (a, b, c)
* c2: (a, b, c), d
* i = 3 e1 = 2 e2 = 3
*
* c1: (b, c, d)
* c2: a, (b, c, d)
* i = 0 e1 = -1 e2 = 0
*/
if (i > e1 && i <= e2) {
console.log('新节点 > 老节点')
while(i <= e2) {
const n2 = c2[i]
patch(null, n2, container)
i++
}
}
/**
* c1: a, (c, b)
* c2: c, b
* i = 0 e1 = 0 e2 = -1
*
* c1: (c, b), a
* c2: (c, b)
* i = 2 e1 = 2 e2 = 1
*/
else if (i > e2 && i <= e1) {
console.log('老节点 > 新节点')
while(i <= e1){
console.log('删除节点: ', c1[i])
i++
}
} else {
// 对比完两边的节点后
// a,b, (c, b, d), a, b -> e1: 4 i: 2
// a, b, (b, c, b), a, b -> e2: 4 i: 2
const s1 = i
const s2 = i
// 存储 新节点 key 对应的 索引
const keyToNewIndexMap = new Map()
for(i = s2; i < e2; i++) {
const nextChild = c2[i]
if(nextChild.key !== null) {
keyToNewIndexMap.set(nextChild.key, i)
}
} // 新节点长度
//(
// 左右对比完成之后的长度,
// a, b, (c, c, c), a, b | a, b, (b, b, b), a, b -> len: 3
//)
const toBePatched = e2 - s2 + 1
// 初始化 新节点 Map
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 // 遍历老节点
// 找出 老节点有新节点没有的 -> 进行删除
// 如果新老节点都有的 -> 继续进行 patch 比对
for(i = s1; i < e1; i++) {
const prevChild = c1[i] let newIndex
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 如果这个节点没有 Key,则尝试查找相同类型的无键节点
for(let j = s2; j < e2; j++) {
if(
newIndexToOldIndexMap[j - s2] === 0
&& isSameVNodeType(prevChild, c2[j])
) {
newIndex = j
break
}
}
} if(newIndex === undefined) {
// 如果新节点中没有这个老节点,删除该节点
// console.log(prevChild)
} else {
// i有可能为0,这里 +1 是保证该值不为 0
newIndexToOldIndexMap[newIndex - s2] = i + 1 // 如果新老节点都存在,继续进行对比
patch(prevChild, c2[newIndex], container)
}
} // 遍历新节点
// 1. 新节点不存在 -> 初始化这个节点
// 2. 新老节点都存在需要移动位置
for(i = toBePatched - 1; i >=0; i--) {
const newIndex = s2 + i
const nextChild = c2[newIndex] if(newIndexToOldIndexMap[i] === 0){
// 新节点不存在,则初始化一个
patch(null, nextChild, container)
} else {
const anchor = newIndex + 1 < c2.length ? c2[newIndex + 1] : null
// 插入新元素, 挂载在 container 中
// 在源码中,这里有一个 move 方法,里面区分了 nextChild 的各种类型,并针对每种类型都进行了处理
// hostInset(nextChild.el, container, anchor && anchor.el)
}
}
}
} const containerBox = null
// oldVirtualDom 老节点
// virtualDom 新节点
// containerBox 容器
patch(oldVirtualDom, virtualDom, containerBox)

Diff 算法流程 (patchKeyedChildren 函数解析)

在 Vue3 中,首先会进行头尾单向遍历来预处理 VNode

  1. 从头部开始遍历:对新老节点的同位元素进行对比

    情况1:新老节点同位元素的 key 与 type 不一致,则跳出循环

    情况2:新老节点同位元素的 key 与 type 一致,则继续对这一组元素的下一级进行对比(patch)

    while(i <= e1 && i <= e2) {
    const prevChild = c1[i]
    const nextChild = c2[i]
    if(!isSameVNodeType(prevChild, nextChild)) {
    break
    } // 相等,继续进行对比
    patch(prevChild, nextChild, container)
    i++
    }
  2. 从尾部开始遍历

    情况1:新老节点尾部元素 key 与 type 不一致,则跳出循环

    情况2:新老节点尾部元素 key 与 type 一致,则继续对这一组元素的下一级进行对比(patch)

    while(i <= e1 && i <= e2) {
    const prevChild = c1[e1]
    const nextChild = c2[e2]
    if(!isSameVNodeType(prevChild, nextChild)) {
    break
    } // 相等,继续进行对比
    patch(prevChild, nextChild, container)
    e1--
    e2--
    }
  3. 当首尾遍历完成之后,我们需要判断是否需要 新创建 或 卸载 VNode

    · 情况1:新节点比老节点多,需要挂载新节点

    /**
    * c1: (a, b, c)
    * c2: (a, b, c), d
    * i = 3 e1 = 2 e2 = 3
    *
    * c1: (b, c, d)
    * c2: a, (b, c, d)
    * i = 0 e1 = -1 e2 = 0
    */
    if (i > e1 && i <= e2) {
    console.log('新节点 > 老节点')
    while(i <= e2) {
    const n2 = c2[i]
    patch(null, n2, container)
    i++
    }
    }

    · 情况2:新节点比老节点少,需要 卸载

    /**
    * c1: a, (c, b)
    * c2: c, b
    * i = 0 e1 = 0 e2 = -1
    *
    * c1: (c, b), a
    * c2: (c, b)
    * i = 2 e1 = 2 e2 = 1
    */
    else if (i > e2 && i <= e1) {
    console.log('老节点 > 新节点')
    while(i <= e1){
    console.log('删除节点: ', c1[i])
    i++
    }
    }

当首尾都对比完成之后,需要对中间部分的元素进行处理(新增、卸载、移动位置等处理)

  1. 将新节点中的 key 与 index 对应缓存起来(方便后面直接通过节点的Key获取到索引)

    // 存储 新节点 key 对应的 索引
    const keyToNewIndexMap = new Map()
    for(i = s2; i < e2; i++) {
    const nextChild = c2[i]
    if(nextChild.key !== null) {
    keyToNewIndexMap.set(nextChild.key, i)
    }
    }
    1. 初始化需要处理的新节点
    // 计算出需要处理的新节点数量
    const toBePatched = e2 - s2 + 1
    // 初始化 新节点 数组
    const newIndexToOldIndexMap = new Array(toBePatched)
    for (i = 0; i < toBePatched; i++) {
    newIndexToOldIndexMap[i] = 0
    }
    1. 遍历老节点

    · 如果 老节点存在,而新节点没有 -> 需要卸载

    · 如果 新老节点都存在 -> 继续进行比对(patch)

    // 遍历老节点
    // 找出 老节点有新节点没有的 -> 进行删除
    // 如果新老节点都有的 -> 继续进行 patch 比对
    for(i = s1; i < e1; i++) {
    const prevChild = c1[i] let newIndex
    if (prevChild.key != null) {
    newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
    // 如果这个节点没有 Key,则尝试查找相同类型的无键节点
    for(let j = s2; j < e2; j++) {
    if(
    newIndexToOldIndexMap[j - s2] === 0
    && isSameVNodeType(prevChild, c2[j])
    ) {
    newIndex = j
    break
    }
    }
    } if(newIndex === undefined) {
    // 如果新节点中没有这个老节点,删除该节点
    // console.log(prevChild)
    } else {
    // i有可能为0,这里 +1 是保证该值不为 0
    newIndexToOldIndexMap[newIndex - s2] = i + 1 // 如果新老节点都存在,继续进行对比
    patch(prevChild, c2[newIndex], container)
    }
    }
  2. 遍历新节点

    1. 新节点不存在 -> 初始化这个节点
    2. 新老节点都存在,需要改变位置
    // 遍历新节点
    // 1. 新节点不存在 -> 初始化这个节点
    // 2. 新老节点都存在需要移动位置
    for(i = toBePatched - 1; i >=0; i--) {
    const newIndex = s2 + i
    const nextChild = c2[newIndex] if(newIndexToOldIndexMap[i] === 0){
    // 新节点不存在,则初始化一个
    patch(null, nextChild, container)
    } else {
    const anchor = newIndex + 1 < c2.length ? c2[newIndex + 1] : null
    // 插入新元素, 挂载在 container 中
    // 在源码中,这里有一个 move 方法,里面区分了 nextChild 的各种类型,并针对每种类型都进行了处理
    // hostInset(nextChild.el, container, anchor && anchor.el)
    }
    }

Vue3源码分析之Diff算法的更多相关文章

  1. 死磕以太坊源码分析之Kademlia算法

    死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...

  2. [dev][ipsec][dpdk] strongswan/dpdk源码分析之ipsec算法配置过程

    1 简述 storngswan的配置里用一种固定格式的字符串设置了用于协商的预定义算法.在包协商过程中strongswan将字符串转换为固定的枚举值封在数据包里用于传输. 协商成功之后,这组被协商选中 ...

  3. Spring Cloud Ribbon 源码分析---负载均衡算法

    上一篇分析了Ribbon如何发送出去一个自带负载均衡效果的HTTP请求,本节就重点分析各个算法都是如何实现. 负载均衡整体是从IRule进去的: public interface IRule{ /* ...

  4. Vue3源码分析之 Ref 与 ReactiveEffect

    Vue3中的响应式实现原理 完整 js版本简易源码 在最底部 ref 与 reactive 是Vue3中的两个定义响应式对象的API,其中reactive是通过 Proxy 来实现的,它返回对象的响应 ...

  5. go-ethereum源码分析 PartII 共识算法

    首先从共识引擎-Engine开始记录 Engine是一个独立于具体算法的共识引擎接口 Author(header) (common.Address, error) 返回打包header对应的区块的矿工 ...

  6. Vue3源码分析之微任务队列

    参考资料:https://zh.javascript.info/microtask-queue#wei-ren-wu-dui-lie-microtaskqueue 简化版 Vue3 中的 微任务队列实 ...

  7. Vue3中的响应式对象Reactive源码分析

    Vue3中的响应式对象Reactive源码分析 ReactiveEffect.js 中的 trackEffects函数 及 ReactiveEffect类 在Ref随笔中已经介绍,在本文中不做赘述 本 ...

  8. 如何实现一个 Virtual DOM 及源码分析

    如何实现一个 Virtual DOM 及源码分析 Virtual DOM算法 web页面有一个对应的DOM树,在传统开发页面时,每次页面需要被更新时,都需要手动操作DOM来进行更新,但是我们知道DOM ...

  9. diff.js 列表对比算法 源码分析

    diff.js列表对比算法 源码分析 npm上的代码可以查看 (https://www.npmjs.com/package/list-diff2) 源码如下: /** * * @param {Arra ...

随机推荐

  1. 【LeetCode】442. Find All Duplicates in an Array 解题报告(Python& C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 字典 原地变负 日期 题目地址:https://le ...

  2. 【剑指Offer】二叉搜索树与双向链表 解题报告(Python)

    [剑指Offer]二叉搜索树与双向链表 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interview ...

  3. Problem 2236 第十四个目标

    Problem 2236 第十四个目标 Accept: 4    Submit: 6Time Limit: 1000 mSec    Memory Limit : 32768 KB Problem D ...

  4. 漫谈grpc 3:从实践到原理,带你参透 gRPC

    ​ 原文链接:万字长文 | 从实践到原理,带你参透 gRPC 大家好,我是煎鱼. gRPC 在 Go 语言中大放异彩,越来越多的小伙伴在使用,最近也在公司安利了一波,希望这一篇文章能带你一览 gRPC ...

  5. 基于Java swing+mysql+eclipse的【水电费管理系统】

    本项目为前几天收费帮学妹做的一个项目,Java swing项目,在工作环境中基本使用不到,但是很多学校把这个当做编程入门的项目来做,故分享出本项目供初学者参考. CSDN9.9赞助下载: https: ...

  6. Android物联网应用程序开发(智慧园区)—— 图片预览界面

    效果图: 实现步骤: 1.首先在 build.gradle 文件中引入 RecycleView implementation 'com.android.support:recyclerview-v7: ...

  7. Android开发 SeekBar(拖动条)的使用

    SeekBar是Progress的子类,Progress主要用来显示进度,但是不能和用户互动,而SeekBar则可以供用户进行拖动改变进度值 实现拖动进度条并显示在文本中: <?xml vers ...

  8. maven打包报错 Fatal error compiling: tools.jar not found: C:\Program Files\Java\jre1.8.0_151\..\lib\tool

    maven 打包报错  [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.5.1:comp ...

  9. pip list 精确查找某一模块的方法

    1. 今天搜资料的时候get一项技能: pip list精确查找某一模块 命令如下: pip list | findstr "win32" (此处win32可以替换成任意想查找的模 ...

  10. linux(CentOS7) 之 MySQL 5.7.30 下载及安装

    一.下载 1.百度搜索mysql,进入官网(或直接进入官网https://www.mysql.com) 2.选择 downloads 3.翻到最下面,选择MySQL Community (GPL) D ...