前言
本小节我们开启响应式原理的篇章。
在 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、optionssource可以是一个响应式对象(此时必须开启深度监听),也可以是一个函数- 深度监听的实现是通过递归取值实现的
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的响应式系统拥有更好的性能,更低的耦合性,更方便的逻辑复用。 在完成数据响应式之后,接下来的问题就是:组件是如何渲染和更新的?接下来,我们将解决这个问题。