前言

上一篇文章 手写 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

链接

感谢各位的:关注点赞收藏评论,我们下期见。


当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

手写 Vue2 系列 之 patch —— diff的更多相关文章

  1. 手写 Vue2 系列 之 初始渲染

    前言 上一篇文章 手写 Vue2 系列 之 编译器 中完成了从模版字符串到 render 函数的工作.当我们得到 render 函数之后,接下来就该进入到真正的挂载阶段了: 挂载 -> 实例化渲 ...

  2. 手写 Vue2 系列 之 编译器

    前言 接下来就要正式进入手写 Vue2 系列了.这里不会从零开始,会基于 lyn-vue 直接进行升级,所以如果你没有阅读过 手写 Vue 系列 之 Vue1.x,请先从这篇文章开始,按照顺序进行学习 ...

  3. 手写 Vue 系列 之 从 Vue1 升级到 Vue2

    前言 上一篇文章 手写 Vue 系列 之 Vue1.x 带大家从零开始实现了 Vue1 的核心原理,包括如下功能: 数据响应式拦截 普通对象 数组 数据响应式更新 依赖收集 Dep Watcher 编 ...

  4. 手写 Vue 系列 之 Vue1.x

    前言 前面我们用 12 篇文章详细讲解了 Vue2 的框架源码.接下来我们就开始手写 Vue 系列,写一个自己的 Vue 框架,用最简单的代码实现 Vue 的核心功能,进一步理解 Vue 核心原理. ...

  5. tensorflow笔记(五)之MNIST手写识别系列二

    tensorflow笔记(五)之MNIST手写识别系列二 版权声明:本文为博主原创文章,转载请指明转载地址 http://www.cnblogs.com/fydeblog/p/7455233.html ...

  6. tensorflow笔记(四)之MNIST手写识别系列一

    tensorflow笔记(四)之MNIST手写识别系列一 版权声明:本文为博主原创文章,转载请指明转载地址 http://www.cnblogs.com/fydeblog/p/7436310.html ...

  7. 常见python面试题-手写代码系列

    1.如何反向迭代一个序列 #如果是一个list,最快的方法使用reversetempList = [1,2,3,4]tempList.reverse()for x in tempList:    pr ...

  8. Vue2手写源码---响应式数据的变化

    响应式数据变化 数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作) 手写简单的响应式数据的实现(对象属性劫持.深度属性劫持.数组函数劫持).模板转成 ast ...

  9. Vue2.0 + ElementUI 手写权限管理系统后台模板(一)——简述

    挤一下: 一开始以为没有多少人用就没建群,但是加我的人太多了,好多问题都是重复的,所以建个群大家互相沟通交流方便点,但是建的有点晚,错过了好多人所以群里人有点少,QQ群: 157216616 小提示 ...

随机推荐

  1. linux_19

    haproxy https实现 总结tomcat的核心组件以及根目录结构 tomcat实现多虚拟主机 nginx实现后端tomcat的负载均衡调度 简述memcached的工作原理

  2. Java执行cmd命令、bat脚本、linux命令,shell脚本等

    1.Windows下执行cmd命令 如复制 D:\tmp\my.txt 到D:\tmp\my_by_only_cmd.txt 现文件如图示: 执行代码: private static void run ...

  3. Kubernets-初见

    只是入门文档. 使用 linux 通过 java -jar 方式部署单体架构,war 包丢tomcat. 使用 Docker部署微服务架构. 使用 K8s Pod 进行部署 一个一个 服务 命令 je ...

  4. ajax请求egg用nginx转发跨域问题

    火狐浏览器报的 谷歌浏览器报的 前提: npm i egg-cors --save config 文件下的pulgin.js 已经添加 //启用跨域支持 exports.cors = { enable ...

  5. C++职工管理系统

    目录 职工管理系统 一. 需求 二. 创建管理类 1.创建文件 2. 头文件实现 3. 源文件实现 三. 菜单功能 1. 添加成员函数 2. 功能实现 3. 测试菜单功能 四. 退出功能 1. 提供功 ...

  6. tarjan2

    反过来调过去,我还是感觉没学明白缩点 讲一个有向图中的所有强连通分量缩成一个点后,构成的新图是一个DAG. 一个点所在的强连通分量一定被该点所在DFS搜索树所包含 树上的边大致分为:树枝边,前向边(从 ...

  7. 降维处理PCA

    要理解什么是降维,书上给出了一个很好但是有点抽象的例子. 说,看电视的时候屏幕上有成百上千万的像素点,那么其实每个画面都是一个上千万维度的数据:但是我们在观看的时候大脑自动把电视里面的场景放在我们所能 ...

  8. Redis 源码简洁剖析 13 - RDB 文件

    RDB 是什么 RDB 文件格式 Header Body DB Selector AUX Fields Key-Value Footer 编码算法说明 Length 编码 String 编码 Scor ...

  9. ThinkPHP5中使用第三方类库

    在TP5中有两种方式使用第三方类库,如果类库支持composer方式安装那就很方便了,使用composer安装的类库存储在Vendor目录下,可以直接使用,以phpmailer为例,使用如下命令安装: ...

  10. 分析CVE-2018-18557与复现

    前言 cve描述: LibTIFF 4.0.9 (with JBIG enabled) decodes arbitrarily-sized JBIG into a buffer, ignoring t ...