持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情
这篇主要是对基础篇的reactive和effect的补全,将各类兼容问题代码加入,完善代码。
为什么要reflect
因为 return target[key]不走代理,如果get里面通过this访问其他属性,这边不能监听到,用反射可以把this指向proxy
因为当某个属性的get被更改过后去取其他属性值时,可能拿不到那个属性的监听
举个例子:
let target = {
name: 'zf',
get alias() {
return this.name
}
}
const proxy = new Proxy(target, {
get(target, key, recevier) {
console.log(key)
// return target[key] 只打印 alias
// 他会把this改成recevier,也就是proxy , 执行 target(proxy).name ,就能再度监听
return Reflect.get(target, key, recevier) //打印 alias name 取到两次
}
})
proxy.alias
代码完善
1.重复reactive包裹同一个对象target,返回已经代理过的对象
比如 let a = {x:1};let b = reactive(a);let c = reactive(a);
const reactiveMap = new WeakMap();
const exisitingProxy = reactiveMap.get(target); // 如果缓存中有 直接使用上次代理的结果
if (exisitingProxy) {
return exisitingProxy
}
2.reactive参数不是对象
// reactiveApi 只针对对象才可以
if (!isObject(target)) {
return target
}
3. reactive参数 是一个代理对象 //let a = reactive(b),c=reactive(a)
const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
//reactive 初始化 会去判断有无__v_isReactive属性,没代理前是返回false的,继续去代理
if ((target as any)[ReactiveFlags.IS_REACTIVE]) {
return target
}
// 代理对象去判断有无__v_isReactive属性时会走get,返回为true。那就返回原对象。
proxy=>{
get(target, key, recevier) { // 代理对象的本身
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
4.收集依赖时 由于单线程原因,本来一个变量赋值为当前effect就可以,但是有effect里面套effect的情况,所以需
要加个变量parent存当前effect(activeEffect)(上级effect),
这样嵌套执行完里面的effect时可以设定activeEffect为parent,恢复上级的effect环境,以前是用数组的。
5.如果effect里面又有更改内部响应式变量的表达式,会导致无限循环,
需要在trigger里面执行的时候判断当前执行的effect是不是现在的activeEffect,如果是不要执行。
6.effect执行前需要清除旧的依赖,如果effect不更新的话可能会有多余的执行。
state.flag = true;
effect(()=>state.flag?state.age:state.name) //此时age绑定这个effect
setTimeout(()=>{
state.flag = false;
//现在是name绑定这个函数 flag触发变动时,effect执行前需要清除依赖,也就是让age对应的依赖里去掉这个effect,(effect对应属性们中依赖effect数组里的当前effect,)让他重新去绑定。执行后,name代替age绑定这个effect。
setTimeout(()=>{
state.age = 'xx'; // 此时age不应该触发上面的effect、
})
})
7.响应式对象里面套对象的情况。在get中判断如果返回的是一个对象,则给他绑定上响应式。
const res = Reflect.get(target, key, recevier); // target[key]
if (isObject(res)) {
return createReactiveObject(res)
}
8.因为移除了effect又新加effect,在trigger中在effect遍历run时要先拷贝一份effect,这样才不会死循环。
举个例子:
let set = new Set(['a'])
for(let s of set){
set.delete('a') //清空依赖
set.add('a') //新增依赖
console.log('a')
}
这样子会陷入死循环
可以这样子 let s = new Set(depsMap.get(key)),拷贝一份;
或者和源码一样,let deps = [].push(depsMap.get(key))
源码参考:https://github1s.com/vuejs/core/blob/main/packages/reactivity/src/effect.ts#L287-L288
9.effect的stop和run
let a = effect(() => {
document.getElementById("app").innerHTML = `我${obj.age}岁`;
});
a.stop() //让 age 属性变了也不让effect响应式执行
a() //手动的执行effect里的函数
需要在effect加个active激活状态,设为false的时候,让其不会去搜集依赖,并在所有属性中去掉有关他的依赖
effct执行时,返回_effect.run.bind(_effect);
- 批量调度 正常情况下,每次改变state的数据,都会导致依赖函数的执行,连续多次更改state,会多次执行依赖函数,有时候是没有必要,只需要执行一次的,这时候可以采取下面代码的作法。
将带scheduler方法的对象传入effect的第二个参数,当数据变动时,响应式调用的将是scheduler方法而不是effect的第一个参数的方法,这样我们就能具体怎么执行了。
这个原理就是同步和异步,类似的可以看我这篇源码解读,也是同样的道理,【若川视野 x 源码共读】第31期 | p-limit
let wait = false
const runner = effect(()=>{
document.body.innerHTML = state.age
},{
scheduler(){
if(!wait){
wait = true
Promise.resolve().then(()=>{
runner()
wait = false
})
}
}
})
setTimeout(()=>{
state.age++
state.age++
state.age++
})
完整代码
- shared.js
export function isObject(res: any) {
return typeof res === "object" && res !== null;
}
- reactive.ts
import { ReactiveFlags, baseHandler } from "./baseHandler";
import { isObject } from "./shared";
export function reactive(target: object) {
return createReactiveObject(target);
}
const reactiveMap = new WeakMap(); //存储代理过的对象
export function createReactiveObject(target: object) {
// 先默认认为这个target已经是代理过的属性了
if (ReactiveFlags.IS_REACTIVE) {
return target;
}
// reactiveApi 只针对对象才可以
if (!isObject(target)) {
return target;
}
const exisitingProxy = reactiveMap.get(target); // 如果缓存中有 直接使用上次代理的结果
if (exisitingProxy) {
return exisitingProxy;
}
const proxy = new Proxy(target, baseHandler); // 当用户获取属性 或者更改属性的时候 我能劫持到
reactiveMap.set(target, proxy); // 将原对象和生成的代理对象 做一个映射表
return proxy; // 返回代理
}
- baseHandler.ts
import { track, trigger } from "./effect";
import { createReactiveObject } from "./reactive";
import { isObject } from "./shared";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
export const baseHandler = {
get(target, key, recevier) {
// 代理对象的本身
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
track(target, key);
// 这里取值了, 可以收集他在哪个effect中
const res = Reflect.get(target, key, recevier); // target[key]
if (isObject(res)) {
// vue2是一开始就对对象包括对象里的对象进行递归劫持,vue3是你用到了对象里的对象再进行劫持代理。
return createReactiveObject(res);
}
return res;
},
set(target, key, value, recevier) {
let oldValue = target[key];
// 如果改变值了, 可以在这里触发effect更新
const res = Reflect.set(target, key, value, recevier); // target[key] = value
if (oldValue !== value) {
// 值不发生变化 effect不需要重新执行
trigger(target, key); // 找属性对应的effect让她重新执行
}
return res;
},
};
- effect.ts
export let activeEffect = undefined;
function cleanEffect(effect) {
const { deps } = effect;
//[set[effect,effect],...]
for (let dep of deps) {
// set 删除effect 让属性 删除掉对应的effect name = []
dep.delete(effect); // 让属性对应的effect移除掉,这样属性更新的时候 就不会触发这个effect重新执行了
}
effect.deps.length = 0;
}
export class ReactiveEffect {
active = true; // this.active = true;
deps = []; // 让effect 记录他依赖了哪些属性 , 同时要记录当前属性依赖了哪些effect
parent = null;
constructor(public fn, public scheduler?) {
// this.fn = fn;
}
run() {
// 调用run的时候会让fn执行
if (!this.active) {
// 稍后如果非激活状态 调用run方法 默认会执行fn函数
return this.fn();
} else {
// 收集依赖时 由于单线程原因,本来一个变量赋值为当前effect就可以,但是有effect里面套effect的情况,所以需要加个变量parent存当前effect(activeEffect),这样嵌套执行完里面的effect时可以设定activeEffect为parent,恢复上级的effect环境,以前是用数组的。
try {
this.parent = activeEffect;
activeEffect = this;
cleanEffect(this);
return this.fn(); // 取值 new Proxy 会执行get方法 (依赖收集)
} finally {
activeEffect = this.parent;
this.parent = null;
}
}
}
stop() {
// 让effect 和 dep 取消关联 dep上面存储的effect移除掉即可
// 需要在effect加个active激活状态,设为false的时候,让其不会去搜集依赖,并在所有属性中去掉有关他的依赖
if (this.active) {
this.active = false;
cleanEffect(this);
}
}
}
export function isTracking() {
return activeEffect !== undefined;
}
export const targetMap = new WeakMap();
export function track(target, key) {
// 一个属性对应多个effect, 一个effect中依赖了多个属性 =》 多对多
// 是只要取值我就要收集吗?
if (!isTracking()) {
// 如果这个属性 不依赖于effect直接跳出即可
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map())); // {对象:map{}}
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set())); // {对象:map{name:set[]}}
}
trackEffects(dep);
}
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect); // 看一下这个属性有没有存过这个effect
if (shouldTrack) {
dep.add(activeEffect); // // {对象:map{name:set[effect,effect]}}
activeEffect.deps.push(dep); // 这里让effect 存储 这个属性对应的依赖数组 [set[effect,effect],...],有这个引用,可以操作这个属性对应的依赖数组
}
}
export function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) return; // 属性修改的属性 根本没有依赖任何的effect
let deps = []; // [set ,set ]
// 可以理解成 let effects = new Set(depsMap.get(key))
if (key !== undefined) {
deps.push(depsMap.get(key));
}
let effects = [];
for (const dep of deps) {
effects.push(...dep);
}
triggerEffects(effects);
}
export function triggerEffects(dep) {
for (const effect of dep) {
// 如果当前effect执行 和 要执行的effect是同一个,不要执行了 防止循环
if (effect !== activeEffect) {
if (effect.scheduler) {
return effect.scheduler();
}
effect.run(); // 执行effect
}
}
}
export function effect(fn, options = {} as any) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run(); // 会默认让fn执行一次
let runner = _effect.run.bind(_effect);
runner.effect = _effect; // 给runner添加一个effect实现 就是 effect实例
return runner;
}