跳到主要内容

认识

一、认识


Vue2

Vue3

二、特点


2.1 基于 MVVM 思想

Vue.js 的一个核心思想是数据驱动。所谓数据驱动, 是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作DOM,而是通过修改数据; 或者数据更新驱动视图的变化。它相比我们传统的前端开发。如使用jQuery等前端库直接修改DOM,大大简化了代码量。特别是当交互特别复杂的时候, 只关心数据的修改会让代码的逻辑变得非常清晰。我们所有的逻辑都是对数据的修改,而不用触碰DOM

三、Vue vs React


Vue 是一个 编译时 + 运行时 的前端框架, Vue 通过编译 Template 得到虚拟 DOM 渲染函数, 可以在编译的时候进行非常好的性能优化, 比如 动态标记靶向更新静态提升(静态节点、动态节点中的静态属性)、预字符串化内联函数缓存等; Vue 采用响应式依赖追踪机制, 基于 Proxy 来监听数据变化, Vue 内部可以精确的知道数据发生变化的组件, 从而自动更新渲染。Vue 内部将 Template 或者 JSX 编译之后, 生成 VNode 虚拟 DOM, 在整个更新渲染中只会操作 VNode 数据。由于 Vue 只有 VNode, 在进行 Diff 算法时, 可以使用双端对比算法, 高效、快速的完成最小差异比对。 Vue3.0 则是把整个静态节点进行提升处理, Diff 的时候是不过进入循环的。而且, 针对动态节点的, 通过建立块虚拟节点数组, 实现了靶向更新。

React 是纯运行时前端框架, 在运行前, 已经将 JSX 全部转换为 ReactElement 虚拟 DOM, 在运行中没有机会进行编译优化。React 需要通过 this.setState 或者 useState dispatch 手动触发更新, 由开发者决定是否触发更新。React 触发更新工作流为: 从当前 Fiber 节点开始, 冒泡标记更新, 从当前 Fiber 向上遍历,更新每个祖先节点的 childLanes 属性,表明该分支中存在待调和的更新任务。在冒泡更新标记的过程中,最终会找到对应的根节点(通常为 FiberRoot),这个根节点是整个更新流程的入口, 并返回 FiberRoot。也就是说, React 每一次更新都是从 根节点 开始, 为调度提供了统一的入口, 使得更新可以被分割、优先级调度、甚至中断。虽然更新调度从根开始, 但实际的重新渲染和 DOM 更新只发生在受影响的部分, Fiber 算法通过 diff 和优化手段避免了全量重渲染。 另外, React 在运行前, 将 JSX 全部转换为 ReactElement 虚拟 DOM, 随后在 Render BeginWork 递阶段 会生成 Fiber, 具有两种数据结构。由于 React 具有两种结构, 在进行 Diff 对比时, 通过对比 ReactElement 数组Fiber 单链表, 只能从左到右依次比对, 无法实现双端对比。而且, 由于 React 是纯运行时前端框架, 在运行前, 已经将 JSX 全部转换为 ReactElement 虚拟 DOM, 在运行中没有机会进行编译优化, 所以无法进行静态节点分析的,因此 React 在对静态节点处理这一块是要逊色的。

四、Vue3.0 Vs Vue2.0


一、响应式原理:

  • Vue2.0 对象内部通过 defineReactive 方法, 使用 Object.defineProperty 来劫持整个对象,然后进行深度遍历所有属性, 给每个属性添加 gettersetter, 实现响应式,因此在嵌套层级比较深时, 一次性递归执行, 把所有子对象变成响应式, 数组则是通过重写数组方法来实现。但是它只会劫持已经存在的属性, 不能监听对象属性的新增和删除,所以需要使用 $set、$delete 这种语法糖去实现,这其实是一种设计上的不足。当页面使用对应属性时,每个属性都拥有自己的 dep 属性, 存放他所依赖的 watcher(依赖收集), 当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

  • Vue3.0 基于 Proxy 代理实现的 reactive。由于 Proxy 不能监听基础数据类型, 所以基于类的访问器和设置器实现的 ref。可以对整个对象进行监听,所以不需要深度遍历, 在访问到嵌套层级较深的子对象时, 才会将他变成响应式。可以监听动态属性的添加、删除。可以监听到数组的索引和数组 length 属性。

