文章有视频版啦,点击直达
前言
Hello,大家好,我是 Sunday。
这是一期专门针对 Vue 源码面试题的文章。
最近很多同学都告诉我,说在他们面试的时候,被问到了很多的 vue3 源码问题,无论是校招还是社招都会有问到。
所以,我专门针对抽离出来了一些 Vue3 的常见源码面试题。无论你是社招,还是校招。相信这些问题,都会对你有巨大的帮助。
那么下面,咱们看看,这些问题都有什么?
01:Vue2 和 Vue3 的区别
vue3 使用 TS 进行了完全的重构,改变的地方还是挺多的,比如:
- 新增的 Composition API(注意:vue3 也支持 Options API)
- 模块化的 API 调用(可以有效的进行 TreeShaking)
- 基于 Fragment 的多个根标签
- 响应式的实现原理
- diff 算法优化
- 生命周期的变化
- 新增的一些组件,比如:teleport、suspense 这些
- .....
那我的话就主要说两个比较核心的变化:
- 响应式实现原理的改变
- diff 算法优化的变化
先说响应式原理: 在 vue2 中响应式核心还是通过 Object.defineProperty 进行实现的。通过 data 方法返回的对象作为 target。这样无论是 简单数据类型 还是 复杂数据类型 ,都可以直接通过
Object.defineProperty 监听 getter 和 setter 行为。 但是,由于 Object.defineProperty 只能监听 指定对象、指定属性 的响应性,所以 vue 需要对 data 中返回的复杂数据类型进行循环监听。 那么这样,当我们为响应式数据 动态新增属性 (为对象新增一个之前不存在的属性,文档)时,会出现失去响应性的问题。 那么为了解决这个问题,vue2 增加了 Vue.set 的 API ,相当于主动触发了一次 Object.defineProperty。 但是,这种方式其实并不方便,需要用户主动触发。 所以,vue3 中改用了 Proxy(也是因为浏览器逐渐升级,不再需要过分兼容旧的浏览器)。利用 Proxy 代理的特性解决了这个问题。
然后是 diff 算法的优化: Vue2 中的 diff 大家都喜欢把它叫做 双端 Diff 对比 。大致的思路是通过:新旧两组节点的四个端点(新节点组的开头、新节点组的结尾、旧节点组的开头、旧节点组的结尾) 进行对比,并试图找到可以复用的节点。 而 Vue3 中的 diff 大家都喜欢叫它做 快速 Diff (注意:快速 diff 并不是官网声明的名字,只是国内都这么叫)。里面涉及到了 最长递增子序列 的概念,整体还是有点复杂的(如果面试官细问,看这里)。
总体来说,Vue3 带来的变化很大。通过 Composition API,特别是 3.2 之后新增了
02:Vue3 中的响应式实现原理
vue3 的响应式实现主要有两个部分:reactive、ref。
reactive 主要是通过 proxy 进行的响应式实现,核心是监听复杂数据类型的 getter 和 setter 行为。当监听到 getter 行为的时候那么就收集当前的依赖行为,也就是 effect 。 当触发 setter 行为的时候,那么就触发刚才收集的依赖。那么此时,所有获取到当前数据的地方都会更新执行,也就是完成了响应性。 但是 proxy 只能监听复杂数据类型,没有办法监听简单数据类型。所以 vue 专门提供了 ref 方法。 ref 方法既可以处理简单数据类型、也可以处理复杂数据类型。 它的实现在 3.2 之前和 3.2 之后是不同的。 3,2 之前主要通过
Object.defineProperty 进行实现,在 3.2 版本的时候,根据社区贡献改为了 get value 和 set value 标记的方式进行实现。这也是为什么 ref 类型的数据必须要通过 .value 的方式使用的原因(本质上是触发 value 方法)。 当 ref 接收复杂数据类型的时候,会直接通过 toReactive 方法,把复杂数据类型交给 reactive 进行处理。
整个的 vue3 响应性,主要就是由这两大块来进行实现的。 proxy 处理复杂数据类型,get value 和 set value 处理简单数据类型。核心都是监听 setter 和 getter ,然后触发 effect 的方式
03:computed 实现原理
computed 和 ref 的实现是有一些类似的,比如:
- 它们本质上都是一个类(ComputedRefImpl)
- 都是通过 get value 和 set value 监听 getter 和 setter 行为的
但是因为 computed 的计算属性特性(依赖的响应式数据发生变化时,才会重新计算),所以在源码的实现上有一些区别,这个区别主要体现在两个地方:
- 调度器:scheduler
- 执行检查(脏状态):_dirty
首先是调度器 scheduler 。 它是作为 ReactiveEffect 的第二个参数存在的回调函数。当触发依赖的时候,会直接执行这个回调函数。 在这个回调函数中,会根据当前的脏值状态来决定是否需要触发依赖。 然后是 _dirty。 它其实就是一个 boolean 的变量。
- true:表示需要获取 计算之后 的最新数据
- false:表示当前数据就是最新的,不需要重新计算
在每次去触发 get value (computed.value)的时候,都会根据这个 _dirty 的值来判断计算的触发。
总的来说,计算属性的核心还是体现在 是否需要重新计算 这里。判断的方式就是通过 _dirty 进行的。而 scheduler 主要提供了函数的作用,在函数内部还是需要依赖 _dirty 来决定触发依赖的时机。
04:watch 实现原理
watch 是一个典型的懒执行 API,它的逻辑更加纯粹:在监听的响应式数据变化时,重新执行回调函数就可以了。 核心的点有两个:
- 如何监听依赖 || 触发依赖的
- 如何进行懒执行的
首先是 watch 监听依赖 || 触发依赖的机制 watch 的监听和触发也是依赖的 setter 和 getter 行为。 这里的 setter 行为触发是比较明确的,本质上就是监听的响应式数据触发 setter 行为。 而 getter 行为的触发是依赖于内部的 traverse 方法进行的。traverse 方法就是 依次遍历数据,分别触发 getter 行为。 至于懒执行本质上就是通过 options 中的 immediate 参数,逻辑比较简单。 因为 watch 内部通过 job 的方法来触发 callback(回调函数),如果 immediate 为 true 那么就主动触发一次 job 就可以了。
总的来说,watch 的实现会更加纯粹一些。
05:Vue 的渲染机制是什么?
Vue 的渲染主要是在 运行时(runtime) 发生的。所以 Vue 的渲染机制和 runtime 的设计原则是分不开的。 Vue 的渲染分为:
- 挂载
- 更新
- 删除
三种。 同时因为 Vue 不光支持浏览器渲染(CSR)还支持服务端渲染(SSR),所以 Vue 在设计 runtime 的时候,就把 runtime 分成了两个包(还有其他的,核心是这两个):
- 核心:runtime-core
- 浏览器渲染:runtime-dom
其中渲染所需要的浏览器 API ,都被放到了 runtime-dom 包中。 渲染的核心逻辑被放到了 runtime-core 中。
当 Vue 要渲染一个 DOM 的时候。 首先会从 render 函数开始。render 函数会接收一个 VNode 。利用这个 VNode 执行 patch 函数开始渲染逻辑。 在 patch 中,根据 VNode 的 type(节点类型)进行区分。这里的节点类型就比较多了,比如:DOM、文本、注释、组件 等等 不同的类型,交给不同的方法进行处理。在方法内部再去判断当前是 挂载 还是 更新 操作。 挂载和更新的细节就比较复杂了,中间涉及到 新增、删除、移动 节点的操作,还有 属性的挂载 逻辑(注意:如果这一块不清楚的话,可以说:细节研究的有点浅)。
整个 Vue 的渲染逻辑其实是非常复杂的逻辑,整个 runtime 都在处理渲染的问题。(可以夸一下面试官:这个问题的难度还是比较大的 😁)
06:h 函数是什么?与 VNode 的关系是什么?
h 函数 是用来快速生成 VNode 的函数。
因为 VNode 是一个对象,通过这个对象的不同属性来代表真实 DOM 的不同特性。 但是一个完整的 VNode ,它的属性是非常多的。如果通过人工去构建这个 VNode 就非常的麻烦了。 所以说 Vue 专门提供了一个 h 函数用来生成 VNode 。 h 函数接受三个参数,并且 h 函数是可以嵌套的。
而在源码内部,h 函数本质上是触发了 createVNode 函数。 这个名字也比较明确,就是用来创建 VNode 的
07:h 函数支持多种调用方式,这是怎么做到的?
一个函数多种调用方式的这种形式,在强类型语言中一般被叫做 重载 。 但是在 JS 中没有重载的概念。
所以说,在这里 vue 其实是 根据对参数的判断,来决定如何调用 createVNode 函数,生成 VNode。 比如:
-
如果用户在调用 h 函数的时候,只传递了两个参数。那么第一个参数是固定的 type。第二个参数有可能是 props,也有可能是 children。 那么 vue 就可以做如下判断:
- 如果 第二个参数是对象,但不是数组。但是他还是一个 VNode,就认为第二个参数是一个单独的 children
- 如果 第二个参数是对象,但不是数组,同时也不是一个 VNode。那就认为他是一个 props
按照这样的逻辑,完成 "重载" 的功能。
简单来说,就是 Vue 在 h 函数内部对参数的类型进行了判断,从而根据不同的类型,来判断参数的不同作用。
07:在日常开发中使用过 render 函数 或 h 函数吗?使用场景是什么?
有使用过。我在构建一个组件库组件的时候使用过它。
之前的时候,我做过一个 confirm 组件 的时候。因为 confirm 组件是需要通过方法来触发组件展示的。 所以说,需要动态的把组件插入到 DOM 中。 那么这个时候,就用到了 render 函数。利用 render 函数进行的渲染。 在制作 Breadcrumb 面包屑 组件的时候,因为 breadcrumb 和 breadcrumb-item 是分离的,所以需要在 breadcrumb 中通过 defineSlots 获取所有的插槽,然后通过 h 函数配合 render 函数进行渲染。
这是两个比较典型的场景。
08:diff 算法的实现逻辑
diff 算法本质上是用来 实现两组DOM对比更新的一个算法。
它在 vue3 中大体实现逻辑分为 5 步: 第一步:sync from start 自前向后的对比 第二步:sync from end:自后向前的对比 第三步:common sequence + mount:新节点多于旧节点,需要挂载 第四步:common sequence + unmount:旧节点多于新节点,需要卸载 第五步:乱序对比
详情可查看 此博客
大体的步骤是这样
09:编译器的设计原则是什么
编译器是一个非常复杂的概念,在很多语言中均有涉及。 Vue 中的编译器是一个 特定领域 下的编译器,一般被叫做 (DSL)
它的编译流程大致分为三步:
- 通过
parse
方法进行解析,得到AST
- 通过
transform
方法对AST
进行转化,得到JavaScript AST
- 通过
generate
方法根据AST
生成render
函数
也就是说:Vue 中的编译器,本质上是把 template 模板转化为 render 函数,交给 runtime 运行的一段代码。
10:什么是 AST,它是如何构建的?
AST 被叫做抽象语法树。 抽象语法树(AST) 是一个用来描述模板的 JS
对象
它内部包含了非常多的属性,比如:type、children、loc(location)等等。 而想要构建抽象语法树,中间涉及到的流程也挺复杂的。
- 首先需要解析 template。解析的时候需要对标签进行处理。这里的处理是通过一个 有限自动状态机 的概念解析 tokens。
- 解析了 tokens 之后,再根据 NodeTypes 类型,转化成对象,这个对象就是 AST
PS:这里如果面试官追问详细,不清楚的同学可以直接说 再深就不是特别了解了。这一块当时看的时候,难度确实挺大的
大体就是,解析 template,利用有限自动状态机解析 tokens,最终生成 AST 对象。
11:你是如何学习源码的?
我学习源码的方式是通过 案例驱动的。
比如,我想要学习 reactive 的实现逻辑。 那么首先:获取 vue 的源代码。 然后:在源代码中构建出一个最简单的 reactive 使用案例 最后:debugger 源代码,跟踪 reactive 的实现流程。 在跟踪实现流程的时候,我会过滤掉边缘逻辑,只关注核心的内容实现。
依据这种方式,根据一个个不同的 单独API 调用案例,来学习源码的知识
总结
文章首发自公众号:【程序员Sunday】
欢迎关注~~