关于算法:越来越受欢迎的Vue想学么90后小姐姐今儿来教你

9次阅读

共计 8671 个字符,预计需要花费 22 分钟才能阅读完成。

摘要:Vue 的相干技术原理成为了前端岗位面试中的必考知识点,把握 Vue 对于前端工程师来说更像是一门“必修课”。

本文原作者为尹婷,善于前端组件库研发和微信机器人。

咱们发现,Vue 越来越受欢迎了。

不论是 BAT 大厂,还是守业公司,Vue 都被宽泛的利用。比照 Angular 和 React,三者都是十分优良的前端框架,但从 GitHub 上来看,Vue 曾经达到了 170 万的 Star。Vue 的相干技术原理也成为了前端岗位面试中的必考知识点,把握 Vue 对于前端工程师来说更像是一门“必修课”。为此,华为云社区邀请了 90 后前端开发工程师尹婷带来了《Vue3.0 新个性介绍以及搭建一个 vue 组件库》的分享。

理解 Vue3.0 先从六大个性说起

Vue.js 是一个 JavaScriptMVVM 库,是一套构建用户界面的渐进式框架。在 2019 年 10 月 05 日凌晨,Vue3 的源代码 alpha。目前曾经公布正式版,作者示意,Vue 3.0 具备六大个性:Tree Shaking;Composition;Fragment;Teleport;Suspense;渲染 Performance。渲染 Performance 次要是框架外部的性能优化,绝对比拟底层,本文会次要为大家介绍前四个个性的解读。

Tree Shaking

大多数编译器都会为咱们的代码进行一个死代码的去除工作。首先咱们要理解一下,什么是死代码呢?

以下几个个性的代码,咱们把它称之为死代码:代码不会被执行,不可达到;代码执行的后果不会被用到;代码只会影响死变量(只写不读)。比方咱们给一个变量赋值,然而并没有去用这个变量,那么这就是一个死变量。这就是在咱们定义阶段会把它去除的一部分,比如说 roll up 打消死代码的工作。

如上图示例,右边是开发的源码提供的两个函数,但最终只用到了 baz 函数。在最初打包的时候,会把 foo 函数去除掉,只把 baz 这个函数打包进浏览器外面运行。Tree Shaking 是打消死代码的一种形式,更关注于无用模块的打消,打消那些援用了但并没有被应用的模块。

右边这块代码,export 有两个函数,一个是 post,一个是 get,然而在咱们生产里边真正应用到只有 post。那么 rollup 在打包之后,就会间接打消掉 get 的函数,而后只把 post 的函数打包进入咱们的生产里。除了 rollup 反对这个个性外,webpack 也反对。

接下来,咱们看一下 VUE3.0 对 Tree Shaking 的反对都做了哪些事件?

首先以 VUE2 和 VUE3 对 nextTick 的应用进行比照:VUE2 把 nextTick 挂载到 VUE 实例上的一个 global API 式;VUE3 先把 nextTick 模块剔除,在要应用的时候,再把这个模块引入。
通过这个比照,咱们能够看到应用 VUE2 的时候,即便没有 nextTick 或者其余办法,但因为它是一个 GLOBA API,它肯定会被挂载到一个实例上,最初打包生产代码的时候,会把这个函数给打包进去,这一段代码进而也会影响到文件体积。在 VUE3.0 如果不须要这个模块的话,最初打包的这个文件里边就不会有这一块代码。通过这种形式就缩小了最初生产代码的体积。

当然,不只是 nextTick,在 VUE3.0 外部也做了其余很多 tree-shaking。例如:如果不应用 keep-alive 组件或 v -show 指令,它会少引入很多跟 keep-alive 或者 v -show 不相干的包。

上图为 Vue2.0 的这段代码,右边是引入 utils 函数,而后把这个函数指为 mixins。这一段代码是在 Vue2 里边是最罕用到的,但这段代码是有问题的。

