diff算法知多少

二、 Diff算法


传统Diff算法

遍历两棵树中的每一个节点,每两个节点之间都要做一次比较。

比如 a->e 、a->d 、a->b、a->c、a->a

  • 遍历完成的时间复杂度达到了O(n^2)

  • 对比完差异后还要计算最小转换方式,实现后复杂度来到了O(n^3)

在这里插入图片描述

Vue优化的Diff算法

Vue的diff算法只会比较同层级的元素,不进行跨层级比较

在这里插入图片描述

三、 Vue中的Diff算法实现


Vnode分类

EmptyVNode: 没有内容的注释节点

TextVNode: 文本节点

ElementVNode: 普通元素节点

ComponentVNode: 组件节点

CloneVNode: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true

Patch函数


patch函数接收以下参数:

  1. oldVnode:旧的虚拟节点

  2. Vnode:新的虚拟节点

  3. hydrating:是否要和真实DOM混合

  4. removeOnly:特殊的flag,用于 transition-group

处理流程大致分为以下步骤:

  1. vnode不存在,oldVnode存在时,移除oldVnode

  2. vnode存在,oldVnode不存在时,创建vnode

  3. vnode和oldVnode都存在时

  4. 如果vnode和oldVnode是同一个节点(通过sameVnode函数对比 后续详解),通过patchVnode进行后续比对工作

  5. 如果vnode和oldVnode不是同一个节点,那么根据vnode创建新的元素并挂载至oldVnode父元素下。如果组件根节点被替换,遍历更新父节点element。然后移除旧节点。如果oldVnode是服务端渲染元素节点,需要用hydrate函数将虚拟dom和真是dom进行映射

源码如下,已写好注释便于阅读

return function patch(oldVnode, vnode, hydrating, removeOnly) {

// 如果vnode不存在,但是oldVnode存在,移除oldVnode

if (isUndef(vnode)) {

if (isDef(oldVnode)) invokeDestroyHook(oldVnode)

return

}

let isInitialPatch = false

const insertedVnodeQueue = []

// 如果oldVnode不存在,但是vnode存在时,创建vnode

if (isUndef(oldVnode)) {

isInitialPatch = true

createElm(vnode, insertedVnodeQueue)

} else {

// 剩余情况为vnode和oldVnode都存在

// 判断是否为真实DOM元素

const isRealElement = isDef(oldVnode.nodeType)

if (!isRealElement && sameVnode(oldVnode, vnode)) {

// 如果vnode和oldVnode是同一个(通过sameVnode函数进行比对 后续详解)

// 受用patchVnode函数进行后续比对工作 (函数后续详解)

patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)

} else {

// vnode和oldVnode不是同一个的情况

if (isRealElement) {

// 如果存在真实的节点,存在data-server-render属性

if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {

// 当旧的Vnode是服务端渲染元素,hydrating记为true

oldVnode.removeAttribute(SSR_ATTR)

hydrating = true

}

// 需要用hydrate函数将虚拟DOM和真实DOM进行映射

if (isTrue(hydrating)) {

// 需要合并到真实DOM上

if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {

// 调用insert钩子

invokeInsertHook(vnode, insertedVnodeQueue, true)

return oldVnode

} else if (process.env.NODE_ENV !== ‘production’) {

warn(

'The client-side rendered virtual DOM tree is not matching ’ +

'server-rendered content. This is likely caused by incorrect ’ +

'HTML markup, for example nesting block-level elements inside ’ +

'

, or missing . Bailing hydration and performing ’ +

‘full client-side render.’

)

}

}

// 如果不是服务端渲染元素或者合并到真实DOM失败,则创建一个空的Vnode节点去替换它

oldVnode = emptyNodeAt(oldVnode)

}

// 获取oldVnode父节点

const oldElm = oldVnode.elm

const parentElm = nodeOps.parentNode(oldElm)

// 根据vnode创建一个真实DOM节点并挂载至oldVnode的父节点下

createElm(

vnode,

insertedVnodeQueue,

oldElm._leaveCb ? null : parentElm,

nodeOps.nextSibling(oldElm)

)

// 如果组件根节点被替换,遍历更新父节点Element

if (isDef(vnode.parent)) {

let ancestor = vnode.parent

const patchable = isPatchable(vnode)

while (ancestor) {

for (let i = 0; i < cbs.destroy.length; ++i) {

cbs.destroyi

}

ancestor.elm = vnode.elm

if (patchable) {

for (let i = 0; i < cbs.create.length; ++i) {

cbs.create[i](emptyNode, ancestor)

}

// #6513

// invoke insert hooks that may have been merged by create hooks.

// e.g. for directives that uses the “inserted” hook.

const insert = ancestor.data.hook.insert

if (insert.merged) {

// start at index 1 to avoid re-invoking component mounted hook

for (let i = 1; i < insert.fns.length; i++) {

insert.fnsi

}

}

} else {

registerRef(ancestor)

}

ancestor = ancestor.parent

}

}

// 销毁旧节点

if (isDef(parentElm)) {

// 移除老节点

removeVnodes(parentElm, [oldVnode], 0, 0)

} else if (isDef(oldVnode.tag)) {

// 调用destroy钩子

invokeDestroyHook(oldVnode)

}

}

}

