前言
Vue3探秘系列文章链接:
不止响应式:Vue3探秘系列— 虚拟结点vnode的页面挂载之旅(一)
不止响应式:Vue3探秘系列— diff算法的完整过程(三)
计算属性:Vue3探秘系列— computed的实现原理(六)
Hello~大家好。我是秋天的一阵风
本篇文章是vue3源码探秘系列的第五篇,前面四篇主要都是围绕着组件初始化、组件渲染、组件更新等方面来探讨。
想必大家也是等不及了希望能马上学习vue3中最最最重要的知识之一:响应式设计。
好了,话不多说,我们进入到今天的正题。
还是老样子,我们在探究vue3响应式之前,我们先回顾一下,在上一篇文章学习的 setup函数
中,我们多次使用一些 API 让数据变成响应式
。除了组件化之外,Vue.js
的另一大核心特性便是其响应式系统。这一特性的核心在于,当数据发生改变时,能够自动调用相应的处理函数。具体到组件层面,这意味着每当数据更新时,组件将会自动重新渲染。 这种响应式机制是Vue.js中组件更新和渲染流程的关键驱动力。
在介绍 Vue.js 3.0 响应式
实现之前,我们先来回顾一下 Vue.js 2.x 响应式
实现的部分: 它在内部通过 Object.defineProperty API
劫持数据的变化,在数据被访问的时候收集依赖,然后在数据被修改的时候通知依赖更新。我们用官网的一张图可以直观地看清这个流程。
在 Vue.js 2.x
中,Watcher
是一个关键概念,它负责监控依赖并触发更新。对于组件渲染而言,存在一种特定类型的 Watcher
,即 Render Watcher
。
在这个过程中,有两个主要阶段:依赖收集与通知更新。首先,在组件渲染时,它会访问模板中的数据,这个访问过程会触发数据的 getter
方法,从而将 Render Watcher
添加到依赖列表中,并建立起数据与组件之间的关联。接着,在数据发生变化时,setter
方法会被触发,进而通知相关的 Render Watcher
进行更新,最终导致组件的重新渲染。
在 Vue 2 中,Object.defineProperty
API 用于实现数据的响应式机制。然而,这种方法有一些局限性和缺点,主要包括以下几点:
-
无法监听到新增属性:
- 如果在初始化之后动态添加新的属性,那么这些新属性不会被转换为响应式的。
-
无法检测到数组索引的变化:
- 当通过数组索引直接修改数组元素时,如
vm.items[index] = newValue
,这种变化不会触发视图更新。
- 当通过数组索引直接修改数组元素时,如
-
不支持数组方法的监听:
- 对数组使用原生 JavaScript 方法(如
push
,pop
,splice
,sort
,reverse
等)时,这些操作不会触发视图更新。
- 对数组使用原生 JavaScript 方法(如
-
深度对象的响应式处理复杂:
- 如果对象的属性本身也是一个对象,那么需要递归地对所有层级的对象进行
Object.defineProperty
的设置,这增加了实现的复杂度。
- 如果对象的属性本身也是一个对象,那么需要递归地对所有层级的对象进行
-
性能问题:
- 对于大型数据结构,深度遍历可能会带来性能上的开销。
-
兼容性问题:
- 在某些旧浏览器中(如 IE9 及更早版本),
Object.defineProperty
不被支持,这可能会影响到 Vue 应用的兼容性。
- 在某些旧浏览器中(如 IE9 及更早版本),
Vue.js 3.0 为了解决 Object.defineProperty
的这些缺陷,使用 Proxy API
重写了响应式部分,并独立维护和发布整个 reactivity
库,下面我们就一起来深入学习 Vue.js 3.0
响应式部分的实现原理。
一、 Reactive API
function reactive (target) {
// 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
if (target && target.__v_isReadonly) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if (!isObject(target)) {
// 目标必须是对象或数组类型
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
return target
}
if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
// target 已经有对应的 Proxy 了
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// 只有在白名单里的数据类型才能变成响应式
if (!canObserve(target)) {
return target
}
// 利用 Proxy 创建响应式
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
// 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
return observed
}
1. 数据校验
可以看到,reactive
内部通过 createReactiveObject
函数把 target
变成了一个响应式对象。
在这个过程中,createReactiveObject
函数主要做了以下几件事情。
-
判断 target 是不是数组或者对象类型,如果不是则直接返回。所以原始数据
target
必须是对象或者数组。 -
判断 对象 是否已经是响应式对象,如果是,返回这个响应式对象,举个例子:
import { reactive } from 'vue'
const original = { foo: 1 }
const observed = reactive(original)
const observed2 = reactive(observed)
const observed3 = reactive(original)
observed === observed2
observed === observed3
可以看到 observed 已经是响应式结果了,如果对它再去执行 reactive
,返回的值 observed
和 observed2
还是同一个对象引用。
因为这里 reactive
函数会通过 target.__v_raw
属性来判断 target 是否已经是一个响应式对象(因为响应式对象的 __v_raw
属性会指向它自身,后面会提到),如果是的话则直接返回响应式对象。
-
除此之外,如果对同一个原始数据多次执行 reactive ,那么会返回相同的响应式对象。所以
observed
和observed2
还是同一个对象引用。 -
通过使用
Proxy
API 来拦截目标对象 (target
) 的操作,我们可以将其转换为响应式对象。返回的Proxy
对象通常被称为响应式对象。根据目标数据的不同类型,对应的Proxy
处理器对象也会有所不同。接下来我们将重点探讨针对基本数据类型的Proxy
处理器对象,其中reactive
函数传递给baseHandlers
的值为mutableHandlers
。
2.Proxy代理
(1) mutableHandlers
接下来,我们继续看 Proxy 处理器对象 mutableHandlers 的实现:
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
}
它其实就是劫持了我们对 observed 对象的一些操作,比如:
-
访问对象属性会触发
get
函数; -
设置对象属性会触发
set
函数; -
删除对象属性会触发
deleteProperty
函数; -
in
操作符会触发has
函数; -
通过
Object.getOwnPropertyNames
访问对象属性名会触发ownKeys
函数。
这里,我们只需要分析重点的get函数和set函数即可。
(2) 依赖收集:get 函数
依赖收集发生在数据访问的过程中。因为我们使用了 Proxy
API 来拦截数据对象的操作,因此当访问响应式对象的属性时,会触发 get
函数的执行。
让我们来看看 get
函数的具体实现:实际上,它是通过调用 createGetter
函数得到的返回值来实现的。为了简化流程分析,这里将忽略 get
函数中的一些分支逻辑,并假设 isReadonly
的值默认为 false
。
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* isReactive */) {
// 代理 observed.__v_isReactive
return !isReadonly
}
else if (key === "__v_isReadonly" /* isReadonly */) {
// 代理 observed.__v_isReadonly
return isReadonly;
}
else if (key === "__v_raw" /* raw */) {
// 代理 observed.__v_raw
return target
}
const targetIsArray = isArray(target)
// arrayInstrumentations 包含对数组一些方法修改的函数
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 求值
const res = Reflect.get(target, key, receiver)
// 内置 Symbol key 不需要依赖收集
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
// 依赖收集
!isReadonly && track(target, "get" /* GET */, key)
return isObject(res)
? isReadonly
?
readonly(res)
// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
: reactive(res)
: res
}
}
get 函数主要做了四件事情:
-
对特殊的 key 做了代理,这就是为什么我们在
createReactiveObject
函数中判断响应式对象是否存在__v_raw
属性,如果存在就返回这个响应式对象本身。 -
通过
Reflect.get
方法求值,如果target
是数组且key
命中了arrayInstrumentations
(也就是通过target.includes、target.indexOf
或者target.lastIndexOf
去访问函数),就会执行arrayInstrumentations
代理的函数,除了调用数组本身的方法求值外,还对数组每个元素做了依赖收集。 因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。
const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function (...args) {
// toRaw 可以把响应式对象转成原始数据
const arr = toRaw(this)
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集
track(arr, "get" /* GET */, i + '')
}
// 先尝试用参数本身,可能是响应式数据
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果失败,再尝试把参数转成原始数据
return arr[key](...args.map(toRaw))
}
else {
return res
}
}
})
-
通过
Reflect.get
求值,然后会执行track
函数收集依赖 -
函数对计算的值 res 进行判断,如果它也是数组或对象,则递归执行
reactive
把res
变成响应式对象。 这么做是因为Proxy
劫持的是对象本身,并不能劫持子对象的变化,这点和Object.defineProperty API
一致。
注意: Object.defineProperty 在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。
(3) track 收集依赖
// 是否应该收集依赖
let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每个 target 对应一个 depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep 集合
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}
这里先提出一个问题:我们要收集的依赖是什么?
我们希望当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数。
-
我们把
target
作为原始的数据,key
作为访问的属性。 -
创建全局的
targetMap
作为原始数据对象的Map
,它的键是target
,值是depsMap
,作为依赖的Map
; -
这个
depsMap
的键是target
的key
,值是dep
集合,dep
集合中存储的是依赖的副作用函数。
为了方便理解,可以通过下图表示它们之间的关系:
所以每次 track ,就是把当前激活的副作用函数 activeEffect
作为依赖,然后收集到 target
相关的 depsMap
对应 key
下的依赖集合 dep
中。
请注意:
目前最新的vue3源码中作者已经将set集合改成了Map对象,具体请查看: github
(4) 派发通知:set 函数
派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set
函数的实现,它是执行createSetter
函数的返回值:
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key]
value = toRaw(value)
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, "add" /* ADD */, key, value)
}
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* SET */, key, value, oldValue)
}
}
return result
}
}
set 函数主要就做两件事情:
-
通过
Reflect.set
求值 -
通过
trigger
函数派发通知 ,并依据key
是否存在于target
上来确定通知类型,即新增还是修改。
(5) trigger 函数派发通知
// 原始数据对象 map
const targetMap = new WeakMap()
function trigger(target, type, key, newValue) {
// 通过 targetMap 拿到 target 对应的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回
return
}
// 创建运行的 effects 集合
const effects = new Set()
// 添加 effects 的函数
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// SET | ADD | DELETE 操作之一,添加对应的 effects
if (key !== void 0) {
add(depsMap.get(key))
}
const run = (effect) => {
// 调度执行
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
// 直接运行
effect()
}
}
// 遍历执行 effects
effects.forEach(run)
}
trigger 函数的实现也很简单,主要做了四件事情:
-
通过
targetMap
拿到target
对应的依赖集合depsMap
; -
创建运行的
effects
集合; -
根据
key
从depsMap
中找到对应的effects
添加到effects 集合
; -
遍历
effects
执行相关的副作用函数。
所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。
在描述依赖收集和派发通知的过程中,我们都提到了一个词:副作用函数,依赖收集过程中我们把 activeEffect
(当前激活副作用函数)作为依赖收集,它又是什么?接下来我们来看一下副作用函数的庐山真面目。
(6) 副作用函数
介绍副作用函数前,我们先回顾一下响应式的原始需求,即我们修改了数据就能自动执行某个函数,举个简单的例子:
import { reactive } from "vue";
const counter = reactive({
num: 0,
});
function logCount() {
console.log(counter.num);
}
function count() {
counter.num++;
}
logCount();
count();
可以看到,这里我们定义了响应式对象 counte
r,然后我们在 logCount
中访问了 counter.num
,我们希望通过执行 count
函数修改 counter.num
值的时候,能自动执行 logCount
函数。
按我们之前对依赖收集过程的分析,如果这个 logCount
就是 activeEffect
的话,那么就可以实现需求,但显然是做不到的,因为代码在执行到 console.log(counter.num)
这一行 的时候,它对自己在 logCount
函数中的运行是一无所知的。
那么该怎么办呢?其实只要我们运行 logCount
函数前,把 logCount
赋值给 activeEffect
就好了,如下
activeEffect = logCount;
logCount();
顺着这个思路,我们可以利用高阶函数的思想,对 logCount 做一层封装,如下:
function wrapper(fn) {
const wrapped = function(...args) {
activeEffect = fn
fn(...args)
}
return wrapped
}
const wrappedLog = wrapper(logCount)
wrappedLog()
这里,wrapper 本身也是一个函数,它接受 fn 作为参数,返回一个新的函数 wrapped
,然后维护一个全局的 activeEffect,当 wrapped
执行的时候,把 activeEffect
设置为 fn
,然后执行 fn
即可。
这样当我们执行 wrappedLog
后,再去修改 counter.num
,就会自动执行 logCount
函数了。
实际上 Vue.js 3.0 就是采用类似的做法,在它内部就有一个 effect 副作用函数
,我们来看一下它的实现:
(7) effect 副作用函数
// 全局 effect 栈
const effectStack = [];
// 当前激活的 effect
let activeEffect;
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
// 如果 fn 已经是一个 effect 函数了,则指向原始函数
fn = fn.raw;
}
// 创建一个 wrapper,它是一个响应式的副作用的函数
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
// lazy 配置,计算属性会用到,非 lazy 则直接执行一次
effect();
}
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect(...args) {
if (!effect.active) {
// 非激活状态,则判断如果非调度执行,则直接执行原始函数。
return options.scheduler ? undefined : fn(...args);
}
if (!effectStack.includes(effect)) {
// 清空 effect 引用的依赖
cleanup(effect);
try {
// 开启全局 shouldTrack,允许依赖收集
enableTracking();
// 压栈
effectStack.push(effect);
activeEffect = effect;
// 执行原始函数
return fn(...args);
} finally {
// 出栈
effectStack.pop();
// 恢复 shouldTrack 开启之前的状态
resetTracking();
// 指向栈最后一个 effect
activeEffect = effectStack[effectStack.length - 1];
}
}
};
effect.id = uid++;
// 标识是一个 effect 函数
effect._isEffect = true;
// effect 自身的状态
effect.active = true;
// 包装的原始函数
effect.raw = fn;
// effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
effect.deps = [];
// effect 的相关配置
effect.options = options;
return effect;
}
结合上述代码来看,effect 内部通过执行 createReactiveEffect
函数去创建一个新的 effect 函数,为了和外部的 effect
函数区分,我们把它称作 reactiveEffect
函数,并且还给它添加了一些额外属性(我在注释中都有标明)。
接着说,这个 reactiveEffect 函数
就是响应式的副作用函数,当执行 trigger
过程派发通知的时候,执行的 effect
就是它。
按我们之前的分析,这个 reactiveEffect 函数只需要做两件事情:
把全局的 activeEffect 指向它
,- 执行被包装的原始函数 fn 即可 。
但实际上它的实现要更复杂一些,首先它会判断 effect 的状态是否是 active,这其实是一种控制手段,允许在非 active 状态且非调度执行情况,则直接执行原始函数 fn 并返回,
接着判断 effectStack
中是否包含 effect
,如果没有就把 effect
压入栈内。之前我们提到,只要设置 activeEffect = effect
即可,那么这里为什么要设计一个栈的结构呢?
其实是考虑到以下这样一个嵌套 effect 的场景:
import { reactive } from "vue";
import { effect } from "@vue/reactivity";
const counter = reactive({
num: 0,
num2: 0,
});
function logCount() {
effect(logCount2);
console.log("num:", counter.num);
}
function count() {
counter.num++;
}
function logCount2() {
console.log("num2:", counter.num2);
}
effect(logCount);
count();
我们每次执行 effect 函数时,如果仅仅把 reactiveEffect
函数赋值给 activeEffect
,那么针对这种嵌套场景,执行完 effect(logCount2)
后,activeEffect
还是 effect(logCount2)
返回的 reactiveEffect
函数,这样后续访问 counter.num
的时候,依赖收集对应的 activeEffect
就不对了,此时我们外部执行 count 函数修改counter.num
后执行的便不是 logCount 函数,而是 logCount2
函数,最终输出的结果如下:
num2: 0;
num: 0;
num2: 0;
而我们期望的结果应该如下:
num2: 0;
num: 0;
num2: 0;
num: 1;
因此针对嵌套 effect 的场景,我们不能简单地赋值 activeEffect
,应该考虑到函数的执行本身就是一种入栈出栈操作,因此我们也可以设计一个 effectStack
,这样每次进入 reactiveEffect
函数就先把它入栈,然后 activeEffect
指向这个 reactiveEffect
函数,接着在 fn 执行完毕后出栈,再把 activeEffect
指向 effectStack
最后一个元素,也就是外层 effect 函数对应的 reactiveEffect
。
这里我们还注意到一个细节,在入栈前会执行 cleanup
函数清空 reactiveEffect
函数对应的依赖 。在执行 track
函数的时候,除了收集当前激活的 effect
作为依赖,还通过 activeEffect.deps.push(dep)
把 dep
作为activeEffect
的依赖,这样在 cleanup
的时候我们就可以找到 effect
对应的 dep
了,然后把 effect
从这些 dep 中删除。cleanup 函数的代码如下所示:
function cleanup(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
deps.length = 0;
}
}
为什么需要 cleanup 呢?如果遇到这种场景:
<template>
<div v-if="state.showMsg">
{{ state.msg }}
</div>
<div v-else>
{{ Math.random()}}
</div>
<button @click="toggle">Toggle Msg</button>
<button @click="switchView">Switch View</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
msg: ‘Hello World’,
showMsg: true
})
function toggle() {
state.msg = state.msg === 'Hello World' ? 'Hello Vue': 'Hello World'
}
function switchView() {
state.showMsg = !state.showMsg
}
return {
toggle,
switchView,
state
}
}
}
</script>
结合代码可以知道,这个组件的视图会根据 showMsg 变量的控制显示 msg 或者一个随机数,当我们点击 Switch View 的按钮时,就会修改这个变量值。
假设没有 cleanup,在第一次渲染模板的时候,activeEffect
是组件的副作用渲染函数,因为模板 render
的时候访问了 state.msg
,所以会执行依赖收集,把副作用渲染函数作为 state.msg 的依赖,我们把它称作 render effect
。然后我们点击 Switch View 按钮,视图切换为显示随机数,此时我们再点击 Toggle Msg 按钮,由于修改了 state.msg
就会派发通知,找到了 render effect
并执行,就又触发了组件的重新渲染。
但这个行为实际上并不符合预期,因为当我们点击 Switch View 按钮,视图切换为显示随机数的时候,也会触发组件的重新渲染,但这个时候视图并没有渲染 state.msg
,所以对它的改动并不应该影响组件的重新渲染。
因此在组件的 render effect
执行之前,如果通过 cleanup
清理依赖,我们就可以删除之前 state.msg
收集的 render effect
依赖。这样当我们修改 state.msg
时,由于已经没有依赖了就不会触发组件的重新渲染,符合预期。
二、readonly API
如果用 const 声明一个对象变量,虽然不能直接对这个变量赋值,但我们可以修改它的属性。如果我们希望创建只读对象,不能修改它的属性,也不能给这个对象添加和删除属性,让它变成一个真正意义上的只读对象。
const original = {
foo: 1,
};
const wrapped = readonly(original);
wrapped.foo = 2;
// warn: Set operation on key "foo" failed: target is readonly.
显然,想实现上述需求就需要劫持对象,于是 Vue.js 3.0 在 reactive API 的基础上,设计并实现了 readonly API。
我们先来看一下 readonly 的实现:
function readonly(target) {
return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if (!isObject(target)) {
// 目标必须是对象或数组类型
if ((process.env.NODE_ENV !== ‘production’)) {
console.warn(value cannot be made reactive: ${String(target)})
}
return target
}
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
return target
}
if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
// target 已经有对应的 Proxy 了
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// 只有在白名单里的数据类型才能变成响应式
if (!canObserve(target)) {
return target
}
// 利用 Proxy 创建响应式
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
// 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
def(target, isReadonly ? “__v_readonly” /* readonly */ : “__v_reactive” /* reactive */\, observed)
return observed
}
其实 readonly 和 reactive 函数的主要区别,就是执行 createReactiveObject
函数时的参数 isReadonly
不同。
在创建过程中如果 isReadonly
变量为 true
, 会给原始对象 target 打上一个 __v_readonly
的标识。
另外还有一个特殊情况,如果 target
已经是一个 reactive
对象,就会把它继续变成一个 readonly
响应式对象。
其次就是 baseHandlers 的 collectionHandlers 的区别,我们这里仍然只关心基本数据类型的 Proxy 处理器对象,readonly 函数传入的 baseHandlers
值是 readonlyHandlers
。
1. readonlyHandlers
const readonlyHandlers = {
get: readonlyGet,
has,
ownKeys,
set(target, key) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true;
},
deleteProperty(target, key) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true;
},
};
readonlyHandlers
和 mutableHandlers
的主要区别体现在 get
、set
和 deleteProperty
这三个函数上。作为只读的响应式对象,不允许修改或删除属性。因此,在非生产环境中,set
和 deleteProperty
函数的实现都会发出警告,提醒用户 target
是只读的。
(1) readonlyGet
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
// ...
// isReadonly 为 true 则不需要依赖收集
!isReadonly && track(target, "get" /* GET */, key);
return isObject(res)
? isReadonly
? // 如果 res 是个对象或者数组类型,则递归执行 readonly 函数把 res readonly
readonly(res)
: reactive(res)
: res;
};
}
可以看到,它和 reactive API 最大的区别就是不做依赖收集了,这一点也非常好理解,因为它的属性不会被修改,所以就不用跟踪它的变化了。
三、ref API
通过前面的分析,我们知道 reactive API
对传入的 target
类型有限制,必须是对象或者数组类型,而对于一些基础类型(比如 String、Number、Boolean)是不支持的。
但是有时候从需求上来说,可能我只希望把一个字符串变成响应式,却不得不封装成一个对象,这样使用上多少有一些不方便,于是 Vue.js 3.0 设计并实现了 ref API。
const msg = ref("Hello World");
msg.value = "Hello Vue";
我们先来看一下 ref 的实现:
function ref(value) {
return createRef(value);
}
const convert = (val) => isObject(val) ? reactive(val) : val
function createRef(rawValue) {
if (isRef(rawValue)) {
// 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
return rawValue
}
// 如果是对象或者数组类型,则转换一个 reactive 对象。
let value = convert(rawValue)
const r = {
__v_isRef: true,
get value() {
// getter
// 依赖收集,key 为固定的 value
track(r, “get” /* GET */, ‘value’)
return value
},
set value(newVal) {
// setter,只处理 value 属性的修改
if (hasChanged(toRaw(newVal), rawValue)) {
// 判断有变化后更新值
rawValue = newVal
value = convert(newVal)
// 派发通知
trigger(r, “set” /* SET */, ‘value’, void 0)
}
}
}
return r
}
可以看到,函数首先处理了嵌套 ref 的情况,如果传入的 rawValue
也是 ref,那么直接返回。
接着对 rawValue
做了一层转换,如果 rawValue
是对象或者数组类型,那么把它转换成一个 reactive 对象
。
最后定义一个对 value 属性做 getter 和 setter 劫持的对象并返回,get 部分就是执行 track 函数做依赖收集然后返回它的值;set 部分就是设置新值并且执行 trigger 函数派发通知。