如果对这个我的项目不相熟,第一次看到这个代码的时候,因为不晓得这个 utils 里边有哪些属性和办法,也就是说这个 mixins 对于开发者就是个黑盒。很容易遇到一种场景:在开发组件初期,利用了 mixins 的一个办法,当初不须要应用该办法了,在删除的过程发现不晓得其余的中央是否援用过 mixins 其余的属性和办法。

Composition

如果应用的是 Vue3.0 的 Composition,该怎么躲避这个问题呢?如上图所示,假如它是一个组件实例,咱们应用 useMouse 函数并返回了 X 和 Y 两个变量。从右边代码能够看到 useMouse 函数就是根,它监听了鼠标的挪动事件之后,返回了鼠标的 XY 坐标。通过这种形式来组织代码,就能够很明确的晓得这个函数返回的变量和扭转的值。

接下来咱们再看一个 Composition 的例子:右边是在 Vue2 中最罕用的一段代码,首先在 data 里边申明 first name 和 last name,而后在回帖的时候去申请接口,拿到接口返回到值,在 computed 之后获取他的 full Name。那么,这段代码的问题是什么呢?

这里的 computed,因为咱们不晓得返回的 full Name 的逻辑是什么。在获取了 data 之后,是心愿通过 data 的返回值来拿到它的 first name 和 last name,而后来获取它的 full name。然而这一段代码的逻辑在获取接口之后就曾经断掉,这就是 Vue2.0 设计不合理的一个中央,导致咱们的逻辑是决裂派的,决裂在个配置下。那么,如果用 Composition 的话,怎么样实现呢?

申请接口之后,间接拿到它的返回数据,而后把这个返回数据的值赋给 computed 函数里,这里就能够拿到 full Name。通过这段代码能够看到,逻辑是更加的聚合了。

如何做到应用 useMouse 函数,里边的变量也是可响应的。在 Vue 3.0 中提供了两个函数:reactive 和 ref。reactive 能够传一个对象进去,而后这个函数返回之后的 state,是可响应的;ref 是间接传一个值进去,而后返回到认识对象,它也是可响应的。如果咱们在 setup 函数里边返回一个可响应值的对象,是能够在字符串模板渲染的时候应用。比方,有时候咱们间接在批改 data 的时候,视图也会相应的扭转。

Vue2 中,个别会采纳 mixins 来复用逻辑代码,但存在一些问题:例如代码起源不清晰、办法属性等抵触。基于此,在 vue3 中引入了 Composition API(组合 API),应用纯函数分隔复用代码,和 React 中的 hooks 的概念很类似。

Composition 的长处是裸露给模板的属性起源清晰,它是从函数返回的;第二,能够进行逻辑重用;第三,返回值能够被任意的命名,不存在机密空间的抵触;第四,没有创立额定的组件实力带来的性能损耗。

以前咱们如果想要获取一个响应式的 data,咱们必须要把这个 data 放在 component 里边,而后在 data 里边进行申明,这样的话能力使这个对象是可响应的,当初可间接应用 reactive 和 ref 函数就能够使被保变成可响应的。

Fragment

在书写 vue2 时,因为组件必须只有一个根节点,很多时候会增加一些没有意义的节点用于包裹。Fragment 组件就是用于解决这个问题的(这和 React 中的 Fragment 组件是一样的)。

Fragment 其实就是在 Vue2 的一个组间里边,它的 template 必须要有一个根的 DIV 把它包住,而后再写里边的 you。在 Vue3,咱们就不须要这个根的 DIV 来把这个组件包住了。上图就是 2 和 3 的比照。

Teleport

Teleport 其实就是 React 中的 Portal。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优良的计划。Teleport 提供一个 Teleport 的组件,会指定一个指标的元素,比如说这里指定的是 body,而后 Teleport 任何的内容都会渲染到这个指标元素中,也就是说上面的这一部分 Teleport 代码,它会间接渲染到 body。

