这篇文章是一个小记, 因为涉及到的东西很简单, 所以只是做一个总结, 想学新东西的读者可以关掉网页了, 如果你还是一个vue3小白, 那么这篇简短的小记可能会帮助你.

渲染器

我们都知道vue中的sfc单文件模板是会被编译为一个渲染函数的, 这个渲染函数最终返回的就是VDOM, VDOM是虚拟dom的自定义实现, 因为虚拟dom不是一个规范, 而vue3对比以往版本也在VDOM上做了很多优化; 我们从宏观上来看vue3的渲染pipeline可以由下面的步骤表示

预编译/运行时编译出渲染函数 -> 进行渲染创建真实dom -> diff比对高效替换dom

具体的优化主要体现在预编译上, 因为vue支持我们手写渲染函数, 但是官方推荐我们使用模板语法, 最主要的原因就在于其更容易静态分析, 也就更容易做编译时的优化, 就和vue的treeshaking优化一样(es规范是可被静态分析的, 从而实现死代码消除技术), 所以我们在后面的讨论都围绕模板语法来讲.

试想一下, 在vue2中的渲染原理, 都是静态编译我们的模板, 但是有一个致命的缺点: 静态内容也会被重新diff

什么意思呢?

如果我们在模板中写一段代码

<p>hello world</p>

事实上这段代码根本不可能有变更, 没有绑定, 也没有读取任何响应式变量, 所以在vue3中采用了PacthFlag去给这段vdom子节点标注类型, 让其在diff期间主动跳过. 关于PacthFlag它在vue源码中是一个枚举, 定义了很多标识, 它们都有一个共同点就是都是位运算, 在diff运行时其实表现的性能消耗非常小

export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

除了PatchFlag, vue编译在其渲染过程中还提供了其他优化, 比如静态内容提升以及树结构打平,监听函数事件缓存, 下面我来一一介绍以下的渲染优化方案

我们在预编译之前已经知道了静态内容, 我们不仅可以让它跳过diff的判断, 也可以让它在生成vdom树时共享引用, 比如下面的伪代码就很好的阐述了vue2和vue3的渲染优化区别

// vue2
const output = () => {
    const template = {text: "hello world"}
    return {
        children: [{template}]
    }
}
// vue3
const template = {text: "hello world"}
const output = () => {
    return {
        children: [{template}]
    }
}

这样的提升对于大型vue项目来说减少了很多内存占用.

而树结构打平也很容易理解, 和我们之前讲的PatchFlag有所关系, 我们刚知道静态内容都会被跳过, 那么如何跳过的呢? 就是通过打平, 我们在模板中编写的html代码可以称之为一个block, 在一个block中会存在很多很多节点, 那么vue编译器将会把所有后代节点的动态节点全部挑出来并且返回一个没有层级关系的vdom, 这样又大大提高了diff效率以及update效率.

而事件监听也很简单, vue在之前的版本会把下面的代码点击事件视作一个动态内容, 以至于每次更新都需要追踪它的变化

<div>
  <button @click = 'onClick'>点我</button>
</div>

而现在不会了, 在内部提供了一个事件缓存, 解决了这个问题

响应式

这个部分是vue3的重中之重, 在vue2中其阉割版的响应式导致了很多恶心的API, 在vue2中vue都会将data中的属性进行深层次的递归, 以便每个对象都绑定了getter和setter方法; 并且在数组的表现上差强人意, 原因都是因为defineProperty, 这个API我相信已经存在了vue人的脑子里好几年了, 我就不展开讲了, 我们着重讲一下vue3的响应式.

首先vue3的响应式是基于proxy的, 而且这个API是不能被polyfill的, 但是由于我们的浏览器越来越先进, 所以使用先进的API提升开发体验也是值得的, 况且IE已死; proxy为对象提供了很强大的拦截功能, 包括数组的delete, push等等, 而且vue也在响应式核心中做了很多魔法, 比如在for循环时的读取和绑定新值...

vue3和vue2一样, 都提供了类观察者模式的方案, 但是我更愿意称之为发布订阅模式, 因为观察者模式是一对一有强关联, 但是发布订阅通常会有一个调度中心; 而在vue中, 一个又一个的副作用函数都订阅了响应式变量, 只要在代码中使用过响应式变量, vue都会追踪到, 并且将副作用追加到一个WeakMap队列中, 当变量变更就会以此执行所属的所有副作用.

