摘要: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。上面来走一下流程:
- li(a)和li(b),key不同,只patch,不挪动
- li(d)和li©,key不同,只patch,不挪动
- li(a)和li©,key不同,只patch,不挪动
- li(d)和li(d),key雷同,先patch,须要挪动挪动,挪动的办法就是把prev的li(d)挪动到li(a)的后面。而后挪动指针,因为prev的最初一个做了挪动,所以把prev的指向前面的指针往前挪动一个,因为next的第一个vnode曾经找到了对应的dom,所以next的后面的指针往后挪动一个。
当初比对的图变成了上面这样:
这个时候的实在DOM:
持续比对
- li(a)和li(b),key不同,只patch,不挪动。
- li©和li©,雷同雷同,先patch,因为next的最初一个元素也刚好是prev的最初一个,所以不挪动,prev和next都往前挪动指针。
这个时候实在DOM:
当初最新的比对图:
持续比对
- li(a)和li(b),key不同,只patch,不挪动。
- li(b)和li(a),key不同,只patch,不挪动。
- 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新个性》,原文作者:技术火炬手。
点击关注,第一工夫理解华为云陈腐技术~