首页
3D照片墙
统计
留言
Search
1
1.OAuth 的简单理解
115 阅读
2
多个拦截器的执行顺序
105 阅读
3
基于Annotation方式的声明式事务
102 阅读
4
6.设计模式汇总
101 阅读
5
Unity 依赖注入
98 阅读
Java
JDBC
Spring
Spring MVC
SpringBoot
SpringCloud
MybatisPlus
Mybatis
Maven
SpringSecurity
JVM
java注解与反射
Java JUC并发编程
SSM
.NET
IdentityServer4
EF
.Net Core
AbpVNext + DDD
.NET MVC Api
前端
Jquery&JavaScript
uniapp
VUE
Echars
Vue底层原理
Python
Django
软考笔记
软件设计师
1.计算机组成与体系结构
10.面向对象技术
11.UML类图建模
12.面向对象程序设计
13.数据结构
14.算法基础
16.知识产权标准化
17.程序设计语言
2.操作系统
3.数据库
4.数据库设计
5.计算机网络
6.信息安全
7.系统开发基础
8.项目管理
9.数据流图
架构设计
CQRS架构
DDD架构
数据库技术
SQL锁
SqlServer
Oracle 主从备份
Oracle RAC集群
Mysql
云原生/容器技术
kubernetes
Docker
数据结构与算法
常用中间件
Redis
RabbitMQ 消息队列
ElasticSearch
其他
PHP
OAuth 2.0
WebSocket
ArkTs Harmony 开发
运维
Search
标签搜索
排序算法
vue
算法
遍历
docker
线性
数组
dom
synchronized
数据库
xml语言
log4j
bigint
静态函数
静态方法
哈夫曼树
const
冒泡排序
商标设计
命令模式
Bi8bo
累计撰写
304
篇文章
累计收到
6
条评论
首页
栏目
Java
JDBC
Spring
Spring MVC
SpringBoot
SpringCloud
MybatisPlus
Mybatis
Maven
SpringSecurity
JVM
java注解与反射
Java JUC并发编程
SSM
.NET
IdentityServer4
EF
.Net Core
AbpVNext + DDD
.NET MVC Api
前端
Jquery&JavaScript
uniapp
VUE
Echars
Vue底层原理
Python
Django
软考笔记
软件设计师
1.计算机组成与体系结构
10.面向对象技术
11.UML类图建模
12.面向对象程序设计
13.数据结构
14.算法基础
16.知识产权标准化
17.程序设计语言
2.操作系统
3.数据库
4.数据库设计
5.计算机网络
6.信息安全
7.系统开发基础
8.项目管理
9.数据流图
架构设计
CQRS架构
DDD架构
数据库技术
SQL锁
SqlServer
Oracle 主从备份
Oracle RAC集群
Mysql
云原生/容器技术
kubernetes
Docker
数据结构与算法
常用中间件
Redis
RabbitMQ 消息队列
ElasticSearch
其他
PHP
OAuth 2.0
WebSocket
ArkTs Harmony 开发
运维
页面
3D照片墙
统计
留言
搜索到
4
篇与
的结果
2024-12-12
Vue响应式原理
Vue的响应式是如何实现的? 数据联动 + 依赖变化 (发布订阅模式) 使用Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。 Vue 不能检测数组和对象的变化。 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。 data 对象就像组件状态的结构 (schema)。提前声明所有的响应式 property,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。 Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、 MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。 当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。 订阅器模型 1.容器(存储数据) 2.订阅操作 3.发布动作
2024年12月12日
71 阅读
0 评论
31 点赞
2024-12-12
深入浅出虚拟 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别
认识虚拟 DOM 虚拟 DOM 简单说就是 用JS对象来模拟 DOM 结构 那它是怎么用 JS 对象模拟 DOM 结构的呢?看个例子 <template> <div id="app" class="container"> <h1>沐华</h1> </div> </template> 上面的模板转在虚拟 DOM 就是下面这样的 { 'div', props:{ id:'app', class:'container' }, children: [ { tag: 'h1', children:'沐华' } ] } 这样的 DOM 结构就称之为 虚拟 DOM (Virtual Node),简称 vnode。 它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tag、props、children tag:必选。就是标签。也可以是组件,或者函数 props:非必选。就是这个标签上的属性和方法 children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素 为什么要使用虚拟 DOM 呢? 看个图 如图可以看出原生 DOM 有非常多的属性和事件,就算是创建一个空div也要付出不小的代价。而使用虚拟 DOM 来提升性能的点在于 DOM 发生变化的时候,通过 diff 算法和数据改变前的 DOM 对比,计算出需要更改的 DOM,然后只对变化的 DOM 进行操作,而不是更新整个视图 在 Vue 中是怎么把 DOM 转成上面这样的虚拟 DOM 的呢,有兴趣的可以关注我另一篇文章详细了解一下 Vue 中的模板编译过程和原理 在 Vue 里虚拟 DOM 的数据更新机制采用的是异步更新队列,就是把变更后的数据变装入一个数据更新的异步队列,就是 patch,用它来做新老 vnode 对比 认识 Diff 算法 Diff 算法,在 Vue 里面就是叫做 patch ,它的核心就是参考 Snabbdom,通过新旧虚拟 DOM 对比(即 patch 过程),找出最小变化的地方转为进行 DOM 操作 扩展在 Vue1 里是没有 patch 的,每个依赖都有单独的 Watcher 负责更新,当项目规模变大的时候性能就跟不上了,所以在 Vue2 里为了提升性能,改为每个组件只有一个 Watcher,那我们需要更新的时候,怎么才能精确找到组件里发生变化的位置呢?所以 patch 它来了 那么它是在什么时候执行的呢? 在页面首次渲染的时候会调用一次 patch 并创建新的 vnode,不会进行更深层次的比较 然后是在组件中数据发生变化时,会触发 setter 然后通过 Notify 通知 Watcher,对应的 Watcher 会通知更新并执行更新函数,它会执行 render 函数获取新的虚拟 DOM,然后执行 patch 对比上次渲染结果的老的虚拟 DOM,并计算出最小的变化,然后再去根据这个最小的变化去更新真实的 DOM,也就是视图 那么它是怎么计算的? 先看个图 比如有上图这样的 DOM 结构,是怎么计算出变化?简单说就是 遍历老的虚拟 DOM 遍历新的虚拟 DOM 然后根据变化,比如上面的改变和新增,再重新排序 可是这样会有很大问题,假如有1000个节点,就需要计算 1000³ 次,也就是10亿次,这样是无法让人接受的,所以 Vue 或者 React 里使用 Diff 算法的时候都遵循深度优先,同层比较的策略做了一些优化,来计算出最小变化 Diff 算法的优化 1. 只比较同一层级,不跨级比较 如图,Diff 过程只会把同颜色框起来的同一层级的 DOM 进行比较,这样来简化比较次数,这是第一个方面 2. 比较标签名 如果同一层级的比较标签名不同,就直接移除老的虚拟 DOM 对应的节点,不继续按这个树状结构做深度比较,这是简化比较次数的第二个方面 3. 比较 key 如果标签名相同,key 也相同,就会认为是相同节点,也不继续按这个树状结构做深度比较,比如我们写 v-for 的时候会比较 key,不写 key 就会报错,这也就是因为 Diff 算法需要比较 key 面试中有一道特别常见的题,就是让你说一下 key 的作用,实际上考查的就是大家对虚拟 DOM 和 patch 细节的掌握程度,能够反应出我们面试者的理解层次,所以这里扩展一下 key key 的作用 比如有一个列表,我们需要在中间插入一个元素,会发生什么变化呢?先看个图 如图的 li1 和 li2 不会重新渲染,这个没有争议的。而 li3、li4、li5 都会重新渲染 因为在不使用 key 或者列表的 index 作为 key 的时候,每个元素对应的位置关系都是 index,上图中的结果直接导致我们插入的元素到后面的全部元素,对应的位置关系都发生了变更,所以全部都会执行更新操作,这可不是我们想要的,我们希望的是渲染添加的那一个元素,其他四个元素不做任何变更,也就不要重新渲染 而在使用唯一 key 的情况下,每个元素对应的位置关系就是 key,来看一下使用唯一 key 值的情况下 这样如图中的 li3 和 li4 就不会重新渲染,因为元素内容没发生改变,对应的位置关系也没有发生改变。 这也是为什么 v-for 必须要写 key,而且不建议开发中使用数组的 index 作为 key 的原因 总结一下: key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效 Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果 从源码里可以知道,Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的 有兴趣的可以去看一下源码:src\core\vdom\patch.js -35行 sameVnode(),下面也有详细介绍 Diff 算法核心原理——源码 上面说了Diff 算法,在 Vue 里面就是 patch,铺垫了这么多,下面进入源码里看一下这个神乎其神的 patch 干了啥? patch 源码地址:src/core/vdom/patch.js -700行 其实 patch 就是一个函数,我们先介绍一下源码里的核心流程,再来看一下 patch 的源码,源码里每一行也有注释 它可以接收四个参数,主要还是前两个 oldVnode:老的虚拟 DOM 节点 vnode:新的虚拟 DOM 节点 hydrating:是不是要和真实 DOM 混合,服务端渲染的话会用到,这里不过多说明 removeOnly:transition-group 会用到,这里不过多说明 主要流程是这样的: vnode 不存在,oldVnode 存在,就删掉 oldVnode vnode 存在,oldVnode 不存在,就创建 vnode 两个都存在的话,通过 sameVnode 函数(后面有详解)对比是不是同一节点 如果是同一节点的话,通过 patchVnode 进行后续对比节点文本变化或子节点变化 如果不是同一节点,就把 vnode 挂载到 oldVnode 的父元素下 如果组件的根节点被替换,就遍历更新父节点,然后删掉旧的节点 如果是服务端渲染就用 hydrating 把 oldVnode 和真实 DOM 混合 下面看完整的 patch 函数源码,说明我都写在注释里了 // 两个判断函数 function isUndef (v: any): boolean %checks { return v === undefined || v === null } function isDef (v: any): boolean %checks { return v !== undefined && v !== null } return function patch (oldVnode, vnode, hydrating, removeOnly) { // 如果新的 vnode 不存在,但是 oldVnode 存在 if (isUndef(vnode)) { // 如果 oldVnode 存在,调用 oldVnode 的组件卸载钩子 destroy if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 如果 oldVnode 不存在的话,新的 vnode 是肯定存在的,比如首次渲染的时候 if (isUndef(oldVnode)) { isInitialPatch = true // 就创建新的 vnode createElm(vnode, insertedVnodeQueue) } else { // 剩下的都是新的 vnode 和 oldVnode 都存在的话 // 是不是元素节点 const isRealElement = isDef(oldVnode.nodeType) // 是元素节点 && 通过 sameVnode 对比是不是同一个节点 (函数后面有详解) if (!isRealElement && sameVnode(oldVnode, vnode)) { // 如果是 就用 patchVnode 进行后续对比 (函数后面有详解) patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 如果不是同一元素节点的话 if (isRealElement) { // const SSR_ATTR = 'data-server-rendered' // 如果是元素节点 并且有 'data-server-rendered' 这个属性 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { // 就是服务端渲染的,删掉这个属性 oldVnode.removeAttribute(SSR_ATTR) hydrating = true } // 这个判断里是服务端渲染的处理逻辑,就是混合 if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn('这是一段很长的警告信息') } } // function emptyNodeAt (elm) { // return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) // } // 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode oldVnode = emptyNodeAt(oldVnode) } // 拿到 oldVnode 的父节点 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上 createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点 if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) // 递归更新父节点下的元素 while (ancestor) { // 卸载老根节点下的全部组件 for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } // 替换现有元素 ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } const insert = ancestor.data.hook.insert if (insert.merged) { for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } // 更新父节点 ancestor = ancestor.parent } } // 如果旧节点还存在,就删掉旧节点 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { // 否则直接卸载 oldVnode invokeDestroyHook(oldVnode) } } } // 返回更新后的节点 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } sameVnode 源码地址:src/core/vdom/patch.js -35行 这个是用来判断是不是同一节点的函数 这个函数不长,直接看源码吧 function sameVnode (a, b) { return ( a.key === b.key && // key 是不是一样 a.asyncFactory === b.asyncFactory && ( // 是不是异步组件 ( a.tag === b.tag && // 标签是不是一样 a.isComment === b.isComment && // 是不是注释节点 isDef(a.data) === isDef(b.data) && // 内容数据是不是一样 sameInputType(a, b) // 判断 input 的 type 是不是一样 ) || ( isTrue(a.isAsyncPlaceholder) && // 判断区分异步组件的占位符否存在 isUndef(b.asyncFactory.error) ) ) ) } patchVnode 源码地址:src/core/vdom/patch.js -501行 这个是在新的 vnode 和 oldVnode 是同一节点的情况下,才会执行的函数,主要是对比节点文本变化或子节点变化 还是先介绍一下主要流程,再看源码吧,流程是这样的: 如果 oldVnode 和 vnode 的引用地址是一样的,就表示节点没有变化,直接返回 如果 oldVnode 的 isAsyncPlaceholder 存在,就跳过异步组件的检查,直接返回 如果 oldVnode 和 vnode 都是静态节点,并且有一样的 key,并且 vnode 是克隆节点或者 v-once 指令控制的节点时,把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上,然后返回 如果 vnode 不是文本节点也不是注释的情况下 如果 vnode 和 oldVnode 都有子节点,而且子节点不一样的话,就调用 updateChildren 更新子节点 如果只有 vnode 有子节点,就调用 addVnodes 创建子节点 如果只有 oldVnode 有子节点,就调用 removeVnodes 删除该子节点 如果 vnode 文本为 undefined,就删掉 vnode.elm 文本 如果 vnode 是文本节点但是和 oldVnode 文本内容不一样,就更新文本 function patchVnode ( oldVnode, // 老的虚拟 DOM 节点 vnode, // 新的虚拟 DOM 节点 insertedVnodeQueue, // 插入节点的队列 ownerArray, // 节点数组 index, // 当前节点的下标 removeOnly // 只有在 ) { // 新老节点引用地址是一样的,直接返回 // 比如 props 没有改变的时候,子组件就不做渲染,直接复用 if (oldVnode === vnode) return // 新的 vnode 真实的 DOM 元素 if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm // 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件 if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 当前节点是静态节点的时候,key 也一样,或者有 v-once 的时候,就直接赋值返回 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } // hook 相关的不用管 let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // 获取子元素列表 const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { // 遍历调用 update 更新 oldVnode 所有属性,比如 class,style,attrs,domProps,events... // 这里的 update 钩子函数是 vnode 本身的钩子函数 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) // 这里的 update 钩子函数是我们传过来的函数 if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 如果新节点不是文本节点,也就是说有子节点 if (isUndef(vnode.text)) { // 如果新老节点都有子节点 if (isDef(oldCh) && isDef(ch)) { // 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 如果新节点有子节点的话,就是说老节点没有子节点 // 如果老节点文本节点,就是说没有子节点,就清空 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 添加子节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 如果新节点没有子节点,老节点有子节点,就删除 removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // 如果老节点是文本节点,就清空 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 新老节点都是文本节点,且文本不一样,就更新文本 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { // 执行 postpatch 钩子 if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } } updateChildren 源码地址:src/core/vdom/patch.js -404行 这个是新的 vnode 和 oldVnode 都有子节点,且子节点不一样的时候进行对比子节点的函数 这里很关键,很关键! 比如现在有两个子节点列表对比,对比主要流程如下 循环遍历两个列表,循环停止条件是:其中一个列表的开始指针 startIdx 和 结束指针 endIdx 重合 循环内容是:{ 新的头和老的头对比 新的尾和老的尾对比 新的头和老的尾对比 新的尾和老的头对比。 这四种对比如图 以上四种只要有一种判断相等,就调用 patchVnode 对比节点文本变化或子节点变化,然后移动对比的下标,继续下一轮循环对比 如果以上四种情况都没有命中,就不断拿新的开始节点的 key 去老的 children 里找 如果没找到,就创建一个新的节点 如果找到了,再对比标签是不是同一个节点 如果是同一个节点,就调用 patchVnode 进行后续对比,然后把这个节点插入到老的开始前面,并且移动新的开始下标,继续下一轮循环对比 如果不是相同节点,就创建一个新的节点 } 如果老的 vnode 先遍历完,就添加新的 vnode 没有遍历的节点 如果新的 vnode 先遍历完,就删除老的 vnode 没有遍历的节点 为什么会有头对尾,尾对头的操作? 因为可以快速检测出 reverse 操作,加快 Diff 效率 function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 // 老 vnode 遍历的下标 let newStartIdx = 0 // 新 vnode 遍历的下标 let oldEndIdx = oldCh.length - 1 // 老 vnode 列表长度 let oldStartVnode = oldCh[0] // 老 vnode 列表第一个子元素 let oldEndVnode = oldCh[oldEndIdx] // 老 vnode 列表最后一个子元素 let newEndIdx = newCh.length - 1 // 新 vnode 列表长度 let newStartVnode = newCh[0] // 新 vnode 列表第一个子元素 let newEndVnode = newCh[newEndIdx] // 新 vnode 列表最后一个子元素 let oldKeyToIdx, idxInOld, vnodeToMove, refElm const canMove = !removeOnly // 循环,规则是开始指针向右移动,结束指针向左移动移动 // 当开始和结束的指针重合的时候就结束循环 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] // 老开始和新开始对比 } else if (sameVnode(oldStartVnode, newStartVnode)) { // 是同一节点 递归调用 继续对比这两个节点的内容和子节点 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 然后把指针后移一位,从前往后依次对比 // 比如第一次对比两个列表的[0],然后比[1]...,后面同理 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 老结束和新结束对比 } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // 然后把指针前移一位,从后往前比 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // 老开始和新结束对比 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // 老的列表从前往后取值,新的列表从后往前取值,然后对比 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 老结束和新开始对比 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // 老的列表从后往前取值,新的列表从前往后取值,然后对比 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 以上四种情况都没有命中的情况 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 拿到新开始的 key,在老的 children 里去找有没有某个节点有这个 key idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 新的 children 里有,可是没有在老的 children 里找到对应的元素 if (isUndef(idxInOld)) { /// 就创建新的元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 在老的 children 里找到了对应的元素 vnodeToMove = oldCh[idxInOld] // 判断标签如果是一样的 if (sameVnode(vnodeToMove, newStartVnode)) { // 就把两个相同的节点做一个更新 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果标签是不一样的,就创建新的元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } // oldStartIdx > oldEndIdx 说明老的 vnode 先遍历完 if (oldStartIdx > oldEndIdx) { // 就添加从 newStartIdx 到 newEndIdx 之间的节点 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) // 否则就说明新的 vnode 先遍历完 } else if (newStartIdx > newEndIdx) { // 就删除掉老的 vnode 里没有遍历的节点 removeVnodes(oldCh, oldStartIdx, oldEndIdx) } } 至此,整个 Diff 流程的核心逻辑源码到这就结束了,再来看一下 Vue 3 里做了哪些改变吧 Vue3 的优化 本文源码版本是 Vue2 的,在 Vue3 里整个重写了 Diff 算法这一块东西,所以源码的话可以说基本是完全不一样的,但是要做的事还是一样的 关于 Vue3 的 Diff 完整源码解析还在撰稿中,过几天就发布了,这里先介绍一下相比 Vue2 优化的部分,尤大公布的数据就是 update 性能提升了 1.3~2 倍,ssr 性能提升了 2~3 倍,来看看都有哪些优化 事件缓存:将事件缓存,可以理解为变成静态的了 添加静态标记:Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff 静态提升:创建静态节点时保存,后续直接复用 使用最长递增子序列优化了对比流程:Vue2 里在 updateChildren() 函数里对比变更,在 Vue3 里这一块的逻辑主要在 patchKeyedChildren() 函数里,具体看下面 事件缓存 比如这样一个有点击事件的按钮 <button @click="handleClick">按钮</button> 来看下在 Vue3 被编译后的结果 export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("button", { onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args))) }, "按钮")) } 注意看,onClick 会先读取缓存,如果缓存没有的话,就把传入的事件存到缓存里,都可以理解为变成静态节点了,优秀吧,而在 Vue2 中就没有缓存,就是动态的 静态标记 看一下静态标记是啥? 源码地址:packages/shared/src/patchFlags.ts export const enum PatchFlags { TEXT = 1 , // 动态文本节点 CLASS = 1 << 1, // 2 动态class STYLE = 1 << 2, // 4 动态style PROPS = 1 << 3, // 8 除去class/style以外的动态属性 FULL_PROPS = 1 << 4, // 16 有动态key属性的节点,当key改变时,需进行完整的diff比较 HYDRATE_EVENTS = 1 << 5, // 32 有监听事件的节点 STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的fragment (一个组件内多个根元素就会用fragment包裹) KEYED_FRAGMENT = 1 << 7, // 128 带有key属性的fragment或部分子节点有key UNKEYEN_FRAGMENT = 1 << 8, // 256 子节点没有key的fragment NEED_PATCH = 1 << 9, // 512 一个节点只会进行非props比较 DYNAMIC_SLOTS = 1 << 10, // 1024 动态slot HOISTED = -1, // 静态节点 BAIL = -2 // 表示 Diff 过程中不需要优化 } 先了解一下静态标记有什么用?看个图 在什么地方用到的呢?比如下面这样的代码 <div id="app"> <div>沐华</div> <p>{{ age }}</p> </div> 在 Vue2 中编译的结果是,有兴趣的可以自行安装 vue-template-compiler 自行测试 with(this){ return _c( 'div', {attrs:{"id":"app"}}, [ _c('div',[_v("沐华")]), _c('p',[_v(_s(age))]) ] ) } 在 Vue3 中编译的结果是这样的,有兴趣的可以点击这里自行测试 const _hoisted_1 = { id: "app" } const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "沐华", -1 /* HOISTED */) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", _hoisted_1, [ _hoisted_2, _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */) ])) } 看到上面编译结果中的 -1 和 1 了吗,这就是静态标记,这是在 Vue2 中没有的,patch 过程中就会判断这个标记来 Diff 优化流程,跳过一些静态节点对比 静态提升 其实还是拿上面 Vue2 和 Vue3 静态标记的例子,在 Vue2 里每当触发更新的时候,不管元素是否参与更新,每次都会全部重新创建,就是下面这一堆 with(this){ return _c( 'div', {attrs:{"id":"app"}}, [ _c('div',[_v("沐华")]), _c('p',[_v(_s(age))]) ] ) } 而在 Vue3 中会把这个不参与更新的元素保存起来,只创建一次,之后在每次渲染的时候不停地复用,比如上面例子中的这个,静态的创建一次保存起来 const _hoisted_1 = { id: "app" } const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "沐华", -1 /* HOISTED */) 然后每次更新 age 的时候,就只创建这个动态的内容,复用上面保存的静态内容 export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", _hoisted_1, [ _hoisted_2, _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */) ])) } patchKeyedChildren 在 Vue2 里 updateChildren 会进行 头和头比 尾和尾比 头和尾比 尾和头比 都没有命中的对比 在 Vue3 里 patchKeyedChildren 为 头和头比 尾和尾比 基于最长递增子序列进行移动/添加/删除 看个例子,比如 老的 children:[ a, b, c, d, e, f, g ] 新的 children:[ a, b, f, c, d, e, h, g ] 先进行头和头比,发现不同就结束循环,得到 [ a, b ] 再进行尾和尾比,发现不同就结束循环,得到 [ g ] 再保存没有比较过的节点 [ f, c, d, e, h ],并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的就说明是新增 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ] 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了 使用最长递增子序列可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作,有兴趣的话去 leet-code 第300题(最长递增子序列) 体验下
2024年12月12日
78 阅读
0 评论
35 点赞
2024-01-17
对于Vue 2 diff的讨论
vue2的diff 组件更新核心是响应式数据监控到数据的改变,重新生成了虚拟dom树,然后通过diff算法计算出前后虚拟dom树的差异点,更新dom时只更新变化的部分。 快问快答: 为什么要diff? 答: O(n^3) 意味着如果要展示1000个节点,就要依次执行上十亿次的比较,无法承受大数据量的对比。 直接比较和修改两个树的复杂度为什么是n^3? 答: 老树的每一个节点都去遍历新树的节点,直到找到新树对应的节点。那么这个流程就是 O(n^2),再紧接着找到不同之后,再计算最短修改距离然后修改节点,这里是 O(n^3)。 diff的策略是什么?有什么根据? 答: 1、Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计,因此仅进行同层比较。 2、如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染; 3、如果子节点有变化,Virtual DOM不会计算变化的是什么,而是重新渲染。 4、同级多个节点可通过唯一的key对比异同; diff流程是什么? 答: 新旧节点不同:创建新节点 ➜ 更新父占位符节点 ➜ 删除旧节点; 新旧节点相同且没有子节点:不变; 新旧节点相同且都有子节点:遍历子节点同级比较,做移动、添加、删除三个操作,具体见下图;
2024年01月17日
67 阅读
0 评论
59 点赞
2022-06-13
Vue历史变迁
1.jquery 优点: 高效开发 兼容性好(IE-现代浏览器) 缺陷: 缺乏组织性 类库缺乏业务分层 性能不好(大量操作dom) 2.MVVM 设计模式 model模型 指的是后端传过来的数据。 view视图 指的是所有看到的页面,可以理解为将数据以某种方式呈现给用户。 ViewModel 指的是视图模型,他是连接view和model的桥梁。 Vue 1.x 设计理念 没有虚拟dom(2.x才有虚拟dom) 一个节点元素对应一个Watcher 优点:少量数据性能快 缺点:大量数据性能慢,每个节点都对应一个watcher Vue 2.x 设计理念 第一次渲染 Component Render 直接从模板渲染成虚拟Virtual Dom Tree 后续数据更新 getter setter 发布 Watcher 触发绑定的组件,再转换成虚拟dom,再渲染成真实dom
2022年06月13日
65 阅读
0 评论
8 点赞