vue3优化与原理

207 阅读9分钟

语法 API 优化:Composition API

优化逻辑组织

Options API 的设计是按照methods、computed、data、props 这些不同的选项分类,当组件小的时候,这种分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用 Options API 的时候,每一个关注点都有自己的 Options,如果需要修改一个逻辑点关注点,就需要在单个文件中不断上下切换和寻找.

Composition API,它有一个很好的机制去解决这样的问题,就是将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。

优化逻辑复用

当我们开发项目变得复杂的时候,免不了需要抽象出一些复用的逻辑。在 Vue.js 2.x中,我们通常会用 mixins 去复用逻辑,但是当我们一个组件混入大量不同的 mixins 的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。

vue3的 hook 函数可以解决与data、props等命名冲突,数据来源清晰。

类型支持

在composition-api中仅利⽤纯变量和函数,规避了对this的使⽤,⾃然的拥有良好的TypeScript类型推断能⼒。

响应式性能优化

响应式变量和组件上下文中共享某个变量

  1. vue2中创建响应式变量需在data中声明,如果我们仅仅想在组件上下文中共享某个变量,而不必去监测它的这个数据变化,这时就特别适合在 created、mounted 钩子函数中去定义这个变量,因为创建响应式的过程是有性能代价的,这相当于一种 Vue.js 应用的性能优化小技巧。

  2. vue3中创建响应式变量利用reactive API,它可以把一个对象数据变成响应式,如果不想让数据变成响应式,就定义成它的原始数据类型即可。

    可以看出来 Composition API 更推荐用户主动定义响应式对象,而非内部的黑盒处理。

数据劫持优化

vue2:

  1. 使用object.defineProperty在初始化需要递归遍历对象所有的key,速度慢;
  2. 数组响应式需要额外实现;
  3. 新增或删除属性⽆法监听,需要使⽤特殊api(set,set,delete)

vue3:

  1. 使用proxy做数据劫持,访问的时候在依赖收集,深层级也是访问到在递归进行依赖收集(懒收集);
  2. 数组不需要额外实现;
  3. proxy监听的是整个对象,所以可以检测到对对象的任何改变

编译器优化

静态节点提升

静态节点(不变的节点)提取出来缓存起来,下次渲染不需要判断静态节点,用内存换时间

补丁标记和动态属性记录

动态属性记录下来,由二进制生成补丁标记,下次更新直接更新属性,提升更新速度

缓存事件处理

vue3认为事件的处理一般是不会变化的,所以其对事件处理函数进行缓存

块 block

打开一个父节点,将动态节点放到动态子节点数组,每次只更新动态节点

虚拟dom和patch算法优化

新的vnode结构

添加了标记属性:

dynamicChildren,记录动态子节点比对时有效减少遍历操作;

dynamicProps,记录动态属性比对时有效减少遍历操作;

patchFlag,标注动态内容类型,使得patch过程更快;

shapeFlag,标记组件形态;

ptach 算法优化

diff算法中使用了最长递增子序列,以一种开销成本最小的方式方式完成 DOM 更新

数据响应式原理

vue2:

一个组件对应一个watcher,在初始化阶段将data中所有的对象创建一个Observer实例,用来判断是对象还是数组,同时创建一个大管家DepObserver实例相对应,

  1. 如果是对象执行walk()遍历所有属性执行defineReactive进行属性劫持,

    getter: 进行依赖收集,depwatcher相互映射

    setter: dep通知更新

  2. 如果是数组遍历覆盖数组的push,pop,shift,unshift,splice,sort,reverse共7个方法,先执行原始方法再执行扩展逻辑,对新插入的元素做响应式操作,添加dep通知更新

vue3:

访问数据时使用proxy劫持整个对象,

getter: 判断数组还是对象,如果是数组遍历每个元素调用track()做依赖收集,如果是对象直接调用track()做依赖收集,如果是嵌套对象则递归执行。

依赖收集的过程,首先创建WeakMap结构是{target:{key:[cb1,cb2...]}},我们把 target 作为原始的数据,key 作为访问的属性。我们创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap的键是 targetkey,值是 dep 集合,dep 集合中存储的是依赖的副作用函数

setter: 触发tigger()派发通知,执行所有的effect函数(调度执行或直接运行)

patch算法实现

vue2:

  1. 属性更新

  2. 文本更新

  3. 子节点更新

    • new vnode 有孩子 old vnode没孩子 新增节点

    • old vnode 有孩子 new vnode没孩子 删除节点

    • 都有孩子diff算法

      • 四个游标:老开,老结束,新开,新结束,首位两两比较

        (1)老开新开一样,patchVnode打补丁,老开新开游标后移

        (2)老结束新结束一样,patchVnode打补丁,老结束新结束游标前移

        (3)老开新结束一样,patchVnode打补丁,移动老开节点到对尾,老开后移,新结束前移

        (4)老结束新开一样,patchVnode打补丁,移动老结束到队首,老结束前移,新开后移

      • 遍历old vnode 找新开

        (1)找到,patchVnode打补丁,找到的节点移动到对首

        (2)没找到,在队首创建新节点createElm

      • 循环结束,收尾工作 (1) 老节点遍历完新节点还没有,将剩下的新节点插入到老节点队尾,真实的dom

        (2) 新节点遍历完,老节点还没有,将剩下的老节点从dom中删除

vue3:

  1. 更新动态子节点数组dynamicChildren

  2. 更新组件updateComponent

    更新组件的属性,插槽,文本如果在更新过程中遇到子组件,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法

  3. 更新普通元素patchElement

    • 更新props

    • 更新子节点 diff 算法

      (1) 掐头,老开新开比对,一样patch,游标向后移;不一样,结束同步,开始去尾

      (2) 去尾,老结束新结束比对,一样patch,游标向前移;不一样,结束同步

      (3) 如果新子节点有剩余,要添加的新节点

      (4) 旧子节点有剩余,要删除的多余节点

      (5) 未知子序列,求最长递增子序列对剩余节点进行添加、删除和移动操作