但是我们没有办法知道哪里读取了响应式变量, 这就要靠proxy了, proxy可以拦截属性的读, 那么同理我们使用ref函数创建一个基本类型

const foo = ref(1)

它也会被包装为一个对象, 如果给ref传递了一个对象, 那么其实和reactive这个函数无异, 它们的底层都是一样的.

另外在vue3中提供了很多编译器宏, 使得我们可以便捷的操作响应式变量, 比如我们想要操作ref的变量

const foo = $ref(1)

foo = 2

因为编译器宏会自动帮助我们加入.value, 还有我们对响应式变量解包时, 会让其失去响应式, 我们同样可以使用编译器宏$()来简化toRef的操作

const { x, y } = $(useXXX())

Composition API

一些面试官也特别喜欢问组合API和Option API的区别, 又会有人问React Hook和Composition API的优劣势, 第一个问题很容易解答, 第二个问题需要我们结合各种周边环境回答 (哈哈哈哈哈)

首先是option和composition的区别

composition主要解决了一个代码复用和可维护性的问题, 尤其是代码复用, 我们曾经在一个组件中要想复用逻辑, 通常会选择mixin, 抱歉我真的没有用过mixin, 但是我见过别人用, 觉得很不理解...

首先mixin它可以编写多个, 并且可以同时混入到一个组件中, 说句难听的话, 这就是把你眼睛蒙上把粑粑拉你头上; 你并不知道程序错误是哪个mixin导致的, 而且也没有命名空间, 会导致逻辑冲突;

而composition就像一个函数一样, 正常导入完全不受限制.

当我们的vue文件变得越来越大时, 我们的关注度并不是methods, computed, 而是业务, 我们需要快速的找到业务代码, 如果在vue2中, 你会发现你的业务代码分散到不同地方, 你无法聚焦你的业务; 但是在vue3中, 我们没有options api的限制, 可以自由的将不同业务进行聚合, 并且实现快速定位.

那么和react hook区别是什么呢? 这个vue官方都说了compistion的设计是借鉴了react hook, 但是其内部实现和react大大不同. 原因还是我之前在知乎说的, react的历史包袱太重, 它本来就不是面向普通应用的, 一切设计都要以内部为先, 而且react hook变量是闭包实现的, 并且由于其核心机制导致react组件渲染一次就会跑一边所有的hook, 所以你可能经常会使用useMemouseCallback来优化调用次数;

vue3不存在react的问题, 所有composition api都会仅执行一次, 并且你很容易捕捉到它最新值, 相比而言react你需要关心它的依赖值和副作用, 心智负担较大; 这里并不是说react不好, 因为react的种种缺点都是react大神承认的, 对于js开发者而言, vue确实负担更轻.

但是我们在面试中, 要揣测面试官是不是react吹, 如果是的话, 请尽可能的吹react

常见问题

  • watch和watchEffect的应用场景和区别
我觉得应该不会问这么傻的问题, watch是懒执行, 依赖明确; watchEffect依赖不明确但是是同步执行, 我们需要在初始化时执行逻辑并且变更也执行逻辑, 就可以考虑watchEffect
  • 为什么会用proxy设计响应式
同上, 文章前面已经解答过了
  • treeshaking咋实现的
基于esmodule静态分析, vue2因为是单例, 无法按需加载
  • vue3对比vue2整体的提升有哪些

    全方位的, 我们刚刚讲的响应式和渲染器都是补了vue2的坑, composition也补了option api的坑, 并且源码使用ts构建, 虽然vue3的sfc类型提示很牵强, 但是相比vue2的类型提示要熬掉鱿鱼丝几根头发写类型体操好得多. 而且源码是采用monorepo构建, 其实在阅读源码更方便了, 每个模块都是独立的, 尤其是响应式模块, 我们如果想要在其他平台单独使用响应式特性, 直接npm安装即可. 而且vue的核心模块也独立了, 支持很多平台, 甚至可以使用vue编写终端应用, 对渲染出口不作限制

标签: none

添加新评论