那么对于 Teleport 利用的地位,咱们能够为大家举个例子来阐明一下。比如说咱们在做组件的时候,常常会实现一个 dialog。dialog 的背景是一个黑的铺满全屏 DIV,咱们对它的布局是 position: absolute。如果父级元素是 relative 布局,咱们的这个背景层就会受它的父元素的影响。那么此时,如果用 Teleport 间接把父组件定为 body,这样它就不会再受到副组件元素款式的影响,就能够确认一个咱们想要的彩色背景画。

上面我写一下 react 和 vue 的 diff 算法的比对, 我是一边写代码,一边写文章,整顿一下思路。注:这里只探讨 tag 属性雷同并且多个 children 的状况,不雷同的 tag 间接替换,删除,这没啥好写的。

用这个例子来阐明:

简略 diff,把原有的删掉,把更新后的插入。

变动前后的标签都是 li,所以只用比对 vnodeData 和 children 即可,复用原有的 DOM。

先只从这个例子登程, 我只用遍历旧的 vnode,而后把旧的 vnode 和新的 vnode patch 就行。

这样就省掉移除和新增 dom 的开销,当初的问题是,我的例子刚好是新旧 vnode 数量一样,如果不一样就有问题,示例改成这样:

实现思路改成:先看看是旧的长度长,还是新的长,如果旧的长,我就遍历新的,而后把多进去的旧节点删掉,如果新的长,我就遍历旧的,而后多进去的新 vnode 加上。

依然有可优化的空间,还是上面这幅图:

通过咱们下面的 diff 算法,实现的过程会比对 preve vnode 和 next vnode,标签雷同,则只用比对 vnodedata 和 children。发现

 标签的子节点(文本节点 a,b,c)不同,于是别离删除文本节点 a,b,c,而后从新生成新的文本节点 c,b,a。然而实际上这几个

 只是地位不同,那优化的计划就是复用曾经生成的 dom,把它挪动到正确的地位。

怎么挪动?咱们应用 key 来将新旧 vnode 做一次映射。

首先咱们找到能够复用的 vnode,能够做两次遍历,外层遍历 next vnode,内层遍历 prev vnode

如果 next vnode 和 prev vnode 只是地位挪动,vnodedata 和 children 没有任何变动,调用 patchVnode 之后不会有任何 dom 操作。
接下来只须要把这个 key 雷同的 vnode 挪动到正确的地位即可。咱们的问题变成了怎么挪动。

首先须要晓得两个事件:

· 每一个 prev vnode 都援用了一个实在 dom 节点,每个 next vnode 这个时候都没有实在 dom 节点。

· 调用 patchVnode 的时候会把 prevVnode 援用的实在 Dom 的援用赋值给 nextVnode,就像这样:

还是拿下面的例子,外层遍历 next vnode, 遍历第一个元素的时候,第一个 vnode 是 li©,而后去 prev vnode 里找,在最初一个节点找到了,这里外层是第一个元素,不做任何挪动的操作,咱们记录一下这个 vnode 在 prevVnode 中的索引地位 lastIndex,接下来在遍历的时候,如果 j <lastIndex,阐明本来 prevVnode 在后面的元素,在 nextVnode 中变到了前面来了,那么咱们就把 prevVnode[j]放到 nextVnode[i-1]的前面。

这里多说一句,dom 操作的 api 里,只有 insertBefore(),没有 insertAfter()。也就是说只有把某个 dom 插入到某个元素后面这个办法,没有插入到某个元素前面这个办法,所以咱们只能用 insertBefore()。那么思路就变成了,当 j <lastIndex 的时候,把 prevChildren[j]插入到 nextVnode[i-1]的实在 dom 的前面元素的后面。

当 j >=lastIndex 的时候,阐明这个程序是正确的的,不必挪动,而后把 lastIndex = j;
也就是说,只把 prevVnode 中前面的元素往前挪动,本来程序是正确的就不变。
当初咱们的 diff 的代码变成了这样:

同样的问题,如果新旧 vnode 的元素数量一样,那就曾经能够工作了。接下来要做的就是新增节点和删除节点。