// 调用insert钩子并返回节点

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)

return vnode.elm

}

sameVnode函数

Vue怎么判断是不是同一个节点?流程如下:

  1. 判断Key值是否一样

  2. tag的值是否一样

  3. isComment,这个不用太关注。

  4. 数据一样

  5. sameInputType(),专门对表单输入项进行判断的:input一样但是里面的type不一样算不同的inputType

从这里可以看出key对diff算法的辅助作用,可以快速定位是否为同一个元素,必须保证唯一性。

如果你用的是index作为key,每次打乱顺序key都会改变,导致这种判断失效,降低了Diff的效率。

因此,用好key也是Vue性能优化的一种方式。

  • 源码如下:

function sameVnode(a, b) {

return (

a.key === b.key && (

(

a.tag === b.tag &&

a.isComment === b.isComment &&

isDef(a.data) === isDef(b.data) &&

sameInputType(a, b)

) || (

isTrue(a.isAsyncPlaceholder) &&

a.asyncFactory === b.asyncFactory &&

isUndef(b.asyncFactory.error)

)

)

)

}

patchVnode函数

前置条件vnode和oldVnode是同一个节点

执行流程:

  1. 如果oldVnode和vnode引用一致,可以认为没有变化,return

  2. 如果oldVnode的isAsyncPlaceholder属性为true,跳过检查异步组件,return

  3. 如果oldVnode跟vnode都是静态节点,且具有相同的key,同时vnode是克隆节点或者v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作,return

  4. 如果vnode不是文本节或注释节点

  5. 如果vnode和oldVnode都有子节点并且两者子节点不一致时,就调用updateChildren更新子节点

  6. 如果只有vnode有自子节点,则调用addVnodes创建子节点

  7. 如果只有oldVnode有子节点,则调用removeVnodes把这些子节点都删除

  8. 如果vnode文本为undefined,则清空vnode.elm文本

  9. 如果vnode是文本节点但是和oldVnode文本内容不同,只需更新文本。

源代码如下,已写好注释便于阅读

function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {

// 如果新老节点引用一致,直接返回。

if (oldVnode === vnode) {

return

}

const elm = vnode.elm = oldVnode.elm

// 如果oldVnode的isAsyncPlaceholder属性为true,跳过检查异步组件

if (isTrue(oldVnode.isAsyncPlaceholder)) {

if (isDef(vnode.asyncFactory.resolved)) {

hydrate(oldVnode.elm, vnode, insertedVnodeQueue)

} else {

vnode.isAsyncPlaceholder = true

}

return

}

// 如果新旧都是静态节点,vnode的key也相同

// 新vnode是克隆所得或新vnode有 v-once属性

// 则进行赋值,然后返回。vnode的componentInstance 保持不变

if (isTrue(vnode.isStatic) &&

isTrue(oldVnode.isStatic) &&

vnode.key === oldVnode.key &&

(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))

) {

vnode.componentInstance = oldVnode.componentInstance

return

}

let i

const data = vnode.data

// 执行data.hook.prepatch 钩子

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)) {

// 遍历调用 cbs.update 钩子函数,更新oldVnode所有属性

// 包括attrs、class、domProps、events、style、ref、directives

for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)

// 执行data.hook.update 钩子

if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)

}

// Vnode 的 text选项为undefined