侦听器的原理

vue3的watcher用来侦听一个响应式对象,多个响应式对象,一个 getter 函数,其作用是当侦听的对象或者函数发生了变化则自动执行某个回调函数。

原理:

  1. 首先将我们传递的侦听对象/数组/函数,生成标准化的 getter 函数,它会返回一个响应式对象;deep:true 的配置内部就是遍历对象的每个属性;
  2. 回调函数的旧值初始值是空数组或者 undefined,执行完回调函数 cb 后,把旧值 oldValue 再更新为 newValue,这是为了下一次的比对。创建 effect 副作用函数,首次执行做依赖收集,在数据发生变化后,通过一定的调度方式(同步方式,异步方式:组件更新前,组件更新后)执行该回调函数。

依赖注入的原理

以provide和inject为例:

provide:在创建组件实例的时候,组件实例的 provides 对象指向父组件实例的 provides 对象,当组件实例需要提供自己的值的时候,它使用父级提供的对象创建自己的 provides 的对象原型。 inject: 我们可以非常容易通过原型链查找来自直接父级提供的数据。

依赖注入和模块化共享数据的差异

  • 作用域不同

对于依赖注入,它的作用域是局部范围,所以你只能把数据注入以这个节点为根的后代组件中,不是这棵子树上的组件是不能访问到该数据的;而对于模块化的方式,它的作用域是全局范围的,你可以在任何地方引用它导出的数据。

  • 数据来源不同

对于依赖注入,后代组件是不需要知道注入的数据来自哪里,只管注入并使用即可;而对于模块化的方式提供的数据,用户必须明确知道这个数据是在哪个模块定义的,从而引入它。

  • 上下文不同

对于依赖注入,提供数据的组件的上下文就是组件实例,而且同一个组件定义是可以有多个组件实例的,我们可以根据不同的组件上下文提供不同的数据给后代组件;而对于模块化提供的数据,它是没有任何上下文的,仅仅是这个模块定义的数据,如果想要根据不同的情况提供不同数据,那么从 API 层面设计就需要做更改。

依赖注入的缺陷和应用场景

依赖注入的特点 :祖先组件不需要知道哪些后代组件使用它提供的数据,后代组件也不需要知道注入的数据来自哪里。

缺陷: 因为依赖注入是上下文相关的,所以它会将你应用程序中的组件与它们当前的组织方式耦合起来,这使得重构变得困难。

应用场景:不推荐在普通应用程序代码中使用,推荐在组件库使用。 依赖注入的方式,即使结构变了,也可以访问到想访问的上层结构,取代$parents强耦合的方式。

props实现原理

  1. 对 props 求值,然后把求得的值赋值给 props 对象,
  2. 校验props合法性
  3. 把 props 变成响应式,添加到组件实例 instance.props 上,父组件传递的子组件就可以获取了
  4. props数据变了,更新组件渲染,更新instance.props的值

插槽的实现原理

  1. 渲染父组件,执行initSlots初始化插槽,将插槽对象数据保留到instance.slots,等待子组件渲染,
  2. 子组件初始化获得slots对象,子组件的插槽部分的 DOM 通过 renderSlot 方法渲染生成

插槽的实现实际上就是一种延时渲染,把父组件中编写的插槽内容保存到一个对象上,并且把具体渲染 DOM 的代码用函数的方式封装,然后在子组件渲染的时候,根据插槽名在对象中找到对应的函数,然后执行这些函数做真正的渲染

指令的实现原理

  1. 指令本质上就是一个 JavaScript 对象,对象上挂着一些钩子函数,在合适的钩子函数中编写一些相关的处理逻辑;
  2. 指令的注册分为全局注册和局部注册,全局注册把指令对象注册到 app 对象创建的全局上下文 context.directives 中,局部注册注册到组件对象上;
  3. render函数生成withDirectives来处理指令,给 vnode 添加了一个 dirs 属性,遍历所有指令,拿到指令对应值构造一个 binding 对象,绑定到组件实例上,然后在各个生命周期前会先会执行指令。

v-model双向绑定实现原理

双向绑定,除了数据变化,会引起 DOM 的变化之外,还应该在操作 DOM 改变后,反过来影响数据的变化。

在普通原生表单上使用v-model:

  1. 首先实现了两个钩子函数,created 和 beforeUpdate;
  2. created 函数首先把 v-model 绑定的值 value 赋值给 el.value,这个就是数据到 DOM 的单向流动
  3. addEventListener 来监听 input /change 标签的事件,当用户手动输入一些数据触发事件的时候,会执行函数,并通过 el.value 获取 input 标签新的值,然后调用 el._assign 方法更新数据,这就是 DOM 到数据的流动

自定义组件使用v-model:

  1. 作用在组件上的 v-model 实际上就是一种打通数据双向通讯的语法糖和v-model指令没有关系,即外部可以往组件上传递数据,组件内部经过某些操作行为修改了数据,然后把更改后的数据再回传到外部;
  2. 往组件传入了一个名为 modelValue 的 prop,它的值是往组件传入的数据 data;
  3. 另外它还在组件上监听了一个名为 update:modelValue 的自定义事件,事件的回调函数接受一个参数,执行的时候会把参数 $event 赋值给数据 data

总结:作用在组件上的v-model就是通过 prop 向组件传递数据,并监听自定义事件接受组件反传的数据并更新。