存档2021-05-31

vue难点突破之diff 算法原理是什么?patch函数实现过程是怎样的?附面试回答技巧

Vue Diff算法原理 patch函数

什么是Diff算法?

首先需要说明,Diff算法并非Vue独有的专用算法,其他框架如react也有。然后再开始来正式说说Diff, Diff是difference 的缩写,直白来说就是用来“找茬”的,也就是用来对比和查找两者的不同之处的算法。Diff算法的原理是什么,如何用Vue的patch函数来实现这个算法?面试如果问到Diff算法应该如何回答。这些是本文要探讨的几个问题。

Diff算法原理的一些基础准备知识-什么是DOM

客户端(浏览器)要呈现目标网页,需要与服务器进行通信,加在和解析数据,完成呈现,这个过程报告DNS寻址,三次握手等过程,具体这里不展开了,可以查看之前的文章。

总的来说,在客户端主要包含三个过程:1.DOM构造 2.布局 3.渲染呈现

DOM 构造 布局 渲染呈现

什么是虚拟DOM?为什么要操作虚拟DOM?

一般我们要修改页面内容,第一想到的是直接用js操作真实的DOM。但DOM操作的执行速度和js的速度有差异,js远比DOM操作的执行快
并且dom 操作也会引起浏览器的回流和重绘,并且随着移动端的发展手机的参数都不同,也会引发性能的问题。