二、模版编译优化: Vue3 对编译器进行了进一步的优化。优化如下:

  1. 动态标记、靶向更新: 动态标记, 在 Vue 3 中,动态标记 主要体现在编译器生成的 Patch Flags 上,它是一种编译时优化技术,用于标记 VNode 中哪些部分是动态的,从而在更新时可以跳过对静态部分的比对,达到提升性能的目的。Patch flags 是编译器在生成渲染函数时附加在 VNode 上的数字标记,它指示了当前节点的哪些属性、子节点或其他内容是动态的,需要在更新时进行比对和更新。通过动态标记, 1. Vuediff 时可以直接根据这些标记判断哪些部分发生了变化,而无需对整个节点进行深度比较,这样大大减少了不必要的计算,提高了更新性能; 2. 在组件更新时, patch 函数利用这些 patch flags 只对动态部分进行比对和更新, 会根据 vnodepatchFlag 上具有的属性来执行不同的 patch 方法, 实现靶向更新, 如果没有patchFlag那么就执行full diff,也就是这里的patchProps。基于 patchFlag 属性, 在创建虚拟节点阶段, 把它的动态节点提取出来, 并将其存入到该虚拟节点的 dyamicChildren 数组内。我们把带有 dyamicChildren 属性的虚拟节点称为BlockBlock 不仅能够收集它的直接动态子节点, 还能够收集所有动态子代节点。有了 Block 之后, 会忽略虚拟节点的 children 数组, 而是直接找到该虚拟节点的 dynamicChildren 数组, 只更新该数组中的动态节点。这样,在更新时就实现了跳过静态内容, 只更新动态内容。同时, 由于动态节点中存在对应的补丁标志, 所以在更新动态节点时, 也能够做到靶向更新

  2. Vue 3 中,静态提升 是一种编译时优化技术, 其目的是在编译阶段将模板中不依赖于响应式状态的静态部分提取出来, 包括静态节点或者静态属性, 提升到渲染函数之外, 并生成常量表达式。由于静态节点只会被创建一次,而不是在每次 render 时都重新生成,从而减少了虚拟节点创建的开销和后续 diff 过程中的比较量。这样在每次组件更新时,这部分静态内容就不需要重新创建,从而减少内存分配和计算开销,提高渲染性能。所有节点遍历、转换完成后, 如果编译选项设置了 hoistStatic 开关, 会进行静态提升。除根节点之外(根节点不可以静态提升), 遍历当前节点的所有子节点。主要逻辑为: 如果遍历到的节点为普通元素或者文本 (只有普通元素和文本可以提升) 或者动态节点的静态属性(包含动态绑定的节点本身不会被提升,该动态节点上可能存在纯静态的属性,静态的属性可以被提升), 根据节点静态类型的枚举值, 判断出是静态节点, 那么将当前子节点的 codeGenNode 属性的 patchFlag 标记为 HOISTED ,即可提升, 并将节点存储到转换上下文 contexthoist 数组中, 在生成渲染函数时, 编译器会将这些 hoisted 常量作为外部变量引用,然后在渲染函数中直接使用它们,而不是重新构造这些节点。

  3. 预字符串化 是基于静态提升的一种优化策略。预字符串化 可以将 静态提升 中提升出来的静态节点序列化为字符串, 并生成一个 Static 类型的 VNode

  4. 缓存内联事件处理函数, 在 Vue 3 中,为了避免在每次渲染时都重新创建内联事件处理函数(从而减少内存分配和 GC 压力),框架采用了两种优化手段: 4.1 编译时优化: 静态提升: 当模板中定义的内联事件处理函数不依赖于动态数据(即是纯静态表达式)时,编译器会将这些函数提取为模块级别的常量。这意味着它们只会创建一次,然后在所有渲染中直接引用,无需重复创建。具体逻辑为: 在模板编译阶段,编译器会对内联事件表达式进行静态分析。如果检测到事件处理函数完全静态(例如 @click="handleClick"handleClick 是一个纯方法引用或内联箭头函数没有依赖动态数据)。 4.2 运行时优化: 事件 Invoker 缓存: 对于动态的内联事件处理函数,Vue 3 在运行时采用了事件 invoker 机制。其核心思路是: 当渲染过程中需要绑定事件时,Vue 并不直接将内联函数绑定到 DOM 元素上,而是创建一个包装函数(通常称为 invoker), 这个 invoker 会存储当前的事件处理函数,并作为唯一的监听器被绑定到 DOM 元素上。在后续更新中,如果事件处理函数没有变化,Vue 会复用同一个 invoker,只需要更新 invoker 内存储的函数引用。这样,在 DOM 上实际绑定的事件处理函数始终是同一个 invoker,避免了重复移除和添加事件监听器,同时使得内联事件处理函数得以 缓存。运行时 patchEvent 逻辑, 在运行时, Vue 3patchEvent 函数负责将事件监听器附加到 DOM 上。其大致逻辑如下: 1. Vue 会在目标元素上检查是否已经存在一个缓存的事件 invoker(比如存储在某个属性中,如 _vei); 2. 如果存在 invoker,则只更新 invoker 内部的 value 属性为最新的事件处理函数; 3. 如果不存在, 则创建一个新的 invoker 函数,并将其绑定到 DOM 元素上,同时把该 invoker 保存下来,供下次更新使用。这种方式保证了即使内联事件处理函数在每次 render 时可能是新创建的对象,但实际绑定到 DOM 上的监听器始终是同一个 invoker,避免了不必要的事件解绑/绑定操作。

