前言
本小节我们开启响应式原理的篇章。
在 Composition API
中我们通过 reactive API
或者 ref API
来创建响应式数据,响应式的实现不再通过 Object.defineProperty
,而是基于 Proxy
;创建响应式数据时也会和 vue2
一样进行依赖收集,但不再通过 Dep
中间值,而是直接收集 activeEffect
;
接下来我将逐一介绍 响应式、依赖收集的实现,并附带实现响应式相关的API,例如 reactive
,ref
,effect
,effect
内部的 runner
、scheduler
和 stop
,以及readonly
,isReadonly
,isReactive
,shallowReadonly
,isProxy
,isRef
,unRef
,toRef
,toRefs
,proxyRefs
,最后还会实现 computed
和 watch
相关的 API。
reactive 相关 API
reactive
对于 reactive API
而言,核心是用来定义集合类型的响应式数据,比如:普通对象、数组 和 Map
、Set
。
它的实现有以下几个特点:
- 本质是使用
Proxy
对数据进行代理; - 数据类型必须是
object
类型 - 经过代理的数据不再重复代理
- 不重复代理同一对象
// reactivity/src/reactive.ts
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./baseHandlers";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
const reactiveMap = new WeakMap(); // 使用 WeakMap 防止内存泄漏
export function reactive(target) {
// 1. reactive 只代理对象
if (!isObject(target)) {
return target;
}
// 2. 如果已经被代理过,则直接返回
// 取 target[ReactiveFlags.IS_REACTIVE] 时,如果target已经被代理过,则会走到get函数,返回true
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
// 3. 不重复代理同一对象
const existProxy = reactiveMap.get(target);
if (existProxy) {
return existProxy;
}
// 4. 创建代理
const proxy = new Proxy(target, mutableHandlers);
reactiveMap.set(target, proxy); // target -> proxy 的映射表
return proxy;
}
下面我们来实现 mutableHandlers
,我们要实现的功能主要有:
- 自定义
getter
和setter
- 在
getter
中进行依赖收集 - 在
setter
中触发更新
// reactivity/src/baseHandlers.ts
import { ReactiveFlags } from "./reactive";
export const mutableHandlers = {
// receiver 是当前的代理对象
get(target, key, receiver) {
// 通过下列判断,解决不重复代理已经经过代理的对象
if (ReactiveFlags.IS_REACTIVE === key) {
return true;
}
track(target, key);
// 使用 Reflect.get 处理了 target 内部的 this 指向问题
let r = Reflect.get(target, key, receiver);
// 取值的时候,如果属性依然是对象,才对该属性递归使用reactive,相较于vue2性能更好
if (isObject(r)) {
return reactive(r);
}
return r;
},
set(target, key, value, receiver) {
let oldValue = target[key];
const r = Reflect.set(target, key, value, receiver); // Reflect.set 返回一个boolean值
if (oldValue !== value) {
trigger(target, key, value, oldValue);
}
return r;
},
};
为什么在
getter
中不能直接通过target[key]
取值,而要使用Reflect.get()
? 现有一个对象person
:
let person = { name: 'jw', get aliasName() { return 'alias' + this.name } }
当通过
person.aliasName
取值时,aliasName
内部的this.name
(this
指向person
)是通过person.name
读取的,name
改变不会触发aliasName
的响应式; 而改成Reflect.get(target, key, receiver)
后,this
指向receiver
(即personProxy
),this.name
即personProxy.name
,name
改变会触发aliasName
的响应式;
proxy
相较于Object.defineproperty
的优势在哪里?
- 可以原生处理数组,而
vue2
中数组的响应式是通过重写数组原型方法实现的;- 在创建
Proxy
数据时,没有对所有属性递归使用Proxy
,只是对第一层的属性进行了代理,只有当读取到深层属性时才递归使用reactive
,相较于Object.defineproperty
性能更好
收集依赖 track(target, key)
和触发更新 trigger(target, key, value, oldValue)
的具体实现将在后文再详细解析。
isReactive
isReactive
用于检查对象是否是由 reactive
创建的响应式代理。判断一个数据是否是 reactive
,只需要知道其是否触发了Proxy
中的get
方法。那么我们只需要在 isReactive
函数中访问target
中的特定属性,然后修改proxy
的get
函数即可:
// reactive.ts
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
}
export function isReactive(value) {
return !!value[ReactiveFlags.IS_REACTIVE];
}
// baseHandlers.ts
function createGetter(isReadonly=false) {
return function get(target,key) {
// 当key为__v_isReactive时,返回!isReadonly
if(key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}
let res = Reflect.get(target,key)
// 依赖收集、深层递归...
}
}
isReadOnly
isReadOnly
用于检查对象是否是由 readonly
创建的只读代理。isReadonly
的源码逻辑和isReactive
源码逻辑一致,通过触发get
方法来实现isReadonly
的判断。
// reactive.ts
export const enum ReactiveFlags {
IS_READONLY = '__v_isReadonly'
}
export function isReadonly(value) {
return !!value[ReactiveFlags.IS_READONLY];
}
// baseHandlers.ts
function createGetter(isReadonly=false) {
return function get(target,key) {
if(key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}else if(key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
let res = Reflect.get(target,key)
// 依赖收集、深层递归...
}
}
isProxy
满足 isReactive
或 isReadonly
中的任何一个,即满足 isProxy
。
export function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
readOnly
readonly
方法的特点:
- 会创建
Proxy
对象 - 无法修改数据,强行修改会报错
- 不会进行依赖收集和触发更新
- 在读取深层属性时,如果该属性是
object
类型,会递归使用readOnly
进行处理
export function readonly(raw) {
return new Proxy(raw, {
get(key, value) {
let res = Reflect.get(target,key)
if (isObject(res)) {
return readonly(res);
}
return res
}
set(target,key,value) {
console.warn(`key:${key} set 失败,因为 target是readonly`,target)
return true
}
})
}
readOnly
为true
时不会进行依赖收集
shallowReadonly
shallowReadonly
的含义:创建一个 proxy
,使其自身的 property
(属性)为只读,但不执行嵌套对象的深度只读转换(直接暴露原始值,不递归处理)。
const shallowReadonlyGet = createGetter(true,true)
function createGetter(isReadonly=false,shallow = false) {
return function get(target,key) {
if(key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}else if(key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
let res = Reflect.get(target,key)
// 如果是shallowReadonly,直接返回值,不进行递归处理
if(shallow) {
return res
}
if(isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
//如果不是readonly,才进行依赖收集
if(!isReadonly) {
track(target,key)
}
return res
}
}
shallow
为true
时,不会递归处理深层属性
effect 相关 API 及依赖收集、触发更新
我们知道,在 vue2
中依赖收集和触发更新是基于 watcher
实现的,一共有三类 watcher
:渲染watcher
、computed watcher
、以及用户自定义watcher
,并且借助中间量 Dep
实现双向收集的目的,可读性较差。在 vue3
中重写了这套逻辑,使用的是 effect
。
effect & cleanupEffect
ReactiveEffect
是effect
实例的构造函数。effect
是一个函数,在函数中会创建一个ReactiveEffect
实例(接收一个回调函数),并执行实例的run
方法;activeEffect
是一个全局变量,用来记录当前活跃的effect
,用于后文的依赖收集;cleanupEffect
用来清空_effect
实例中所有的deps
,以及将该_effect
实例从deps
中的dep
集合中删除;
// reactivity/src/effect.ts
function cleanupEffect(effect) {
// 每次执行effect之前,都应该将该effect从deps所有属性的dep中清理出去,以及清空effect的deps数组
let { deps } = effect;
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
effect.deps.length = 0;
}
export let activeEffect;
export class ReactiveEffect {
public fn;
public active = true;
public deps = [];
public parent = undefined;
constructor(fn) {
this.fn = fn;
}
run() {
// 通过设置parent属性,确保在嵌套effect中activeEffect的准确性
try {
this.parent = activeEffect;
activeEffect = this;
// 执行 fn 之前,清除 _effect 实例中所有 deps 相关的 effect 实例
cleanupEffect(this);
// 执行fn的时候就会取值,取值的时候收集当前的activeEffect
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
【问】当
effect
函数内部再嵌套一个effect
时,如何保证activeEffect
的准确性,使得在依赖收集时不发生差错? 【问题描述】当创建内层effect
实例时,activeEffect
为内层的effect
实例,当执行完内层effect
函数,activeEffect
依然为内层的effect
实例,导致外层effect
在进行依赖收集时,收集的是内层的effect
实例。 【解决方案】先用一个变量parent
存储当前的activeEffect
(例如执行内层的_effect.run()
时,存储的activeEffect
就是外层_effect
实例),然后将activeEffect
设为当前的_effect
实例,执行完effect
函数后,将activeEffect
设置回parent
变量存储的activeEffect
(即在执行完内层_effect.run()
后,会将activeEffect
设为外层的_effect
实例),最后将parent
变量置空。
【问】依赖变化时,意料之外的重新渲染 【问题描述】每次触发
trigger
重新执行_effect.run()
,此时effect
的依赖项可能发生变化(可能会删除之前的依赖或新增新的依赖),如果之前的依赖被删掉了,那么就需要清除之前收集的依赖,不然当该依赖变化时会重新执行_effect.run()
,与预期不符。 【解决方案】在_effect.run()
中执行fn
之前,先清除effect.deps
中所有dep
对应的_effect
,然后执行fn
(会读取响应式数据,触发Proxy
中的getter
,重新进行依赖收集)
在执行 _effect.run()
时,会将activeEffect
设为当前的 _effect
实例,然后执行回调函数 fn
,会读取响应式数据,触发数据的 getter
函数,执行 track
进行依赖收集。
track
依赖收集是一个双向收集的过程,在数据中需要收集所有相关的 _effect
实例,在 _effect.deps
中也需要收集所有 _effect
实例相关的数据。
在收集 _effect
实例时,会先创建一个 WeakMap
,收集所有 target
;然后针对每个target
创建一个Map
,在Map
中针对每个属性创建Set
,用来存储与该属性相关的所有 _effect
实例。在属性收集 _effect
实例时,同时在_effect.deps
中收集该属性对应的 Set
数据(里面存储的是该属性相关的所有 _effect
实例)。
// reactivity/src/effect.ts
// 双向依赖收集
const targetMap = new WeakMap();
export function track(target, key) {
// 1. 如果取值操作没有发生在effect中,直接返回,不会进行依赖收集
if (!activeEffect) {
return;
}
// 2. 从映射表中读取属性对应的dep
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 3. 依赖收集
trackEffects(dep);
}
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect);
// 双向收集:一个属性可能对应多个effect,一个effect可能对应多个属性
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
trigger
在响应式数据发生变化时,会触发proxy
中的getter
函数,进而执行trigger
触发更新。
trigger
本质上就是执行依赖收集的所有 _effect
实例的 run()
方法。
// reactivity/src/effect.ts
// 触发更新
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key);
triggerEffects(dep);
}
export function triggerEffects(dep) {
if (dep) {
const effects = [...dep];
// 执行dep中所有effect的run方法
effects.forEach((effect) => {
/**
* 【问题描述】如果在effect内部修改依赖,会触发effect重新执行,造成死循环;
* effect(() => {
* state.age = Math.random(); // 在effect内部修改state,如果此时重新执行当前的activeEffect,会造成死循环
* app.innerHTML = state.age
* })
* 所以重新执行effect时需要判断重新执行的effect是否是当前的activeEffect,如果是当前的activeEffect,则不重新执行
*/
if (activeEffect !== effect) {
effect.run();
}
});
}
}
runner
effect
函数除了执行 _effect.run()
之外,还会将该方法返回;对 effect
函数做出相应修改如下:
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
const runner = _effect.run.bind(_effect); // 保证_effect执行的时候this是当前的effect
runner.effect = _effect;
return runner;
}
stop
_effect
实例还存在一个 stop
方法,该方法会清除依赖,然后将该 _effect
实例变成失活状态,使得在执行 _effect.run()
时仅执行回调函数,不设置activeEffect
(则不会进行依赖收集)
export class ReactiveEffect {
public active = true;
stop() {
// 先将effect的依赖全部删除掉,然后将它变成失活态
if (this.active) {
cleanupEffect(this);
this.active = false;
}
}
run() {
if (!this.active) {
return this.fn();
}
// ...
}
}
scheduler
scheduler
是我们调用 effect
函数时传入的第二个参数 — options
对象中的一个scheduler
属性,它是一个函数,在创建 ReactiveEffect
实例时,将scheduler
作为第二个参数传入。它的特点是:当执行 trigger
触发响应式更新时,如果存在 _effect.scheduler
,则不是执行 _effect.run()
,而是执行 effect.scheduler()
;
所以我们需要对 effect
函数的入参、reactiveEffect
构造函数、以及trigger
的逻辑进行一些修改:
export function triggerEffects(dep) {
if (dep) {
const effects = [...dep];
effects.forEach((effect) => {
if (activeEffect !== effect) {
// 触发trigger时,有effect.scheduler时执行【scheduler】,没有scheduler时才执行run
if (!effect.scheduler) {
effect.run();
} else {
effect.scheduler();
}
}
});
}
}
export class ReactiveEffect {
public fn;
private scheduler;
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
}
// ...
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
// ...
}
ref 相关 API
ref
为什么需要
ref
?reactive
是使用Proxy
创建响应式数据的,而Proxy
不可用于基本数据类型;所以使用ref
创建基本数据类型的响应式收据(ref
也能接收object
类型的数据,会先使用reactive
对其进行处理)
ref
的实现步骤:
ref
主要用于基础数据类型,也能接收object
类型的数据;如果接收的是object
类型的数据,会使用reactive
处理。ref
函数会创建一个class
的实例,并利用class
的访问器,对数据的value
属性进行代理,在get value()
中进行依赖收集,在set value()
中触发更新。- 依赖收集是一个双向收集的过程:在
activeEffect.deps
中收集_RefImpl.dep
;并在_RefImpl.dep
中收集activeEffect
; 也就是说:在执行_effect.run()
时会设置activeEffect
,此时如果触发到_RefImpl.get value()
就会将该activeEffect
收集到_RefImpl.dep
中,也会在activeEffect.deps
中收集_RefImpl.dep
。(注意区分一下与reactive
中双向收集的区别) - 触发更新会遍历
_RefImpl.dep
中所有的_effect
,并执行它们的run()
方法或者scheduler()
方法。 set value()
中,如果新数据是object
类型,也会使用reactive
进行处理。
具体实现如下:
import { isObject } from "@vue/shared";
import { reactive } from "./reactive";
import { activeEffect, trackEffects, triggerEffects } from "./effect";
export function ref(value) {
return new RefImpl(value);
}
function toReactive(value) {
return isObject(value) ? reactive(value) : value;
}
class RefImpl {
dep = undefined;
_value;
__v_isRef = true;
constructor(public rawValue) {
// 如果 ref 传入的是对象,则用reactive将它变成响应式的
this._value = toReactive(rawValue);
}
get value() {
// 依赖收集
if (activeEffect) {
// 双向收集:在 activeEffect.deps 中收集 _RefImpl.dep;并在 _RefImpl.dep 中收集 activeEffect
trackEffects(this.dep || (this.dep = new Set()));
}
return this._value;
}
set value(newValue) {
if (newValue !== this.rawValue) {
// 更新数据:newValue 也需要用 toReactive 进行处理
this._value = toReactive(newValue);
this.rawValue = newValue;
// 触发effect更新
triggerEffects(this.dep);
}
}
}
isRef
isRef
用于判断当前数据是否为 ref
类型。
在ref实例的构造函数中,我们将ref类型的数据做了标记,即 public __v_isRef = true;
,所以只需要判断数据的 __v_isRef
属性是否为 true
,即可判断它是否是 ref
类型。
export function isRef(ref) {
return !!ref.__v_isRef;
}
unRef
unRef
的含义:如果参数为 ref
类型,则返回内部的 value
值,否则返回参数本身。
export function unRef(ref) {
//如果数据是ref,我们返回ref.value,如果不是的话 ,直接返回value就好了
return isRef(ref) ? ref.value : ref
}
toRef
为什么需要
toRef
? 使用reactive
创建的响应式对象,如果要使得其中某一个属性的改变触发响应式更新,需要通过data.xxx.xxx
这种链式取值的方式,如果在setup
中对reactive
数据解构后导出,它会失去响应式。toRef
就是为了保证响应式对象的某一属性在不丢失响应式的前提下,对其进行解构,使其变成ref
类型。
Ref 的特点:
toRef
接收两个参数,第一个参数是一个响应式对象,第二个参数是该对象的某一个键值。toRef
返回的数据与响应式对象的数据是引用关系,两者任意一个发生改变,另一个也会随着改变。toRef
本质上就是一个代理;以let nameRef = toRef(person,'name')
为例,它是通过class
的get
和set
将nameRef.value
代理到person.name
;获取或修改nameRef.value
相当于获取或修改person.name
(由于person.name
是响应式的,所以toRef
返回的数据也是响应式的)。- 经过
toRef
处理的数据也是ref
类型。
export function toRef(target, key) {
return new ObjectRefImpl(target, key);
}
class ObjectRefImpl {
__v_isRef = true; // 经过 toRef 处理的数据也是ref类型
constructor(public _object, public _key) {}
// 一定要代理到value属性,就是为了和ref保持一致,使得 toRef 返回的数据和 ref 一样在 template 中可以直接使用,不必添加 value 属性的后缀
get value() {
return this._object[this._key];
}
set value(newValue) {
this._object[this._key] = newValue;
}
}
toRefs
toRefs
的特点:
- 将一个响应式对象转为普通对象
- 对象的每一个属性都是对应的
ref
类型数据 toRefs
返回的数据与原响应式对象保持引用关系toRefs
本质是遍历对象,对每一个属性使用toRef
进行代理,使得解构之后的对象属性仍具有响应式。
示例:
setup() {
const state1 = reactive(obj1);
const state2 = reactive(obj2);
const stateRefs = toRefs(state1);
const ageRef = toRef(state2, 'age')
return stateRefs.asign({state1, ageRef})
}
toRefs
产生的对象可以直接返回,也可以拼接其它的ref
、toRef
、reactive
数据一起返回。
export function toRefs(object) {
const ret = {};
for (let key in object) {
ret[key] = toRef(object, key);
}
return ret;
}
proxyRefs
我们在 template
中使用 ref
类型的数据(ref
、toRef
、toRefs
产生的数据)不用手动添加 .value
,其原因就是 vue3
内部会将数据用 proxyRefs
做处理。
proxyRefs
会用 Proxy
代理一个对象:
- 如果对象属性是
ref
类型,则将get
和set
都代理到该属性的.value
属性上; - 如果对象属性不是
ref
类型,则正常取值和设值.
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key, receiver) {
let v = Reflect.get(target, key, receiver);
// 如果对象内部属性是 ref 类型,则取其 .value 值,否则直接取值
return v.__v_isRef ? v.value : v;
},
// 如果新值不是ref,且旧值是ref,则替换调旧的value属性;
// 否则整体替换
set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value, receiver);
}
},
});
}
computed
使用
computed
计算属性可以接受一个 getter
函数,返回一个只读的响应式 ref
对象。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref
对象。
实现
具体实现:
- 根据
computed
传入的getter
、setter
,创建ComputedRefImpl
实例; ComputedRefImpl
是一个class
,会创建get value()
、set value()
存/取值函数,并将class
实例设置为ref
类型;- 在
constructor
中会创建一个_effect
实例:在取值函数中当_dirty
为true
时会执行_effect.run()
,此时会计算出最新值,以及收集computed
内部的依赖;在创建_effect
时通过scheduler
实现当内部依赖变化时不直接计算最新值,而是将_dirty
设为true
,并触发与计算属性相关的effect
更新。 - 在取值函数中,如果当前存在
activeEffect
,需要进行依赖收集,收集与当前计算属性相关的_effect
; - 利用
_dirty
实现缓存效果:在取值函数中,如果_dirty
为true
,会执行this.effect.run()
(执行完再将_dirty
设为false
),执行getter
计算出最新值,并在读取到computed
内部的响应式数据时进行依赖收集;当computed
内部的依赖发生变化时,执行_effect
的scheduler
,将_dirty
设为true
,并触发与计算属性相关effect
的更新。
import { isFunction } from "@vue/shared";
import {
ReactiveEffect,
activeEffect,
trackEffects,
triggerEffects,
} from "./effect";
const noop = () => {};
class ComputedRefImpl {
dep = undefined;
effect;
__v_isRef = true; // 设置为 ref 类型
_dirty = true; // 是否需要重新计算
_value; // 计算属性的缓存结果
constructor(getter, public setter) {
this.effect = new ReactiveEffect(getter, () => {
// 属性更新时,触发trigger重新执行effect,但是不执行run,而是执行scheduler,将dirty设为true,下次取值时重新计算;
this._dirty = true;
// 2. 并且触发 this.dep 中所有effect(即计算属性作为依赖的effect)执行
triggerEffects(this.dep);
});
}
// get 和 set 是类的属性访问器,等价于 Object.defineProperty() 中的get、set
get value() {
if (activeEffect) {
// 1. 读取计算属性时,如果存在activeEffect,意味着这个计算属性在effect中使用,需要让这个计算属性收集这个effect;那么当计算属性发生变化时,收集的 _effect 会重新执行
trackEffects(this.dep || (this.dep = new Set()));
}
if (this._dirty) {
// 第一次取值和取新值时才执行effect,并且把取到的值缓存起来
this._value = this.effect.run(); // 执行计算属性的 getter,会进行依赖收集(计算属性依赖的响应式数据会收集计算属性)
this._dirty = false;
}
return this._value;
}
set value(newValue) {
this.setter(newValue);
}
}
export function computed(getterOrOptions) {
let onlyGetter = isFunction(getterOrOptions);
let getter;
let setter;
if (onlyGetter) {
getter = getterOrOptions;
setter = noop;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set || noop;
}
return new ComputedRefImpl(getter, setter);
}
watch 相关 API
watch
watch
的本质就是监听一个响应式数据,当数据发生变化时候,去执行相应的回调函数。
实现步骤:
watch
可以接受三个参数:source
、cb
、options
source
可以是一个响应式对象(此时必须开启深度监听),也可以是一个函数- 深度监听的实现是通过递归取值实现的
watch
的实现本质是创建一个ReactiveEffect
实例,默认先执行一次effect.run()
,会收集监听的数据;当数据变化时,执行scheduler
;在scheduler
中重新执行effect.run()
计算出新值,然后执行回调函数cb(newValue, oldValue)
;- 如果
options.immediate
为true
,会默认先执行一次scheduler
。
import { isReactive } from "./reactive";
import { ReactiveEffect } from "./effect";
import { isFunction, isObject } from "@vue/shared";
// 深度监听:通过递归取值实现
function traverse(source, s = new Set()) {
if (!isObject(source)) {
return source;
}
// 通过 Set 数据类型去重
if (s.has(source)) {
return source;
}
s.add(source);
for (let key in source) {
// 递归取值,取值过程就是收集依赖的过程,即实现了深度监听
traverse(source[key], s);
}
return source;
}
export function watch(source, cb, options) {
let getter;
// 【关键】watch的第一个参数是对象时,该对象必须是响应式的
if (isReactive(source)) {
// watch第一个参数是响应式对象时,默认需要开启深度监听(通过traverse递归取值实现)
getter = () => traverse(source);
} else if (isFunction(source)) {
getter = source;
}
let oldValue;
// watcher effect 的依赖变化时,就会执行scheduler(即job)
const job = () => {
// watch监听的数据变化时,执行scheduler中的effect.run(),即再次执行getter,拿到新值
let newValue = effect.run();
// 执行 watch 中的回调函数
cb(newValue, oldValue);
oldValue = newValue;
};
const effect = new ReactiveEffect(getter, job);
// immediate为true会默认先执行一次回调
if (options?.immediate) {
return job();
}
oldValue = effect.run(); // 执行getter,收集依赖
}
watchEffect
watchEffect
的含义:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
watch
和 watchEffect
的区别:
watch
在回调函数cb
中执行副作用;watchEffect
直接传入一个副作用函数,并立即执行。watch
就是一个effect
+ 自定义scheduler
(在scheduler
中执行effect.run()
和回调cb
);watchEffect
的第一个参数就是副作用函数,它就是一个effect
,当依赖变化时重新执行effect.run()
,没有回调函数cb
;watchEffect
的副作用函数会立即执行,相当于immediate
为true
;watch
和watchEffect
都可以使用doWatch
方法实现,该方法会返回一个函数(本质是执行effect.stop()
)用来停止监听。
function doWatch(source, cb, { immediate } = {} as any) {
let getter;
if (isReactive(source)) {
getter = () => traverse(source);
} else if (isFunction(source)) {
getter = source;
}
let oldValue;
// watcher effect 的依赖变化时,就会执行scheduler(即job)
const job = () => {
// 2.1. 如果是watch API,在 scheduler 中重新执行effect.run(),并调用 cb
if (cb) {
let newValue = effect.run();
cb(newValue, oldValue);
oldValue = newValue;
} else {
// 2.2. 如果是 watchEffect API,在 scheduler 中重新执行 effect.run() 即可
effect.run();
}
};
const effect = new ReactiveEffect(getter, job);
if (immediate) {
return job();
}
oldValue = effect.run(); // 1. 执行getter,收集依赖
// 返回一个函数,用来停止监听。本质就是调用effect.stop()
return () => {
effect.stop()
}
}
export function watch(source, cb, options) {
doWatch(source, cb, options);
}
export function watchEffect(effect, options) {
doWatch(effect, null, options.assign(immediate: true));
}
停止侦听器:
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
watch/watchEffect 中的 onCleanup
无论是 watch
还是 watchEffect
,可以发现在副作用函数中都可以接受一个 onCleanup
;watch
的 onCleanup
是回调的第三个参数,watchEffect
的是回调的第一个参数。
清理回调的方法 onCleanup
会在该副作用下一次执行前被调用,可以用来清理无效的副作用。
onCleanup
的实现步骤:
- 声明一个变量
cleanup
,再声明一个函数onCleanup
;在onCleanup
函数中将cleanup
设为onCleanup
的入参(即用户传入的回调); - 然后在
effect.scheduler
中执行cleanup
;(即每次执行scheduler
前都会执行onCleanup
的回调函数) - 再将
onCleanup
传入副作用函数。
function doWatch(source, cb, { immediate } = {} as any) {
let getter;
if (isReactive(source)) {
getter = () => traverse(source);
} else if (isFunction(source)) {
getter = source;
}
let oldValue;
let cleanup;
function onCleanup(userCb) {
cleanup = userCb;
}
const job = () => {
// 2.1. 如果是watch API,在 scheduler 中调用 cb
if (cb) {
let newValue = effect.run();
// watch的回调函数入参中如果存在cleanup,优先执行cleanup
if (cleanup) {
cleanup();
}
// 在 watch 中,将 onCleanup 是作为cb的第三个参数传入
cb(newValue, oldValue, onCleanup);
oldValue = newValue;
} else {
// 2.2. 如果是 watchEffect API,在 scheduler 中再次执行 effect.run
effect.run();
}
};
const effect = new ReactiveEffect(getter, job);
if (immediate) {
return job();
}
oldValue = effect.run();
}
使用场景:现重复请求某一接口,期望以最新一次的请求结果为准。如果第一次接口请求速度慢,第二次重复请求该接口速度比较快,那么第一次请求的结果会覆盖掉第二次的请求结果,与预期不符;可以使用 onCleanup
将上一次未完成的请求取消/不进行渲染。
watch(source, async (old, new, OnCleanup) => {
// 是否过期是标志
let expired = false
// 注册过期回调
OnCleanup(()=> {
expired = true
})
const res = await fetch('something')
// 如果未过期,那么可以取res为finalData
if (!expired) {
finalData = res
}
})
使用
OnCleanup
关键在于创建闭包,在下一次请求时将上一次的flag
设为false
,导致上一次的请求结果无法渲染。
总结
本小节我们阐述了 vue3 中响应式系统的基本原理,以及相关API的具体实现;它相较于 vue2 有着重大改变,vue3的响应式系统拥有更好的性能,更低的耦合性,更方便的逻辑复用。 在完成数据响应式之后,接下来的问题就是:组件是如何渲染和更新的?接下来,我们将解决这个问题。