首先是新增节点,整个框架中将 vnode 挂载到实在 dom 上都调用 patch 函数,patch 里调用 createElm 来生成实在 dom。依照下面的实现,如果 nextVnode 中有一个节点是 prevVnode 中没有的,就有问题:

在 prevVnode 中找不到 li(d),那咱们须要调用 createElm 挂在这个新的节点,因为这里的节点须要超入到 li(b)和 li©之间,所以须要用 insertBefore()。在每次遍历 nextVnode 的时候用一个变量 find=false 示意是否可能在 prevVnode 中找到节点,如果找到了就 find=true。如果内层遍历后 find 是 false,那阐明这是一个新的节点。

咱们的 createElm 函数须要判断一下第四个参数,如果没有就是用 appendChild 间接把元素放到父节点的最初,如果有第四个参数,则须要调用 insertBefore 来插入到正确的地位。

接下来要做的是删除 prevVnode 多余节点:

在 nextVnode 中曾经没有 li(d)了,咱们须要在执行完下面所讲的所有流程后在遍历一次 prevVnode,而后拿到 nextVnode 里去找,如果找不到雷同 key 的节点,那就阐明这个节点曾经被删除了,咱们间接用 removeChild 办法删除 Dom。

残缺的代码:https://github.com/TingYinHelen/tempo/blob/main/src/platforms/web/patch.js 在 react-diff 分支(目前有可能代码仓库还没有开源,等我实现更欠缺的时候会开源进去,我的项目构造可能有变动,看 tempo 仓库就行)

这里我的代码实现的 diff 算法很显著看进去工夫复杂度是 O(n2)。那么这里在算法上仍然又能够优化的空间,这里我把 nextChildren 和 prevChildren 都设计成了数组的类型,这里能够把 nextChildren、prevChildren 设计成对象类型,用户传入的 key 作为对象的 key,把 vnode 作为对象的 value,这样就能够只循环 nextChildren,而后通过 prevChildren[key]的形式找到 prevChidren 中可复用的 dom。这样就能够把工夫复杂度降到 O(n)。

以上就是 react 的 diff 算法的实现。

vue 的 diff 算法

先说一下下面代码的问题,举个例子,上面这个状况:

如果依照 react 的办法,整个过程会挪动 2 次:
li©是第一个节点,不须要挪动,lastIndex=2
li(b), j=1, j<lastIndex, 挪动到 li©前面 (第 1 次挪动)
li(a), j=0, j<lastIndex, 挪动到 li(b)前面 (第 2 次挪动)

然而通过肉眼来看,其实只用把 li©挪动到第一个就行,只须要挪动 1 一次。
于是 vue2 这么来设计的:

首先找到四个节点 vnode:prev 的第一个,next 的第一个,prev 的最初一个,next 的最初一个,而后别离把这四个节点作比对:1. 把 prev 的第一个节点和 next 的第一个比对;2. 把 prev 的最初一个和 next 的最初一个比对;3.prev 的第一个和 next 的最初一个;4. next 的第一个和 prev 的最初一个。如果找到雷同 key 的 vnode,就做挪动,挪动后把后面的指针往后挪动,前面的指针往前挪动,直到前后的指针重合,如果 key 不雷同就只 patch 更新 vnodedata 和 children。上面来走一下流程:

  1. li(a)和 li(b),key 不同,只 patch,不挪动
  2. li(d)和 li©,key 不同,只 patch,不挪动
  3. li(a)和 li©,key 不同,只 patch,不挪动
  4. li(d)和 li(d),key 雷同,先 patch,须要挪动挪动,挪动的办法就是把 prev 的 li(d)挪动到 li(a)的后面。而后挪动指针,因为 prev 的最初一个做了挪动,所以把 prev 的指向前面的指针往前挪动一个,因为 next 的第一个 vnode 曾经找到了对应的 dom,所以 next 的后面的指针往后挪动一个。

