在Vue3中,一般提到的响应式包括几个部分
- reactive
- ref
- computed
- watch
这里按照顺序,参考Vue3的源码,把这几个响应式的核心功能给实现一下
reactive(引用类型响应式)
使用方法
Vue3中使用reactive,只是针对对象Object或者数组Array等引用类型数据
创建之后,会生成一个响应式数据,我们可以直接读取/修改值,值一旦修改,会触发对应模板内容的更新
const { reactive, effect } = Vue;
const obj = reactive({
name: "张三",
});
effect(() => {
document.querySelector("#app").innerText = obj.name;
});
setTimeout(() => {
obj.name = "李四";
}, 2000);
分析
对于以上效果,需要分别实现这样一些功能:
- 响应式创建:要可以监听数据变化,并拿到新的数据
- effect方法:监听响应式数据的变化,在初始化以及数据变化时,对应执行
响应式创建
reactive创建的响应式数据大多是Object或者Array。在Vue3中,响应式使用了Proxy创建。
为了能同时监听/触发响应式数据的副作用,应该考虑把副作用和数据绑定起来。
综合这两点,参考源码,可以创建对应的reactive方法,它本质上是执行了createReactiveObject方法,其中入参包括三个:
- 响应式数据target
- 副作用处理baseHandler
- 响应式数据缓存proxyMap
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap);
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
};
// weakMap的意义在于,key是object,一旦这个object清除,则weakmap对应的key也清除
export const reactiveMap = new WeakMap<object, any>();
createReactiveObject方法用Proxy把数据和对应的副作用绑定,同时添加了一个缓存机制(存在reactiveMap中),创建Proxy之前会先看看缓存中有没有记录
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
// 获取proxy缓存
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
响应式的副作用
对于一个响应式数据,我们要能
- 读取最新数据
- 修改数据
这两个副作用,可以用es6的get/set实现,我们可以看到之前的代码中,这两个方法是在baseHandlers里面和响应式数据绑定的
读取get
读取响应式数据的最新值,用到的是get方法,源码中创建自createGetter,而这个方法的核心又是Reflect.get方法,包括三个参数:
- 响应式数据target
- 监听的属性key
- receiver(相当于bind中的this绑定)
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver);
return res;
};
}
除了获取最新值,如果我们还希望在set修改值的时候调用一些副作用,那我们应该在这里收集依赖,即:把和当前响应式数据相关的方法存起来
这里暂时不提,在下面的effect中会完善
写入set
对照上面get方法,修改响应式数据的值的时候,触发Reflect.set修改对应的key的值即可
function createSetter() {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) {
const result = Reflect.set(target, key, value, receiver);
return result;
};
}
对照get获取,set修改值的时候应该触发一些副作用,例如视图更新,这里应该有一个触发依赖的动作
同样暂时不提,在下面effect中完善
effect方法
在示例中,effect的作用是把响应式数据绑定到页面视图中,且当数据更新时,视图也应该跟着更新。
这里effect要做两件事情:
- 首次绑定响应式数据时,把数据渲染到视图上(执行一次)
- 响应式数据更新时,把更新后的数据渲染到视图上(再执行一次)
effect初次调用
创建这个effect方法,参考源码,核心用了ReactiveEffect类,创建完成后,会执行一次run方法,即运行effect中的函数,并标记当前触发的effect以便依赖收集
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn);
if (options) {
// extend本质是Object.assign
extend(_effect, options);
}
if (!options || !options.lazy) {
// 完成第一次run执行
_effect.run();
}
}
export class ReactiveEffect<T = any> {
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null
) {}
run() {
// 标记当前触发的effect
activeEffect = this;
return this.fn();
}
stop() {}
}
export let activeEffect: ReactiveEffect | undefined;
依赖收集
当我们触发了一次依赖以后,我们就能知道这个响应式数据在修改的时候,应该有哪些副作用了,这时可以收集依赖,而依赖的收集过程,从源码中可以得知,存在于get方法中
先前get方法只添加了一个Reflect.get返回最新值,我们可以在这里添加一个track方法,收集这个响应式数据对应key的副作用(准确地说,是响应式数据-key-副作用集合)
这里依赖因为可能一个响应式数据的key有好几个,所以需要创建一个响应式数据key-Set的映射,存储多个,封装为createDep
// 依赖的ts类型和创建方法
export type Dep = Set<ReactiveEffect>;
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects);
return dep;
};
完整的依赖收集代码如下
function get(target: object, key: string | symbol, receiver: object) {
......
track(target, key);
......
};
export function track(target: object, key: unknown) {
if (!activeEffect) return;
// 拿到当前响应式对象的有副作用的key集合
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 拿到当前响应式对象的指定key的副作用
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = createDep()));
}
trackEffects(dep);
// 考虑effect副作用可能有多个,不能直接设置
// depsMap.set(key, activeEffect);
}
// 依次收集依赖
export function trackEffects(dep: Dep) {
dep.add(activeEffect!);
}
/**
* 依赖收集要考虑使用weakmap操作
* key:响应性对象
* value:Map对象
* key:响应性对象指定属性
* value:指定对象的指定属性的执行函数
*/
type KeyToDepMap = Map<any, Dep>;
const targetMap = new WeakMap<object, KeyToDepMap>();
依赖触发(数据更新的副作用)
这里的依赖触发和响应式数据的set关联,在修改响应式数据本身后,我们也得触发一下对应的副作用,实现数据驱动
依赖触发的核心是使用trigger方法
function createSetter() {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) {
......
trigger(target, key, value);
......
};
}
export function trigger(target: object, key: unknown, newValue: unknown) {
// 拿到当前响应式数据所有的有副作用的key集合
let depsMap = targetMap.get(target);
if (!depsMap) return;
// 拿到各个有副作用key的副作用集合
const dep: Dep | undefined = depsMap.get(key);
if (!dep) return;
// 因为一个响应式数据的key可能不止一个副作用,需要循环执行
triggerEffects(dep);
}
// 依次触发依赖
export function triggerEffects(dep: Dep) {
Array.from(dep).forEach((effect) => {
triggerEffect(effect);
});
}
export function triggerEffect(effect: ReactiveEffect) {
effect.run();
}
总结
reactive整体使用包括几个部分:
- 响应式数据创建:
createReactiveObject - 依赖收集:
createGetter、track - 依赖触发:
createSetter、trigger - 副作用:
effect、ReactiveEffect
ref
使用方法
Vue3中使用最灵活的响应式就是ref了,可以自由传入任意的数据类型,包括字符串、数字、布尔,引用类型的对象和数组也可以监听
使用上,ref和reactive类似,都是创建一个响应式数据,数据变化的时候会触发对应的副作用方法,但是有一点不同:读取和修改值的时候要通过value属性操作
const { ref, effect } = Vue;
const name = ref("张三");
effect(() => {
document.querySelector("#app").innerText = name.value;
});
setTimeout(() => {
name.value = "李四";
}, 2000);
分析
对于ref来说,实现起来有两个疑惑点:
- 为什么能监听
引用类型数据呢,和reactive有什么区别 - 为什么要使用
value去访问和修改值
从这两个疑问出发,开始学习源码并写一个简化版的ref
响应式创建
ref的创建,从源码看来,核心也是一个createRef方法,这个方法会创建一个RefImpl类的值并返回
export function ref(value?: unknown) {
return createRef(value, false);
}
// shallow表示浅层响应性,即内部的引用属性不是响应式
// 这里默认创建深层,即每个层级都是响应式
function createRef(rawValue: unknown, shallow: boolean) {
// 要判断一下是否是Ref,不重复创建
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
// 判断是否为ref
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true);
}
RefImpl类内部有rawValue和value两个属性,其中rawValue就是响应式数据的原始内容,而value则是响应式数据
在这里可以看到,ref会把引用类型Object使用reactive创建,且使用了value的get和set钩子,这也就能解答为什么ref也可以输入引用类型,且使用value监听了
class RefImpl<T> {
private _value: T;
private _rawValue: T;
// 是否为ref类型的判断条件
public readonly __v_isRef = true;
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = value;
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
......
return this._value;
}
set value(newVal) {
// 缓存机制,只有数据变化的时候再触发响应性
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = toReactive(newVal);
......
}
}
}
// 判断数据是否改变
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue);
// 判断是否是对象,是的话转用reactive
export const toReactive = <T extends unknown>(value: T): T => {
return isObject(value) ? reactive(value as object) : value;
};
依赖收集
这个和reactive一样,在get钩子中收集响应式对应的副作用方法
class RefImpl<T> {
......
public dep?: Dep = undefined;
get value() {
trackRefValue(this);
......
}
}
// 收集依赖
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
依赖触发
也是一样,在set钩子中触发收集好的副作用方法
class RefImpl<T> {
......
public dep?: Dep = undefined;
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
......
triggerRefValue(this);
}
}
}
// 触发依赖
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep);
}
}
总结
ref的使用其实结合了reactive,对于引用类型的数据,交给reactive去处理,自己主要处理基本类型的数据
ref内部创建了rawValue和value两个属性,get和set钩子绑定在value属性上,因此必须读取/修改value才能触发对应的响应式