三、最小差异 Diff 优化: Vue.js 3.0 采用的是 双端对比 + 快速 Diff 算法。首先进行预处理, 从左往右进行比对,寻找相同的前置节点,再从右往左进行比对,寻找相同的后置节点。相同的前置节点和后置节点,它们的相对位置不变,只需要在它们之间打补丁即可。紧接着挂载新增节点和删除旧节点,最后锁定中间乱序的部分, 这种双端扫描能够快速跳过那些在前后都没有发生变化的部分,从而减少需要进一步比对的节点数。在双端对比之后, 剩下的部分(中间未匹配的节点)需要更精细的对比。遍历剩余新节点, 构建以 newChild.keykey, 以当前遍历位置 ivalue剩余新节点映射表。记录剩余新节点总数(待处理总数)当前处理数是否发生过移动以新节点索引为 index旧节点索引为value的新旧节点索引数组, 初始值为 0进行中间对比的删除逻辑: 遍历剩余老节点, 如果遍历过程中, 当前处理的数量大于等于待处理总数, 说明是多余节点, 当前旧节点标记删除。否则, 根据有无设置 oldChild.key , 有设置, 基于 oldChild.key 从剩余新节点映射表中查找对应新节点索引, 没有设置, 遍历所有剩余新节点。找出对应新节点索引, 如果索引存在, 则复用旧节点,打补丁,并将当前旧节点索引存储到新旧节点索引数组中, 不存在则删除旧节点。随后进行中间对比的移动逻辑: 根据最长递增子序列, 找出新旧节点索引数组中最长稳定序列, 在新旧节点的对比中, 在递增序列里面的旧节点不需要移动, 因此, 递增序列越长, 需要移动的旧节点越少。倒序遍历待处理新节点, 根据新节点索引查找新旧节点索引数组中是否对应旧节点, 如果没有对应旧节点, 新增新节点, 如果有对应旧节点, 并且稳定序列中没有该旧节点, 移动旧节点。

四、Vue3.0 源码引入 Tree Shaking 特性: Tree Shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination。简单来讲,就是在保持代码运行结果不变的前提下,找出使用的代码, 去除无用的代码, 进而减小程序运行时间和程序打包体积。Vue3 源码引入 Tree Shaking 特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中。Tree shaking 是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。Tree shaking 无非就是做了两件事:

  1. 编译阶段利用ES6 Module判断哪些模块已经加载

  2. 判断那些模块和变量未被使用或者引用,进而删除对应代码

五、Vue 3.0 引入 setupComposition API: 组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API: 例如 ref()reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。

  • 生命周期钩子: 例如 onMounted()onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。

  • 依赖注入: 例如 provide()inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。

虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。

五、说说 Vue 3.0 中 TreeShaking 特性?举例说明一下?


Vue3.0 源码引入 Tree Shaking 特性: Tree Shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination。简单来讲,就是在保持代码运行结果不变的前提下,找出使用的代码, 去除无用的代码, 进而减小程序运行时间和程序打包体积。Vue3 源码引入 Tree Shaking 特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中。Tree shaking 是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。Tree shaking 无非就是做了两件事:

  1. 编译阶段利用ES6 Module判断哪些模块已经加载

  2. 判断那些模块和变量未被使用或者引用,进而删除对应代码