当初比对的图变成了上面这样:

这个时候的实在 DOM:

持续比对

  1. li(a)和 li(b),key 不同,只 patch,不挪动。
  2. li©和 li©,雷同雷同,先 patch,因为 next 的最初一个元素也刚好是 prev 的最初一个,所以不挪动,prev 和 next 都往前挪动指针。

这个时候实在 DOM:

当初最新的比对图:

持续比对

  1. li(a)和 li(b),key 不同,只 patch,不挪动。
  2. li(b)和 li(a),key 不同,只 patch,不挪动。
  3. li(a) 和 li (a),key 雷同,patch,把 prev 的 li(a)挪动到 next 的前面指针的元素的前面。

实在的 DOM 变成了这样:

比对的图变成这样:

持续比对:
li(b)和 li(b)的 key 雷同,patch,都是前指针雷同所以不挪动,挪动指针
这个时候前指针就在后指针前面了,这个比对就完结了。

这就实现了惯例的比对,还有不惯例的,如下图:

通过 1,2,3,4 次比对后发现,没有雷同的 key 值可能挪动。

这种状况咱们没有方法,只有用老办法,用 newStartIndex 的 key 拿去顺次到 prev 里的 vnode,直到找到雷同 key 值的老的 vnode,先 patch,而后获取实在 dom 挪动到正确的地位(放到 oldStartIndex 后面),而后在 prevChildren 中把挪动过后的 vnode 设置为 undefined,在下次指针挪动到这里的时候间接跳过,并且 next 的 start 指针向右挪动。

function updateChildren (elm, prevChildren, nextChildren) {
  let oldStartIndex = 0;
  let oldEndIndex = prevChildren.length - 1;
  let newStartIndex = 0;
  let newEndIndex = nextChildren.length - 1;

  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {let oldStartVnode = prevChildren[oldStartIndex];
    let oldEndVnode = prevChildren[oldEndIndex];
    let newStartVnode = nextChildren[newStartIndex];
    let newEndVnode = nextChildren[newEndIndex];

    if (oldStartVnode === undefined) {oldStartVnode = prevChildren[++oldStartIndex];
    }
    if (oldEndVnode === undefined) {oldEndVnode = prevChildren[--oldEndIndex];
    }

    if (oldStartVnode.key === newStartVnode.key) {patchVnode(newStartVnode, oldStartVnode);
      oldStartIndex++;
      newStartIndex++;
    } else if (oldEndVnode.key === newEndVnode.key) {patchVnode(newEndVnode, oldEndVnode);
      oldEndIndex--;
      newEndIndex--;
    } else if (oldStartVnode.key === newEndVnode.key) {patchVnode(newEndVnode, oldStartVnode);
      elm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      newEndIndex--;
      oldStartIndex++;
    } else if (oldEndVnode.key === newStartVnode.key) {patchVnode(newStartVnode, oldEndVnode);
      elm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      oldEndIndex--;
      newStartIndex++;
    } else {const idxInOld = prevChildren.findIndex(child => child.key === newStartVnode.key);
      if (idxInOld >= 0) {elm.insertBefore(prevChildren[idxInOld].elm, oldStartVnode.elm);
        prevChildren[idxInOld] = undefined;
        newStartIndex++;
      }
    }
  }
}

接下来就是新增节点:

这种排列办法,依照下面的办法,通过 1,2,3,4 比对后找不到雷同 key,而后而后用 newStartIndex 到老的 vnode 中去找,依然找不着,这个时候阐明是一个新节点,把它插入到 oldStartIndex 后面

最初是删除节点,我把他作为课后作业,同学能够本人实现最初的删除的算法。

残缺代码在 https://github.com/TingYinHelen/ tempo 的 vue 分支。

PS. 本文局部内容参考自《比对一下 react,vue2.x,vue3.x 的 diff 算法》。

本文分享自华为云社区《90 后小姐姐带你理解 Vue3.0 新个性》,原文作者:技术火炬手。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0