手写 Vue2 系列 之 patch —— diff
前言
上一篇文章 手写 Vue2 系列 之 初始渲染 中完成了原始标签、自定义组件、插槽的的初始渲染,当然其中也涉及到 v-bind、v-model、v-on 指令的原理。完成首次渲染之后,接下来就该进行后续的更新了:
响应式数据发生更新 -> setter 拦截到更新操作 -> dep 通知 watcher 执行 update 方法 -> 进而执行 updateComponent 方法更新组件 -> 执行 render 生成新的 vnode -> 将 vnode 传递给 vm._update 方法 -> 调用 patch 方法 -> 执行 patchVnode 进行 DOM diff 操作 -> 完成更新
目标
所以,本篇的目标就是实现 DOM diff,完成后续更新。涉及知识点只有一个:DOM diff。
实现
接下来就开始实现 DOM diff,完成响应式数据的后续更新。
patch
/src/compiler/patch.js
/**
* 负责组件的首次渲染和后续更新
* @param {VNode} oldVnode 老的 VNode
* @param {VNode} vnode 新的 VNode
*/
export default function patch(oldVnode, vnode) {
if (oldVnode && !vnode) {
// 老节点存在,新节点不存在,则销毁组件
return
}
if (!oldVnode) { // oldVnode 不存在,说明是子组件首次渲染
} else {
if (oldVnode.nodeType) { // 真实节点,则表示首次渲染根组件
} else {
// 后续的更新
patchVnode(oldVnode, vnode)
}
}
}
patchVnode
/src/compiler/patch.js
/**
* 对比新老节点,找出其中的不同,然后更新老节点
* @param {*} oldVnode 老节点的 vnode
* @param {*} vnode 新节点的 vnode
*/
function patchVnode(oldVnode, vnode) {
// 如果新老节点相同,则直接结束
if (oldVnode === vnode) return
// 将老 vnode 上的真实节点同步到新的 vnode 上,否则,后续更新的时候会出现 vnode.elm 为空的现象
vnode.elm = oldVnode.elm
// 走到这里说明新老节点不一样,则获取它们的孩子节点,比较孩子节点
const ch = vnode.children
const oldCh = oldVnode.children
if (!vnode.text) { // 新节点不存在文本节点
if (ch && oldCh) { // 说明新老节点都有孩子
// diff
updateChildren(ch, oldCh)
} else if (ch) { // 老节点没孩子,新节点有孩子
// 增加孩子节点
} else { // 新节点没孩子,老节点有孩子
// 删除这些孩子节点
}
} else { // 新节点存在文本节点
if (vnode.text.expression) { // 说明存在表达式
// 获取表达式的新值
const value = JSON.stringify(vnode.context[vnode.text.expression])
// 旧值
try {
const oldValue = oldVnode.elm.textContent
if (value !== oldValue) { // 新老值不一样,则更新
oldVnode.elm.textContent = value
}
} catch {
// 防止更新时遇到插槽,导致报错
// 目前不处理插槽数据的响应式更新
}
}
}
}
updateChildren
/src/compiler/patch.js
/**
* diff,比对孩子节点,找出不同点,然后将不同点更新到老节点上
* @param {*} ch 新 vnode 的所有孩子节点
* @param {*} oldCh 老 vnode 的所有孩子节点
*/
function updateChildren(ch, oldCh) {
// 四个游标
// 新孩子节点的开始索引,叫 新开始
let newStartIdx = 0
// 新结束
let newEndIdx = ch.length - 1
// 老开始
let oldStartIdx = 0
// 老结束
let oldEndIdx = oldCh.length - 1
// 循环遍历新老节点,找出节点中不一样的地方,然后更新
while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) { // 根为 web 中的 DOM 操作特点,做了四种假设,降低时间复杂度
// 新开始节点
const newStartNode = ch[newStartIdx]
// 新结束节点
const newEndNode = ch[newEndIdx]
// 老开始节点
const oldStartNode = oldCh[oldStartIdx]
// 老结束节点
const oldEndNode = oldCh[oldEndIdx]
if (sameVNode(newStartNode, oldStartNode)) { // 假设新开始和老开始是同一个节点
// 对比这两个节点,找出不同然后更新
patchVnode(oldStartNode, newStartNode)
// 移动游标
oldStartIdx++
newStartIdx++
} else if (sameVNode(newStartNode, oldEndNode)) { // 假设新开始和老结束是同一个节点
patchVnode(oldEndNode, newStartNode)
// 将老结束移动到新开始的位置
oldEndNode.elm.parentNode.insertBefore(oldEndNode.elm, oldCh[newStartIdx].elm)
// 移动游标
newStartIdx++
oldEndIdx--
} else if (sameVNode(newEndNode, oldStartNode)) { // 假设新结束和老开始是同一个节点
patchVnode(oldStartNode, newEndNode)
// 将老开始移动到新结束的位置
oldStartNode.elm.parentNode.insertBefore(oldStartNode.elm, oldCh[newEndIdx].elm.nextSibling)
// 移动游标
newEndIdx--
oldStartIdx++
} else if (sameVNode(newEndNode, oldEndNode)) { // 假设新结束和老结束是同一个节点
patchVnode(oldEndNode, newEndNode)
// 移动游标
newEndIdx--
oldEndIdx--
} else {
// 上面几种假设都没命中,则老老实的遍历,找到那个相同元素
}
}
// 跳出循环,说明有一个节点首先遍历结束了
if (newStartIdx < newEndIdx) { // 说明老节点先遍历结束,则将剩余的新节点添加到 DOM 中
}
if (oldStartIdx < oldEndIdx) { // 说明新节点先遍历结束,则将剩余的这些老节点从 DOM 中删掉
}
}
sameVNode
/src/compiler/patch.js
/**
* 判断两个节点是否相同
* 这里的判读比较简单,只做了 key 和 标签的比较
*/
function sameVNode(n1, n2) {
return n1.key == n2.key && n1.tag === n2.tag
}
结果
好了,到这里,虚拟 DOM 的 diff 过程就完成了,如果你能看到如下效果图,则说明一切正常。
动图地址:https://gitee.com/liyongning/typora-image-bed/raw/master/202203151929235.image
可以看到,页面已经完全做到响应式数据的初始渲染和后续更新。其中关于 Computed 计算属性的内容仍然没有正确的显示出来,这很正常,因为还没实现这个功能,所以接下来就会去实现 conputed 计算属性,也就是下一篇内容 手写 Vue2 系列 之 computed。
链接
- 配套视频,微信公众号回复:"精通 Vue 技术栈源码原理视频版" 获取
- 精通 Vue 技术栈源码原理 专栏
- github 仓库 liyongning/Vue 欢迎 Star
- github 仓库 liyongning/Lyn-Vue-DOM 欢迎 Star
- github 仓库 liyongning/Lyn-Vue-Template 欢迎 Star
感谢各位的:关注、点赞、收藏和评论,我们下期见。
当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
手写 Vue2 系列 之 patch —— diff的更多相关文章
- 手写 Vue2 系列 之 初始渲染
前言 上一篇文章 手写 Vue2 系列 之 编译器 中完成了从模版字符串到 render 函数的工作.当我们得到 render 函数之后,接下来就该进入到真正的挂载阶段了: 挂载 -> 实例化渲 ...
- 手写 Vue2 系列 之 编译器
前言 接下来就要正式进入手写 Vue2 系列了.这里不会从零开始,会基于 lyn-vue 直接进行升级,所以如果你没有阅读过 手写 Vue 系列 之 Vue1.x,请先从这篇文章开始,按照顺序进行学习 ...
- 手写 Vue 系列 之 从 Vue1 升级到 Vue2
前言 上一篇文章 手写 Vue 系列 之 Vue1.x 带大家从零开始实现了 Vue1 的核心原理,包括如下功能: 数据响应式拦截 普通对象 数组 数据响应式更新 依赖收集 Dep Watcher 编 ...
- 手写 Vue 系列 之 Vue1.x
前言 前面我们用 12 篇文章详细讲解了 Vue2 的框架源码.接下来我们就开始手写 Vue 系列,写一个自己的 Vue 框架,用最简单的代码实现 Vue 的核心功能,进一步理解 Vue 核心原理. ...
- tensorflow笔记(五)之MNIST手写识别系列二
tensorflow笔记(五)之MNIST手写识别系列二 版权声明:本文为博主原创文章,转载请指明转载地址 http://www.cnblogs.com/fydeblog/p/7455233.html ...
- tensorflow笔记(四)之MNIST手写识别系列一
tensorflow笔记(四)之MNIST手写识别系列一 版权声明:本文为博主原创文章,转载请指明转载地址 http://www.cnblogs.com/fydeblog/p/7436310.html ...
- 常见python面试题-手写代码系列
1.如何反向迭代一个序列 #如果是一个list,最快的方法使用reversetempList = [1,2,3,4]tempList.reverse()for x in tempList: pr ...
- Vue2手写源码---响应式数据的变化
响应式数据变化 数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作) 手写简单的响应式数据的实现(对象属性劫持.深度属性劫持.数组函数劫持).模板转成 ast ...
- Vue2.0 + ElementUI 手写权限管理系统后台模板(一)——简述
挤一下: 一开始以为没有多少人用就没建群,但是加我的人太多了,好多问题都是重复的,所以建个群大家互相沟通交流方便点,但是建的有点晚,错过了好多人所以群里人有点少,QQ群: 157216616 小提示 ...
随机推荐
- ANT中的copy和move标签
Copy标签 该标签用于文件或文件集的拷贝,其属性如下: file 表示源文件. tofile 表示目标文件. todir 表示目标目录. overwrite 表示指定是否覆盖目标文件,默认值是不覆盖 ...
- Ansible 自动化运维——剧本(playbook)
Ansible 自动化运维--剧本(playbook) 1.playbook介绍: playbook是ansible用于配置,部署,和管理被控节点的剧本.通过playbook的详细描述,执行其中的ta ...
- 《PHP程序员面试笔试真题解析》——新书上线
你好,是我--琉忆.很高兴可以跟你分享我的新书. 很高兴,在出版了PHP程序员面试笔试宝典后迎来了我的第二本书出版--<PHP程序员面试笔试真题解析>. 如果你是一个热爱PHP的程序员,刚 ...
- 6、前端--DOM操作(查找标签、节点操作、获取值操作、class操作、样式操作、绑定事件、内置参数this)
DOM操作之查找标签 前缀关键字>>>:document # 基本查找(核心) document.getElementById 根据ID获取一个标签 document.getElem ...
- Solution -「ARC 125F」Tree Degree Subset Sum
\(\mathcal{Description}\) Link. 给定含有 \(n\) 个结点的树,求非负整数对 \((x,y)\) 的数量,满足存在 \(\exist S\subseteq V ...
- mysql悬案 之 为什么用docker启动的mysql配置文件不生效
文章目录 故事前景 查看docker启动时挂载了哪些目录 使用相同镜像启动一个mysql 新建一个目录用来存放容器内的mysql配置文件 复制容器内的mysql配置文件到本地 查看mysql配置文件目 ...
- JNDI漏洞利用探索
最近学习了师傅寻找的一些JNDI漏洞的利用链受益匪浅,自己也尝试关于JNDI漏洞利用做一些挖掘,目前JNDI在利用过程我想到了两个问题. 测试每一个JNDI Bypass 利用链都需要手动更改URL很 ...
- 日常问题记录 E138: Can't write viminfo file /root/.viminfo!
问题是因为磁盘不足导致 看磁盘占用情况 排查:du --max-depth=1 -h 这个命令可以查看当前目录下哪个目录占用最大,然后再进一步看
- IP欺骗实验
实验目的 1.掌握IP欺骗的原理 2.学会利用IPSpoof软件工具进行伪造源IP地址的IP欺骗. 实验内容 使用IPSpoof进行本机IP地址修改,与目标主机通信,进行IP欺骗实验 实验环境描述 1 ...
- [题解]Codeforces Round #709 (Div. 1, based on Technocup 2021 Final Round) - A. Basic Diplomacy
[题目] A. Basic Diplomacy [描述] Aleksey有n个朋友,有一个m天的假期,每天都需要一个朋友来陪他.给出每天有空的朋友的编号,要求同一个朋友来的天数不能超过m/2上取整.求 ...