(回流:当页面中的元素的大小或是位置等发生改变,浏览器会根据改变对页面的结构重新计算
(重绘:当页面中元素的背景,颜色改变引发浏览器对元素重新描绘。)

    虚拟DOM就是为了解决浏览器性能问题而出现,用js对象模拟DOM节点,把一些DOM操作保存到一个js对象,在改变dom之前,会先比较相应虚拟dom的数据,如果需要改变,才会将改变应用到真实dom上。
简单概括有三点:
          1.用JavaScript模拟DOM树,并渲染这个DOM树。
          2.比较新老DOM树,得到比较的差异对象。
          3.把差异对象应用到渲染的DOM树。

有了以上的基础知识,我们开始来探讨一些diff算法和vue下的patch函数实现过程

diff 发生的时机

diff发生在什么时候呢?当然我们可以说在数据更新的时候发生diff,因为数据更新会运行render函数得到虚拟dom树,最后页面重新渲染。

当组件创建的时候,组件所依赖的属性或者数据变化时,会运行一个函数 (下面代码中的updateComponent),该函数会做两件事:

  • 运行_render生成一颗新的虚拟dom树(vnode tree)
  • 运行_updata,传入_render生成的虚拟dom树的根节点,对新旧两棵树进行对比,最终完成对真实dom的更新

核心代码如下,跟原代码有所差异,但都差不多,是这么个意思:

diff 核心代码

diff就发生在_update函数的运行过程中

代码中先调用_render函数得到虚拟dom根节点,然后传入_update函数中,在将updateComponent传入Watcher中,watcher可以监听函数执行的过程,监测函数执行期间用到了哪些响应式数据并且进行依赖收集,关于watcher可以瞅瞅我上一篇文章:一文带你了解vue2之响应式原理

🔨 _update函数在干什么?

_update函数会接收到一个vonde参数,这就是生成的虚拟dom树,同时,_update函数通过当前组件的_vnode属性,拿到的虚拟dom树。_update函数首先会给组件的_vnode属性重新赋值,让它指向新树

简单用代码表示:

_update函数 vonde参数

如果只考虑更新虚拟dom树,这一步已经完成了,但是最终目的是要更新页面,所以就要用到diff进行树的节点对比,所以可以保存下旧树oldVnode用于对比

diff进行树的节点对比

代码如下:

 <body>    
<div id="app"></div>    <script src="./vue.js"></script>    <script>      const vm = new Vue({        el: "#app",      });      
function update(vnode) {        //vnode新树        //this._vnode旧树                let oldVnode = vm._vnode; //保存旧树                this._vnode = vnode; //更新新树      }    </script>  </body>

对比oldVnodevnode就行了,对比的目的就是更新真实dom

节点树

接下来,会判断旧树oldVnode是否存在:

  • 不存在:说明这是第一次加载组件,于是通过内部的patch函数,直接遍历新树,为每个节点生成真实DOM,然后挂载到每个节点的elm属性上
新树

简单用代码表示:

源代码如下:

<body>    <div id="app"></div>    <script src="./vue.js"></script>    <script>      const vm = new Vue({        el: "#app",      });      console.log(vm);      function update(vnode) {        //vnode新树        //this._vnode旧树        let oldVnode = vm._vnode; //保存旧树        this._vnode = vnode; //更新新树        //如果旧树oldVnode不存在        if(!oldVnode){            this.__patch__(vm.$el,vnode);        }      }    </script>  </body>
  • 存在:说明之前已经渲染过该组件,于是通过内部的patch函数,对新旧两棵树进行对比,从而达到下面两个目标:
  1. 完成对所有真实dom的最小化处理
  2. 让新树的节点对应合适的真实dom
patch 函数处理后的节点树

🙌 patch函数的对比流程

术语解释: 一会看到以下字眼,均代表以下意思

1.「相同」:是指两个虚拟节点的标签类型和key值均相同,但input元素还要看type属性。这个术语在vue源码中叫sameVnode,它是一个函数,用来判断两个虚拟节点是不是同一个节点

例:两个虚拟节点div是否相同

<div>法医</div><div>前端猎手</div>

标签类型都为div,key值不仅仅在v-for遍历中,也可以用在任何标签中,上面两个div中没有key值,所以都为undefined,所以标签类型和key值都相同,不用看内容是否相同,它是另一个节点:文本节点

<div key="fayi">法医</div><div key="qdls">前端猎手</div>

上面两个虚拟节点是不同的,因为key值不同

 <input type="text"> <input type="radio">

上面两个虚拟节点是不同的,因为input不仅仅要看key值和标签类型,还要看type是否相同

2.「新建元素」:是指根据一个虚拟节点提供的信息,创建一个真实dom元素,同时挂载到虚拟节点的elm属性上

3.「销毁元素」:是指:vnode.elm.remove()

4.「更新」:是指对两个虚拟节点进行对比更新,它仅发生在两个虚拟节点「相同」的情况下。具体过程稍后描述。

5.「对比子节点」:是指对两个虚拟节点的子节点进行对比,具体过程稍后描述

详细流程

根节点比较

patch函数首先对根节点进行对比

如果两个节点:

  • 「相同」,进入 「更新」 流程
  1. 将旧节点的真实dom赋值到新节点:newVnode.elm = oldVnode.elem,旧节点会被垃圾回收机制回收
  2. 对比新节点和旧节点的属性,有变化的更新到真实dom中
  3. 当前新旧两个节点处理完成,开始 「对比子节点」
  • 不 「相同」
  1. 新节点递归, 「新建元素」
  2. 旧节点 「销毁元素」

对比子节点

虚拟dom树已经完成,就剩修改真实dom了,但是修改真实dom的效率是比较耗时的,vue的原则是能不改就不改,尽量啥也别做,在「对比子节点」时,vue一切的出发点,都是为了:

  • 尽量啥也别做
  • 不行的话,尽量仅改动元素属性
  • 还不行的话,尽量移动元素,而不是删除和创建元素
  • 实在不行的话,删除和创建元素

对比流程:

对比流程

图片说明:

  • 黄色圆圈:表示旧子节点和新子节点所对应的相同节点类型
  • 数字:表示key值,用来区分是不是同一个节点
  • 蓝色方块:表示对比之前旧子节点所对应的真实dom
  • 箭头:分别表示头指针和尾指针

接下来,我们要做的就是对比旧子节点新子节点之间的差异,目标是改变真实dom,并且将新虚拟子节点对应到真实dom里面去,vue使用两个指针分别指向新旧子节点树的头和尾

步骤:

  1. 首先对比新树和旧树的头指针,瞅瞅两个节点是否一样,从图中可以看到是一样的,如果一样则进入 「更新」 流程:先将旧节点的真实dom赋值到新节点(真实dom连线到新子节点),然后循环对比新旧节点的属性,看看有没有不一样的地方,将有变化的更新到真实dom中,最后还要采用深度优先(一颗树的节点走到尽头,再走另一个节点)的方式递归循环这两个新旧子节点是否还有子节点,如果存在,则同理,这里我们就假设它不存子节点。灰色表示已经处理完成,然后两个头指针往后移动
  1. 接下来,继续比较两个头指针,看看两个节点是否一样,很明显,两个节点是不一样的,因为key值不同,不一样的时候它不会销毁删除从建立,吃个🍗压压惊,淡定!前面有提到尽量别操作dom,它一定会找到一样的节点,一条道走到黑,然后会对比尾指针,可以看到尾指针是一样的,跟第一步是一样的:一顿操作猛如虎,先将旧节点的真实dom赋值到新节点(真实dom连线到新子节点),然后循环对比新旧节点的属性,将有变化的更新到真实dom中,接着还要递归循环这两个新旧子节点是否还有子节点,最后两个尾指针往前移动
  1. 然后继续比较头指针,很明显不一样,尾指针呢?也不一样,因为key值还是不一样。随后它会比较头指针和尾指针,看看是否一样,可以看到旧节点的圆2头指针和新节点圆2尾指针是一样的,所以操作跟前两步是一样的,又是一顿操作猛如虎,结果如下图:

这里我们要注意的是真实dom必须和新虚拟子节点要一一对应上的,所以除了更新变化的地方之外还要进行位置移动,移动到旧树尾指针的后面,最后旧树头指针往后移动,新树尾指针往前移动,如下图:

  1. 继续比对,新旧头指针不同,尾指针不同,两个头尾也不同,然后它会以新树头指针为基准,循环旧虚拟子节点,看看新树圆3是否存在于旧虚拟子节点,存在的话在哪个位置,找到之后进行复用,连线,有变化的地方更新到真实dom,操作跟前面几步一样,真实dom也要进行位置移动,移动到旧树头指针之前。随后新树头指针继续往后移动到圆9位置,如下图:
  1. 继续比对,新旧头指针不同,尾指针不同,但新树头指针和旧树尾指针相同,操作跟前面几步相同,但依然需要进行位置移动,移动到旧树头指针之前。随后新树头指针往后移动,与新树尾指针重合,旧树尾指针向前移动到圆1位置,如下图:
  1. 继续比对,新旧两树头指针不同,尾指针不同,两个头尾也不同,然后它以新树头指针为基准,循环旧虚拟子节点,找圆8在旧树中存不存在,从图中可以看出,并不存在,这个时候确实没办法了,只能 「新建元素」。随后新树头指针继续向后移动到圆2位置,如图:
  1. 当头指针移动到圆2位置时,头指针已经不再是有效的了,当头指针超过尾指针的时候,循环结束,从过程我们可以看到新树先循环完成,但是旧树还有剩余的节点,这说明旧树中剩余的节点都是应该被删除的节点,所对应的真实dom也会被移除

最终真实dom生成完毕,整个过程我们只新建了一个元素,如下图:

面试被问到关于diff算法的常见问题,应该如何回答

面试问到diff的问题一般是跟diff 算法在vue的patch实现原理和机制有关,务必理解原理和实现机理,以下是面试中经常被问到的问题,希望对大家有所帮助

当组件创建和更新时,vue会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom

对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程

在对比时,vue采用深度优先、同级比较的方式进行比对。同级比较就是说它不会跨越结构进行比较

在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的

具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。

在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。

这样一直递归的遍历下去,直到整棵树完成对比。

好了,这是本文对开篇三个问题的探讨,如有错误和遗漏,还请帮忙指正和补充。

谷歌正式发布Fuchsia OS操作系统,在鸿蒙即将官宣的时刻

华为鸿蒙OS- Google Fuchsia OS

沉寂了数年之久的Fuchsia OS,近日迎来了5年来最重要的时刻,曾经引起巨大轰动的Fuchsia OS正式发布,即将迈入大规模的实际场景应用。而另一边,万众瞩目的华为本土操作系统——鸿蒙,也将在6月2日正式官宣。这两个苹果IOS之外的两个最被关注的系统,一个被视为未来的安卓,一个被视为安卓之外的新选择,都很大程度决定这两个科技巨擘的未来。而发布的时间如此接近,还真有点枕巾对麦芒的味道。

谷歌Fuchsia OS正式开始接入智能设备

9to5Google 报道,谷歌已经向其确认,Fuchsia OS 将向 2018 年发布的初代 Nest Hub 智能显示器推出。更新后的 Nest Hub 将不会有功能改变,但系统底层将由基于 Linux 的 Cast OS 变更为 Fuchsia OS。

谷歌 Fuchsia OS 项目技术负责人 Petr Hosek 在推特上庆祝了新平台的发布:“今天是个重要的日子,我们发布新操作系统啦!”

New OS-Google Fuchsia OS

Nest Hub 基于 Fuchsia OS 的更新会在未来几个月内陆续推出,考虑到界面和体验将保持不变,用户可能不会有直接的感知。自 2016 年以来 Fuchsia 的发展一直备受关注,从实验性的用户界面开始,一直到运行至一些内部测试设备,包括谷歌智能家居和 Chromebook 系列的全部产品。如今,谷歌宣布将在几个月内为初代 Nest Hub 智能显示器用户推送 Fuchsia OS,这表明它已经准备好在个人设备上提供服务。

Google Fuchsia OS on Nest Hub

不过,Fuchsia 不只是智能显示操作系统。彭博社(Bloomberg)于 2018 年发布的一份报告迄今都完全命中 Fuchsia 的发布计划,其中提到谷歌希望“在三年之内”首先在联网的家用设备上发布该操作系统。该报告还指出了 Fuchsia 的下一步措施,包括计划在 2023 年大规模扩张到智能手机和笔记本电脑。

谷歌在 Fuchsia OS 上投入了数百人,经过五年多的发展,Fuchsia OS 已经开始引起其他行业巨头的关注。最近,三星开始与谷歌合作开发该项目。很多人猜测 Fuchsia OS 未来可能会全面取代 Android 与 Chrome OS,而三星可能会成为最先抛弃 Android 改用 Fuchsia OS 的手机厂商。

谷歌 Fuchsia OS 是什么?

Fuchsia 是一套全新的操作系统,其项目定位一直在发展变化。

作为一套新的操作系统,Fuchsia 最初于 2016 年首次亮相于谷歌代码库与 GitHub,该项目完全开源:https://fuchsia.googlesource.com/。更重要的是,Fuchsia 并非基于 Linux 内核,而 Linux 内核又恰恰是 Android(谷歌打造的移动操作系统)与 Chrome OS(谷歌台式机与笔记本电脑操作)的核心基础。很明显,Fuchsia 承载着谷歌更大的野心。

谷歌 Android 工程副总裁 Dave Burke 在 2017 年接受采访时如何介绍 Fuchsia:“Fuchsia 是一个早期实验项目。大家可能都知道,我们在谷歌筹划过不少非常酷炫的早期项目。我认为最有趣的点在于 Fuchsia 直接开源,每个人都可以查看成果并做出评论。与其他早期项目一样,Fuchsia 也会不断发展变化。”

时间到了 2018 年,Fuchsia 开发者 Travis Geiselbrecht 通过公共 Fuchsia IRC 频道强调,这套操作系统绝不是“玩具”,于是情况变得更加扑朔迷离。他证实称,Fuchsia 的开发进度已经颇为可观,而且参与其中的谷歌开发人员可以随意进行兴趣化探索。在他看来,Fuchsia“绝不是那种用掉就丢的垃圾项目。”

之后的两年 Fuchsia 蜇伏了起来,直到 2020 年谷歌再次推动宣传,希望通过平台开放为其吸引更多软件开发支持者。2021 年初,先是项目的 F1 分支,之后又有 F3 分支,随着一个个重要代码开发步骤的落地,Fuchsia 的面貌及发展方向也开始愈发清晰,事实证明这套操作系统已经达到了一系列重要发展里程碑。

谷歌 Fuchsia OS 的将会用来做什么,用在哪里?

Fuchsia 只是一套内核。谷歌可能想用 Fuchsia 证明自己对未来的探索。

与基于 Linux 的 Android 或 Chrome OS 不同,Fuchsia 基于 Zircon(原名 Magenta)构建而成。该内核开始时使用的是 C ++ 代码,为了实现其安全目标,现在正朝着 Rust 发展(现在已经达到 50%)。一年前谷歌还向 Fuchsia 添加了对 Swift 的支持。

目前比较流行的观点是,Fuchsia 应该代表一款新型操作系统,未来用于将 Chrome OS 与 Android 统一在同一套系统之下(自 2015 年以来始终存在此类传闻)。但根据最近浮出水面的说明文档、代码以及 UI 来看,这套操作系统好像并不是 Android 与 Chrome OS 的融合体、甚至不属于任何完整操作系统。目前,它还只是个内核项目——也就是操作系统的核心所在。

谷歌公司在内部文档中指出,Fuchsia 主要面向采用“高速处理器”加“低内存容量”的“现代手机与个人计算机平台”。文档还明确提到,“Fuchsia 不是 Linux”。Fuchsia 的 GitHub 页面上出现了两位顶尖嵌入式系统开发者的姓名,一位是谷歌高级软件工程师、另一位则是前 Android TV 与 Nexus Q 项目工程师。

此外,卡片化设计的早期用户界面 Armadillo 内置于谷歌的 Flutter SDK 之内,而后者专门用于创建可在多种设备及操作系统上运行的跨平台代码。使用 Armadillo,用户可以随意拖动不同卡片实现屏幕拆分、或者在选项卡式界面中使用。

同时,Fuchsia OS 的核心独立于硬件规格,使用模块化方法,这意味着它将不再是一大堆代码,而是将其分割成多个构建块或“包”,制造商能够根据设备选择 Fuchsia 的功能。

Fuchsia OS 模块化

Fuchsia OS 中的模块化

Fuchsia 的模块化框架带来的另一个优势是,它可能仅通过安装更新的组件就可以添加新功能。从实际出发,模块化不仅可以解决系统更新时可能出现 Bug 的问题,而且还可以加快应用程序的更新速度。这种模块化方法对于 Fuchsia 所提供的统一体验至关重要。

因此有猜测,未来我们可能会看到 Fuchsia 与其他新兴技术融合在一起,发展成为一个集合的、相互连接的设备系统,这样操作系统就不会单独运行在每个设备上。取而代之的是,可以在每个设备上以分散化的实例形式运行这个无所不包的 OS,所有这些实例都可以协同工作。

谷歌 Fuchsia OS 是否会取代 Android?

新系统确实能解决 Android 中的不少问题。但 Android 已经全面铺开,何必重新发明轮子?

Android 最初是为带有 QWERTY 键盘的智能手机设计的,后来逐渐适配触摸屏控制。并且有说法是 Android 在设计时并未考虑虚拟现实或增强现实。鉴于它已经有十年历史了,因此如果谷歌希望应对下一个十年的挑战,那么比起修改 Android 代码,也许重新开始设计一个新操作系统才是更好的办法。

Android 本身的碎片化问题仍然很严重,根源当然是几十家手机制造商推出的数百款设备都在使用不同的自定义 Android 版本。另外,由于 Android 系统为开源项目,所以在更新方面也有不少冲突。谷歌为 Android 制定了年度更新发布时间表,但要真正向整个生态系统推开还需要一段时间。

目前,谷歌仍然只能将 Android 新版本交付给 OEM 厂商和电信运营商,再由他们安装并加载至目标硬件上,这种无法由谷歌直接控制的体系必然导致碎片化加剧。另外,Android 还基于 Linux 内核,而后者目前不仅面临诸多法律问题的困扰,而且内核还经历了一番全面调整,极大提升了出现 bug 及安全漏洞的可能性。

也许一套全新操作系统平台能帮助谷歌解决以上所有问题,同时也将有效回避昂贵的专利许可成本。由于从零开始构建而成,这套现代化操作系统将更安全、更可靠且优化度更高。另外,新系统既可以采用模块化设计、也可以强调统一性,保证更全面地覆盖各类设备。但无论如何展望,我们都需要回答最核心的灵魂拷问:Android 已经全面铺开,何必还要重新发明轮子?

华为鸿蒙OS宣布将于6月2日正式发布

谷歌很早之前就封禁了华为设备,导致华为海外欧美市场急剧下滑,华为也因此从世界第一跌落神坛。

鸿蒙系统的推出是无奈,是一种反击,也包含着华为的野心和决心。

宣布将于62日晚8召开鸿蒙操作系统及华为全场景新品发布会,届时会发布鸿蒙OS正式版。

  按照近期进行开发者测试的用户反馈来看,鸿蒙OS目前已经非常完善,稳定性和性能都十分出色,与基于安卓的EMUI相比也毫不逊色,还能带来一些此前在安卓无法实现的功能。

  据悉,目前华为正在与全球排名前200的App厂商沟通合作,共同开发跨终端设备的应用。华为公司预计,2021年底搭载鸿蒙操作系统的设备数量将达3亿台,其中华为设备超过2亿台,面向第三方合作伙伴的各类终端设备数量超过1亿台。此外,鸿蒙系统也向第三方手机厂商开源

谷歌Fuchsia OS 和华为鸿蒙谁将赢得未来?

  而华为在最初研发鸿蒙系统的时候就有很大的野心,鸿蒙系统不仅仅是针对手机推出的操作系统,鸿蒙OS还将打通电脑、平板、智慧屏、手表、车机等设备,是布局IoT、汽车领域的重要一环。 

一个被认为全新的安卓,一个被视为全新的系统,这场事关生死的博弈,谷歌Fuchsia OS 和华为鸿蒙谁将赢得未来?