vue3-响应式高级篇(一)

175 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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;
}