if (isUndef(vnode.text)) {

if (isDef(oldCh) && isDef(ch)) {

//新老节点的children不同,执行updateChildren方法

if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

} else if (isDef(ch)) {

// oldVnode children不存在 执行 addVnodes方法

if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ‘’)

addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

} else if (isDef(oldCh)) {

// vnode不存在执行removeVnodes方法

removeVnodes(elm, oldCh, 0, oldCh.length - 1)

} else if (isDef(oldVnode.text)) {

// 新旧节点都是undefined,且老节点存在text,清空文本。

nodeOps.setTextContent(elm, ‘’)

}

} else if (oldVnode.text !== vnode.text) {

// 新老节点文本内容不同,更新文本

nodeOps.setTextContent(elm, vnode.text)

}

if (isDef(data)) {

// 执行data.hook.postpatch钩子,至此 patch完成

if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)

}

}

updateChildren函数

重点!!!

前置条件:vnode和oldVnode的children不相等

整体的执行思路如下:

  1. vnode头对比oldVnode头

  2. vnode尾对比oldVnode尾

  3. vnode头对比oldVnode尾

  4. vnode尾对比oldVnode头

  5. 只要符合一种情况就进行patch,移动节点,移动下标等操作

  6. 都不对再在oldChild中找一个key和newStart相同的节点

  7. 找不到,新建一个。

  8. 找到,获取这个节点,判断它和newStartVnode是不是同一个节点

  • 如果是相同节点,进行patch 然后将这个节点插入到oldStart之前,newStart下标继续移动

  • 如果不是相同节点,需要执行createElm创建新元素

为什么会有头对尾、尾对头的操作?

  • 可以快速检测出reverse操作,加快diff效率。

源码如下 已写好注释便于阅读:

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {

// 定义变量

let oldStartIdx = 0 // 老节点Child头下标

let newStartIdx = 0 // 新节点Child头下标

let oldEndIdx = oldCh.length - 1 // 老节点Child尾下标

let oldStartVnode = oldCh[0] // 老节点Child头结点

let oldEndVnode = oldCh[oldEndIdx] // 老节点Child尾结点

let newEndIdx = newCh.length - 1 // 新节点Child尾下标

let newStartVnode = newCh[0] // 新节点Child头结点

let newEndVnode = newCh[newEndIdx] // 新节点Child尾结点

let oldKeyToIdx, idxInOld, vnodeToMove, refElm

// removeOnly is a special flag used only by

// to ensure removed elements stay in correct relative positions

// during leaving transitions

const canMove = !removeOnly

if (process.env.NODE_ENV !== ‘production’) {

checkDuplicateKeys(newCh)

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
env.NODE_ENV !== ‘production’) {

checkDuplicateKeys(newCh)

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-Y0EMJzjh-1715343938277)]

[外链图片转存中…(img-6skFSsmU-1715343938278)]

[外链图片转存中…(img-0hZODsoj-1715343938278)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 30
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前端的diff算法在虚拟DOM的实现中起着重要的作用,它用于比较两个虚拟DOM树的差异,然后只对差异部分进行更新,以提高性能和效率。在面试中,可能会问到一些与前端diff算法相关的问题。以下是一些常见的问题和答案供参考: 1. 什么是前端diff算法? 前端diff算法是指用于比较两个虚拟DOM树之间差异的算法。通过比较新旧虚拟DOM树的差异,可以确定需要更新的部分,从而减少不必要的页面重绘和重新渲染,提高性能和效率。 2. 常见的前端diff算法有哪些? 常见的前端diff算法包括: - O(n²)算法:遍历新旧节点进行比较,时间复杂度为O(n²),性能较差。 - O(n)算法:采用双指针或者哈希表等方式,将遍历时间复杂度优化为O(n),例如React中采用的Virtual DOM diff算法。 - Fiber算法:React Fiber算法是一种增量渲染算法,通过将更新操作拆分为多个单元,可以在每个帧中执行一部分工作,从而提高用户体验。 3. React中采用的前端diff算法是什么? React中采用的是一种基于O(n)算法的Virtual DOM diff算法。该算法通过遍历新旧虚拟DOM树的节点,对比差异并更新只有差异的部分,以提高性能。 4. 前端diff算法的优化策略有哪些? 前端diff算法可以通过以下优化策略提高效率: - 对比时忽略静态节点:对比时可以忽略没有变化的静态节点,减少不必要的对比操作。 - 使用唯一标识符:给每个节点添加唯一标识符,可以更精确地确定哪些节点需要更新。 - 列表元素的优化:在对比列表元素时,可以使用Key属性标识唯一性,以减少重新排序和重渲染的开销。 这些问题涵盖了前端diff算法的概念、常见算法以及优化策略。在面试中,你可以根据自己的理解和经验进行回答。希望对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值