一、什么是Vue的响应式?
首先Vue的响应式与要与Vue的双向绑定区分开,Vue的双向绑定指的是视图更新可以驱动数据更新,在使用中即是指v-model
语法糖,而Vue的响应式是指当数据发生变化时,相关的视图会自动更新。
二、Vue3的响应式原理是什么
1.三个核心
要学习Vue3的响应式核心原理首先需要认识响应式的几个核心,学习完这三个角色的作用后实现Reactive
ref
computed
等就非常容易了,但是初学者开始学习这三个响应式核心时往往很容易看得一头雾水,本文从实现一个 Reactive
开始,带你彻底理解Vue3响应式原理
首先认识一下上面的三兄弟
baseHandlers
:baseHandlers是Vue3中响应式系统的处理器,它定义了响应式对象的操作行为。当我们访问响应式对象的属性时,baseHandlers会被触发,它负责收集依赖、触发更新等操作。baseHandlers中定义了一系列处理函数,如get、set、deleteProperty等,这些函数会在相应的操作发生时被调用,从而实现了响应式的效果。dep
:dep是一个依赖管理器,它用来管理响应式对象和effect之间的依赖关系。每个响应式对象上都会有一个与之对应的dep实例,当响应式对象的属性被访问时,会将正在执行的effect函数添加到dep中,同时effect函数也会将dep添加到自己的依赖列表中。当响应式对象的属性发生变化时,dep会通知所有依赖于它的effect函数进行更新。effect
:effect函数是Vue3中用来定义副作用的函数。当我们在effect函数内部访问响应式对象的属性时,Vue3会自动追踪这些依赖,并将effect函数与这些依赖进行关联。当依赖发生变化时,effect函数会被重新执行,从而实现了自动更新。
2.实现一个Reactive
了解完上面三兄弟的大概作用后,我们就可以着手实现一个Reactive
了
2.1 什么是Reactive
返回一个对象的响应式代理。
-
类型
jsfunction reactive<T extends object>(target: T): UnwrapNestedRefs<T>
-
详细信息
响应式转换是"深层"的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何
ref
值得注意的是,当访问到某个响应式数组或
Map
这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用
shallowReactive()
作替代。返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。
-
示例
创建一个响应式对象:
js
const obj = reactive({ count: 0 })
obj.count++
2.2实现一个Reactive
- 创建一个
reactiveMap
,用于存储已经创建的响应式对象。 - 创建一个枚举
ReactiveFlags
,用于标记响应式对象的状态。 - 创建一个
isReactive
函数,用于判断一个对象是否为响应式对象。该函数通过检查对象是否具有ReactiveFlags.IS_REACTIVE
属性来判断。 - 创建一个
toRaw
函数,用于获取一个对象的原始对象。该函数通过检查对象是否具有ReactiveFlags.RAW
属性来判断是否为代理对象。如果是代理对象,则返回原始对象,否则返回对象本身。 - 创建一个
createReactiveObject
函数,用于创建响应式对象。该函数接受一个target
参数和一个baseHandlers
参数,其中target
是要转换为响应式的目标对象,baseHandlers
是响应式对象的处理器。在函数内部,首先检查reactiveMap
中是否已经存在该target
的代理对象,如果存在则直接返回代理对象。如果不存在,则使用Proxy
创建一个代理对象,并将target
和baseHandlers
传入。然后将代理对象存储在reactiveMap
中,并返回代理对象。 - 创建一个
reactive
函数,用于将一个对象转换为响应式对象。该函数调用createReactiveObject
函数,并传入mutableHandlers
作为baseHandlers
参数。
Reactive.ts
js
const reactiveMap = new WeakMap();
const ReactiveFlags = {
IS_REACTIVE: '__v_isReactive',
RAW: '__v_raw',
};
function isReactive(value) {
// 如果 value 是 proxy 的话
// 会触发 get 操作,而在 createGetter 里面会判断
// 如果 value 是普通对象的话
// 那么会返回 undefined ,那么就需要转换成布尔值
return !!value[ReactiveFlags.IS_REACTIVE];
}
function toRaw(value) {
// 如果 value 是 proxy 的话 ,那么直接返回就可以了
// 因为会触发 createGetter 内的逻辑
// 如果 value 是普通对象的话,
// 我们就应该返回普通对象
// 只要不是 proxy ,只要是得到了 undefined 的话,那么就一定是普通对象
// TODO 这里和源码里面实现的不一样,不确定后面会不会有问题
return value && value[ReactiveFlags.RAW] ? value[ReactiveFlags.RAW] : value;
}
function createReactiveObject(target, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
// 如果命中的话就直接返回就好了
// 使用缓存做的优化点
const existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
reactiveMap.set(target, proxy);
return proxy;
}
function reactive(target) {
return createReactiveObject(target, mutableHandlers);
}
通过以上代码,我们可以注意到,Reactive
实现的核心是createReactiveObject
函数,而createReactiveObject
的核心则是 new Proxy(target, baseHandlers)
,也就是说,我们只需要传入target
(需要转换为响应式的对象),以及mutableHandlers
这个Handlers即可将一个对象转换为响应式,我们下一步就可以关注mutableHandlers
的实现即可
baseHandler.ts
js
import { track, trigger } from "./effect";
import {
reactive,
ReactiveFlags,
reactiveMap,
readonly,
readonlyMap,
shallowReadonlyMap,
} from "./reactive";
import { isObject } from "@mini-vue/shared";
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const isExistInReactiveMap = () =>
key === ReactiveFlags.RAW && receiver === reactiveMap.get(target);
const isExistInReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === readonlyMap.get(target);
const isExistInShallowReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === shallowReadonlyMap.get(target);
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
} else if (
isExistInReactiveMap() ||
isExistInReadonlyMap() ||
isExistInShallowReadonlyMap()
) {
return target;
}
const res = Reflect.get(target, key, receiver);
// 问题:为什么是 readonly 的时候不做依赖收集呢
// readonly 的话,是不可以被 set 的, 那不可以被 set 就意味着不会触发 trigger
// 所有就没有收集依赖的必要了
if (!isReadonly) {
// 在触发 get 的时候进行依赖收集
track(target, "get", key);
}
if (shallow) {
return res;
}
if (isObject(res)) {
// 把内部所有的是 object 的值都用 reactive 包裹,变成响应式对象
// 如果说这个 res 值是一个对象的话,那么我们需要把获取到的 res 也转换成 reactive
// res 等于 target[key]
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 在触发 set 的时候进行触发依赖
trigger(target, "set", key);
return result;
};
}
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
// readonly 的响应式对象不可以修改值
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
return true;
},
};
export const mutableHandlers = {
get,
set,
};
export const shallowReadonlyHandlers = {
get: shallowReadonlyGet,
set(target, key) {
// readonly 的响应式对象不可以修改值
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
return true;
},
};
可以看出,mutableHandlers
是一个包含了get
和set
属性的对象,用于处理响应式对象的属性获取和设置操作。也就是说在mutableHandlers
重写了get
和set
方法,以实现一个对象的响应式。接下来我们关注一下Getter
和Setter
的实现。
在createGetter
函数中,我们创建了一个get
函数,该函数接受两个参数:isReadonly
和shallow
。在get
函数内部,我们首先检查是否是特殊的标识属性,如ReactiveFlags.IS_REACTIVE
和ReactiveFlags.IS_READONLY
,如果是,我们返回相应的值。然后我们检查该属性是否存在于reactiveMap
、readonlyMap
或shallowReadonlyMap
中,如果存在,则返回目标对象本身。接下来,我们使用Reflect.get
方法获取目标对象的属性值,并在非只读模式下进行依赖收集。如果shallow
为true
,我们直接返回属性值。否则,如果属性值是一个对象,我们将其转换为响应式对象。
在createSetter
函数中,我们创建了一个set
函数,该函数接受四个参数:target
、key
、value
和receiver
。在set
函数内部,我们使用Reflect.set
方法设置目标对象的属性值,并在触发set
操作后进行依赖触发。
因此,mutableHandlers
对象的get
属性是一个用于处理属性获取的函数,会进行依赖收集和转换为响应式对象的操作。而set
属性是一个用于处理属性设置的函数,会触发依赖更新。 这样,当我们使用mutableHandlers
作为处理器创建响应式对象时,执行get方法时,会进行依赖收集,而执行set方法时会触发依赖更新(视图更新),即实现了一个对象的响应式。
3.实现track
和trigger
通过实现mutableHandlers
可以看出,所谓将一个对象由普通对象变成一个响应式对象只是干了一件事:
重写一个对象(包括对象的对象,即深层)的get
set
方法,以达到劫持一个对象的get
set
方法,在get
中进行依赖收集 track(target, "get", key)
;,在set
触发的时候进行触发依赖 trigger(target, "set", key)
来更新视图
3.1实现track
和trigger
理解完如何将一个对象变成响应式之后,我们就可以来实现一下函数了track
和trigger
,事实上,track
和trigger
函数都存在于effect.ts
中
js
import { createDep } from "./dep";
import { extend } from "@mini-vue/shared";
let activeEffect = void 0;
let shouldTrack = false;
const targetMap = new WeakMap();
// 用于依赖收集
export class ReactiveEffect {
active = true;
deps = [];
public onStop?: () => void;
constructor(public fn, public scheduler?) {
console.log("创建 ReactiveEffect 对象");
}
run() {
console.log("run");
// 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
// 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
// 这里就需要控制了
// 是不是收集依赖的变量
// 执行 fn 但是不收集依赖
// 处于非激活状态的effect对象不会再被触发执行,
// 也不会进行依赖收集。这样可以有效地控制副作用函数的执行时机,
// 避免不必要的执行和资源浪费。
if (!this.active) {
return this.fn();
}
// 执行 fn 收集依赖
// 可以开始收集依赖了
shouldTrack = true;
// 执行的时候给全局的 activeEffect 赋值
// 利用全局属性来获取当前的 effect
activeEffect = this as any;
// 执行用户传入的 fn
console.log("执行用户传入的 fn");
const result = this.fn();
// 重置
shouldTrack = false;
activeEffect = undefined;
return result;
}
stop() {
if (this.active) {
// 如果第一次执行 stop 后 active 就 false 了
// 这是为了防止重复的调用,执行 stop 逻辑
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
export function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn);
// 把用户传过来的值合并到 _effect 对象上去
// 缺点就是不是显式的,看代码的时候并不知道有什么值
extend(_effect, options);
_effect.run();
// 把 _effect.run 这个方法返回
// 让用户可以自行选择调用的时机(调用 fn)
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
export function stop(runner) {
runner.effect.stop();
}
export function track(target, type, key) {
if (!isTracking()) {
return;
}
console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
// 1. 先基于 target 找到对应的 dep
// 如果是第一次的话,那么就需要初始化
let depsMap = targetMap.get(target);
if (!depsMap) {
// 初始化 depsMap 的逻辑
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffects(dep);
}
export function trackEffects(dep) {
// 用 dep 来存放所有的 effect
// TODO
// 这里是一个优化点
// 先看看这个依赖是不是已经收集了,
// 已经收集的话,那么就不需要在收集一次了
// 可能会影响 code path change 的情况
// 需要每次都 cleanupEffect
// shouldTrack = !dep.has(activeEffect!);
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
(activeEffect as any).deps.push(dep);
}
}
export function trigger(target, type, key) {
// 1. 先收集所有的 dep 放到 deps 里面,
// 后面会统一处理
let deps: Array<any> = [];
// dep
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 暂时只实现了 GET 类型
// get 类型只需要取出来就可以
const dep = depsMap.get(key);
// 最后收集到 deps 内
deps.push(dep);
const effects: Array<any> = [];
deps.forEach((dep) => {
// 这里解构 dep 得到的是 dep 内部存储的 effect
effects.push(...dep);
});
// 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
// 这里的目前应该是为了 triggerEffects 这个函数的复用
triggerEffects(createDep(effects));
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
export function triggerEffects(dep) {
// 执行收集到的所有的 effect 的 run 方法
for (const effect of dep) {
if (effect.scheduler) {
// scheduler 可以让用户自己选择调用的时机
// 这样就可以灵活的控制调用了
// 在 runtime-core 中,就是使用了 scheduler 实现了在 next ticker 中调用的逻辑
effect.scheduler();
} else {
effect.run();
}
}
}
再来认识一下dep
js
// 用于存储所有的 effect 对象
export function createDep(effects?) {
const dep = new Set(effects);
return dep;
}
通过上面代码,我们可以知道:
1.track
track
函数用于在依赖收集阶段将响应式对象和对应的属性关联起来。它接受三个参数:target
表示目标对象,type
表示操作类型,key
表示属性名。在track
函数中,首先判断当前是否正在进行依赖收集,如果不是,则直接返回。然后,通过target
从targetMap
中获取对应的depsMap
,如果depsMap
不存在,则创建一个新的Map
并将其存储到targetMap
中。接下来,从depsMap
中获取对应的dep
,如果dep
不存在,则创建一个新的dep
并将其存储到depsMap
中。最后,通过trackEffects
函数将当前的dep
和activeEffect
关联起来。
2.trackEffects
trackEffects
函数用于将依赖添加到dep
中,并将dep
添加到activeEffect
的deps
数组中。它接受一个dep
参数,表示要添加依赖的dep
对象。在trackEffects
函数中,首先判断dep
中是否已经包含了activeEffect
,如果不包含,则将activeEffect
添加到dep
中,并将dep
添加到activeEffect
的deps
数组中。
3.trigger
trigger
函数用于触发依赖更新,即执行与目标对象和属性相关联的依赖的副作用函数。它接受三个参数:target
表示目标对象,type
表示操作类型,key
表示属性名。在trigger
函数中,首先根据target
从targetMap
中获取对应的depsMap
,如果depsMap
不存在,则直接返回。然后,根据key
从depsMap
中获取对应的dep
,并将其添加到deps
数组中。接下来,遍历deps
数组,获取每个dep
中的所有依赖effect
,并将其添加到effects
数组中。最后,通过createDep
函数创建一个包含所有effects
的dep
对象,并调用triggerEffects
函数执行所有依赖的副作用函数。
讲到这里响应式的核心基本已经讲完了,接下来我们可以顺手实现一下Readonly
和shallowReadonly
以及computed
和watch
2.实现Readonly
和shallowReadonly
js
import {
mutableHandlers,
readonlyHandlers,
shallowReadonlyHandlers,
} from "./baseHandlers";
export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_READONLY = "__v_isReadonly",
RAW = "__v_raw",
}
export function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers);
}
export function readonly(target) {
return createReactiveObject(target, readonlyMap, readonlyHandlers);
}
export function shallowReadonly(target) {
return createReactiveObject(
target,
shallowReadonlyMap,
shallowReadonlyHandlers
);
}
export function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
export function isReadonly(value) {
return !!value[ReactiveFlags.IS_READONLY];
}
export function isReactive(value) {
// 如果 value 是 proxy 的话
// 会触发 get 操作,而在 createGetter 里面会判断
// 如果 value 是普通对象的话
// 那么会返回 undefined ,那么就需要转换成布尔值
return !!value[ReactiveFlags.IS_REACTIVE];
}
export function toRaw(value) {
// 如果 value 是 proxy 的话 ,那么直接返回就可以了
// 因为会触发 createGetter 内的逻辑
// 如果 value 是普通对象的话,
// 我们就应该返回普通对象
// 只要不是 proxy ,只要是得到了 undefined 的话,那么就一定是普通对象
// TODO 这里和源码里面实现的不一样,不确定后面会不会有问题
if (!value[ReactiveFlags.RAW]) {
return value;
}
return value[ReactiveFlags.RAW];
}
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
// 如果命中的话就直接返回就好了
// 使用缓存做的优化点
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
3.实现ref
js
import { trackEffects, triggerEffects, isTracking } from "./effect";
import { createDep } from "./dep";
import { isObject, hasChanged } from "@mini-vue/shared";
import { reactive } from "./reactive";
export class RefImpl {
private _rawValue: any;
private _value: any;
public dep;
public __v_isRef = true;
constructor(value) {
this._rawValue = value;
// 看看value 是不是一个对象,如果是一个对象的话
// 那么需要用 reactive 包裹一下
this._value = convert(value);
this.dep = createDep();
}
get value() {
// 收集依赖
trackRefValue(this);
return this._value;
}
set value(newValue) {
// 当新的值不等于老的值的话,
// 那么才需要触发依赖
if (hasChanged(newValue, this._rawValue)) {
// 更新值
this._value = convert(newValue);
this._rawValue = newValue;
// 触发依赖
triggerRefValue(this);
}
}
}
export function ref(value) {
return createRef(value);
}
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
function createRef(value) {
const refImpl = new RefImpl(value);
return refImpl;
}
export function triggerRefValue(ref) {
triggerEffects(ref.dep);
}
export function trackRefValue(ref) {
if (isTracking()) {
trackEffects(ref.dep);
}
}
// 这个函数的目的是
// 帮助解构 ref
// 比如在 template 中使用 ref 的时候,直接使用就可以了
// 例如: const count = ref(0) -> 在 template 中使用的话 可以直接 count
// 解决方案就是通过 proxy 来对 ref 做处理
const shallowUnwrapHandlers = {
get(target, key, receiver) {
// 如果里面是一个 ref 类型的话,那么就返回 .value
// 如果不是的话,那么直接返回value 就可以了
return unRef(Reflect.get(target, key, receiver));
},
set(target, key, value, receiver) {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value, receiver);
}
},
};
// 这里没有处理 objectWithRefs 是 reactive 类型的时候
// TODO reactive 里面如果有 ref 类型的 key 的话, 那么也是不需要调用 ref.value 的
// (but 这个逻辑在 reactive 里面没有实现)
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
// 把 ref 里面的值拿到
export function unRef(ref) {
return isRef(ref) ? ref.value : ref;
}
export function isRef(value) {
return !!value.__v_isRef;
}
4.实现computed
js
import { createDep } from "./dep";
import { ReactiveEffect } from "./effect";
import { trackRefValue, triggerRefValue } from "./ref";
export class ComputedRefImpl {
public dep: any;
public effect: ReactiveEffect;
private _dirty: boolean;
private _value
constructor(getter) {
this._dirty = true;
this.dep = createDep();
this.effect = new ReactiveEffect(getter, () => {
// scheduler
// 只要触发了这个函数说明响应式对象的值发生改变了
// 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
if (this._dirty) return;
this._dirty = true;
triggerRefValue(this);
});
}
get value() {
// 收集依赖
trackRefValue(this);
// 锁上,只可以调用一次
// 当数据改变的时候才会解锁
// 这里就是缓存实现的核心
// 解锁是在 scheduler 里面做的
if (this._dirty) {
this._dirty = false;
// 这里执行 run 的话,就是执行用户传入的 fn
this._value = this.effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}
5.实现watch
js
import { ReactiveEffect } from "./effect";
export function watch(source, cb) {
let oldValue; // 用于存储旧值
const effect = new ReactiveEffect(() => {
const newValue = source(); // 调用source函数获取新值
cb(newValue, oldValue); // 调用回调函数,传递新旧值
oldValue = newValue; // 更新旧值为新值,以备下次比较
});
effect.run(); // 初始触发一次回调函数
}
总结:
看完了,但是还是很懵逼怎么办?可以背呀,以下总结几道常见的Vue3响应式原理面试题,供大家参考。
1.什么是Vue3的响应式原理,介绍一下:
答:你一个月给我多少钱,问我这么刁钻的问题*-/*-/*¥...,开个玩笑
正经的:
Vue3的响应式是指当数据发生变化时,相关的视图会自动更新,在Vue3的底层实现中,这主要是靠Proxy
来劫持一个对象的get
set
方法,通过重写这两个方法,在触发get
时通过track(target, "get", key)
进行依赖收集 ,在set
触发的时候通过 trigger(target, "set", key)
触发依赖来更新视图。
比如在创建一个Reactive
响应对象的过程中,我们会传入一个reactiveMap
(数据结构是weakMap()
),用来存储所有的响应式对象,使用Reactive
创建的响应式对象,对象的属性也都是响应式的,这主要是靠在track
函数的依赖收集时,传入了一个target
,也就是当前需要转化为响应式的对象,初始时,track
内部会创建一个depsMap
(数据结构是Map)用来存储这个对象中所有属性的依赖dep
(键值key为每个对象的属性名),如果没有dep
,会通过createDep
,创建一个dep
(数据结构是Set),并将dep
传入trackEffects
函数中,将activeEffect
也就是当前的effect
函数add
到dep
中,并将dep
push到activeEffect
的deps
中,完成双向的依赖收集。如果二次访问的话,则不会收集依赖,track
会根据isTracking()
(根据shouldTrack
和activeEffect
来判断)值来确定当前是否处于依赖收集阶段,来确定是否需要依赖收集,如果为否,则会直接return,不需要收集依赖。
当修改这个由Reactive
创建的响应式变量时,会在set
中执行trigger
函数,在trigger
函数中,会将传入的target
作为depsMap
,再根据传入的key
值拿到相应的dep
,再将dep
传入triggerEffects
中遍历出所有的effect
并执行effect.run()
方法,以达到更新所有依赖的目的。
借用一下@林三心 大佬的图帮助理解
2.为什么要使用WeakMap存储响应式对象?
答:
- 避免内存泄漏 :
WeakMap
是一种弱引用的数据结构,它的键是弱引用的。这意味着当键对象(即响应式对象)不再被引用时,WeakMap
会自动将其从映射中移除,从而避免了内存泄漏的问题。这对于 Vue 3 的响应式系统来说非常重要,因为当一个组件被销毁时,响应式对象也应该被释放,而不再产生任何引用。 - 隐藏响应式对象 :使用
WeakMap
可以将响应式对象隐藏起来,外部无法直接访问。这对于保护响应式对象的完整性和封装性是有帮助的。只有通过指定的方式(如reactive
函数)创建的响应式对象才能被追踪和使用。 - 高效查找 :
WeakMap
提供了高效的键值查找和存储操作,可以快速地找到对应的响应式对象。这对于在依赖收集和触发依赖更新时需要查找响应式对象非常重要,可以提高系统的性能和效率。
3.为什么要使用dep要选择Set来存储依赖
答:
- 唯一性:
Set
是一种集合数据结构,其中的元素是唯一的,不会重复。在依赖收集的过程中,同一个响应式对象可能会被多个属性依赖,使用Set
可以确保每个依赖只被收集一次,避免重复收集和重复触发依赖更新。 - 快速查找:
Set
提供了高效的查找操作,可以在常数时间内判断某个依赖是否已经存在。这对于在触发依赖更新时需要快速判断是否已经存在依赖非常重要,可以提高系统的性能和效率。 - 理论基础:
Set
作为一种集合数据结构,与依赖收集的概念非常契合。依赖收集的本质是建立起属性与依赖之间的关联关系,而Set
可以提供集合的操作,方便管理和维护这些关联关系。 - 可迭代性:
Set
是可迭代的,可以通过遍历Set
来获取其中的每一个依赖。这对于触发依赖更新时需要遍历依赖进行相应操作非常有帮助。
综上所述,Dep
使用 Set
来存储依赖具有唯一性、快速查找、理论基础和可迭代性等优势。这样可以确保每个依赖只被收集一次,方便管理和维护依赖关系,并提供高效的依赖更新操作。
4.为什么是 readonly 的时候不做依赖收集呢
答:readonly
的话,是只读的,是不可以被 set
的, 那不可以被 set 就意味着不会触发 trigger
所有就没有收集依赖的必要了
5.为什么computed具有缓存的功能
答:初始情况下,计算属性的值是未缓存的,即 _dirty
为 true
。当计算属性的值被访问时,会执行 get value()
方法。该方法首先会通过 trackRefValue(this)
收集依赖,然后会检查 _dirty
的状态。
如果 _dirty
为 true
,表示计算属性的依赖发生了变化,需要重新计算属性的值。此时,会执行 this.effect.run()
方法来执行 getter
函数,计算属性的值得到更新,并缓存在 _value
中。然后,将 _dirty
设置为 false
,表示计算属性的值已经被缓存。
如果 _dirty
为 false
,表示计算属性的依赖没有发生变化,可以直接返回缓存的值 _value
,避免重复计算。
这样,通过 _dirty
的状态来控制计算属性的缓存,只有在依赖发生变化时才重新计算属性的值,否则直接使用缓存的值,实现了计算属性的缓存功能。
6.scheduler的功能是什么
答:scheduler
可以让用户自己选择调用的时机,这样就可以灵活的控制调用了,在 runtime-core
中,就是使用了 scheduler
实现了在 nextticker
中调用的逻辑。 在computed
中new ReactiveEffect
赋值给effect时也有传入scheduler
,作用是只有当this._dirty
为false
也就是不需要重新计算属性的值时才需要触发依赖更新,以提高性能。
js
() => {
// scheduler
// 只要触发了这个函数说明响应式对象的值发生改变了
// 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
if (this._dirty) return;
this._dirty = true;
triggerRefValue(this);
});
}
7.WeakMap和Map,WeakSet和Set的区别是什么?
答:
-
ES6 提供了新的数据结构
Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值。WeakSet
结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet
的成员只能是对象,而不能是其他类型的值。其次,WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 -
ES6 提供了
Map
数据结构。它类似于对象,也是键值对的集合,但是"键"的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object
结构提供了"字符串---值"的对应,Map
结构提供了"值---值"的对应,是一种更完善的 Hash 结构实现。如果你需要"键值对"的数据结构,Map
比Object
更合适。WeakMap
与Map
的区别有两点。首先,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。其次,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap
里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
8.为什么在Handlers重写get set方法要使用Reflect.get/Reflect.set来调用?
答:为了避免无限循环,如果直接receiver[key]
或者receiver[key] = value
,相当于又访问了一遍自身,导致无限循环,而通过Reflect.get和Reflect.set
,绕个弯去访问属性或者设置属性,则不会。
介绍下Reflect
Reflect
是一个内置的 JavaScript 对象,提供了一组与原型链、属性、方法等相关的操作方法。它的主要目的是将一些原本只能通过语言内部操作的功能,暴露给开发者,使得这些功能可以通过方法调用的方式进行操作。
Reflect
对象提供了一系列静态方法,这些方法与一些对应的操作符和内置函数具有相同的功能。这些方法可以用于处理对象的属性、原型链、构造函数等。
一些 Reflect
的常用方法包括:
Reflect.get(target, property, receiver)
:获取目标对象的指定属性的值。Reflect.set(target, property, value, receiver)
:设置目标对象的指定属性为给定的值。Reflect.has(target, property)
:检查目标对象是否具有指定属性。Reflect.deleteProperty(target, property)
:删除目标对象的指定属性。Reflect.construct(target, argumentsList, newTarget)
:使用给定的参数列表创建目标对象的实例。Reflect.apply(target, thisArgument, argumentsList)
:调用目标函数,并传递给定的参数列表。
使用 Reflect
的好处是它提供的方法与操作符和内置函数在语义和行为上保持一致,使得代码更加统一和易于阅读。同时,通过 Reflect
提供的方法,可以在某些情况下获得更好的性能和更严格的错误处理。
需要注意的是,Reflect
方法的返回值通常与相应的操作符和内置函数的返回值一致,但在某些情况下会略有不同,例如 Reflect.set()
方法返回一个布尔值,表示属性是否设置成功。
Reflect
提供了一组与对象操作相关的方法,可以用于替代一些原本只能通过语言内部操作的功能,使得代码更加统一、易于阅读和维护。
参考:
@mini-vue
@林三心
@阮一峰