reactivity的核心流程

232 阅读7分钟

首先来看我们reactivity的单元测试 (单元测试会用到我们的jest插件,以及jest-run用于我们单侧的debug调试,单侧必备!!!

describe("reactive", () => {
  test("Object", () => {
    const original = { foo: 1 };
    const observed = reactive(original);
    expect(observed).not.toBe(original);
    expect(isReactive(observed)).toBe(true);
    expect(isReactive(original)).toBe(false);
    // get
    expect(observed.foo).toBe(1);
    //     // has
    expect("foo" in observed).toBe(true);
    //     // ownKeys
    expect(Object.keys(observed)).toEqual(["foo"]);
  });
 })
 

reactivity会将我们的对象通过proxy转换成代理对象,转换之后我们就可以通过proxy访问对象中的所有属性 在源码中可以看到我们的reactive入口函数接受一个target对象object

export function reactive(target) {
  return createReactiveObject(target, reactiveMap, mutableHandlers);
}

可以看到我们createReactiveObject创建的就是一个Proxy对象

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;
}

在传入target的同时,还有一个重要的参数就是我们的baseHandlers也就是处理器 在我们的baseHandlers文件中首先定义了我们的get和set方法来操作

const get = createGetter();
const set = createSetter();
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;
    }
    
}

定义了一个枚举类型判断

export const enum ReactiveFlags{
    IS_REACTIVE = '__v_isReactive',
    IS_READONLY = '__v_isReadonly'
}

在我们的get方法中可以看到通过Reflect将我们存储在Proxy中的对象的值也就是res进行了返回,这就是我们的Proxy代理对象的作用,在下面的判断中可以了解reactive做了什么

  • 判断target是否是可读的,如果不是就正常走我们的依赖收集
  • 如果是可读的,但只是浅层可读(也就是我们的shallow)那只需要返回第一层即可
  • 再判断是否是对象,不是就直接返回
  • 是对象再判断是否是可读,为什么还要判断?因为这里就要去实现我们readonly的逻辑,前面只是一个状态,如果是可读用readonly将对象处理,如果不是就继续用reactive进行包裹
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;
  };

下面来看我们的set方法,也就是将我们代理对象中的值进行改变并且把改变后的值进行一个返回

function createSetter() {
  return function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);

    // 在触发 set 的时候进行触发依赖
    trigger(target, "set", key);

    return result;
  };
}

在上面两段代码中分别可以其中有一个track函数和一个target函数,这两个函数的作用就是收集对象中的依赖以及触发依赖 ,至此我们对处理输入对象的get和set方法都有了一个大致的了解

<=============================================================> 下面我们来看另一个重要的模块 effect, 同样是根据我们的单侧来一步步推理

describe('effect',()=>{
       it("should observe nested properties", () => {
    let dummy;
    const counter = reactive({ nested: { num: 0 } });
    effect(() => (dummy = counter.nested.num));

    expect(dummy).toBe(0);
    counter.nested.num = 8;
    expect(dummy).toBe(8);
  });
})

effect会接受用户传入的一个函数,在这个函数中我们可以去做一系列操作.当effect被调用时,要确保传入的函数被调用,以及被更改的值会对象的更新,这个功能的实现会触发两个关键点,就是我们提到的依赖收集以及触发依赖.下面来看看effect中的逻辑

export function effect(fn,options:any = {}){
    // 因为是触发响应,所以接受的是一个函数fn 并且需要先调用一次
    // const scheduler = options.scheduler
    // 18 创建上面类的一个实例,并将fn传入进去
    const _effect = new ReactiveEffect(fn,options.scheduler)  // 47 将 scheduler传入到构造函数中去,并接受他
    //  57 将onStop引入到类中
    // _effect.onStop = options.onStop  
    // 59 代码优化  使用extend  这些公共的工具函数我们可以将其抽离到指定文件夹内 src/shared
    // Object.assign(_effect,options)  
    extend(_effect,options)
    // 19-我们希望可以通过该实例调用上面类的方法时来间接调用fn函数
    _effect.run();


    // 41- 结回effect中的调用 在这里就相当于是调用了实例的方法.我们可以返回该函数  在通过一些处理

    const runner:any = _effect.run.bind(_effect)
    runner.effect=_effect
    return runner
}

可以看到在effect内部先创建了一个类 new ReactiveEffect(fn,options.scheduler) 并把用户传入的fn传入进去,并且调用了它的一个run函数(关键点),内部其实就是将用户传入的fn进行了调用

export class ReactiveEffect{
    private  _fn:any;
    public scheduler:Function | undefined
    deps = [];
    active = true;
    onStop?: ()=> void; // 可有可无
    // 21-我们可以通过内部的构造函数来替换一个等价的fn 下面调用得也就换成了this._fn
    constructor(fn,scheduler?:Function){  // 加上public使其能够被外界获取到  
        this._fn = fn
        this.scheduler = scheduler
    }
    // 20-响应下面的函数调用来创造一个run函数
    run(){
    // 33-将实例对象指向全局变量
    activeEffect = this;
    // 77 我们的收集依赖其实就是在我们的run函数调用后执行的  因此我们可以在这里进行响应的区分
    // 我们可以用active来判断是否是stop状态
    if(!this.active){
    // 78 当前状态为stop直接调用我们的fn并返回
       return this._fn()
    }
}

其中的activeEffect是一个全局变量,接受这个类中的this(接下来会解释其作用) 回到我们的run,当我们调用了run函数之后对值进行操作并要求值会更新,其实就触发了我们reactive中的get操作,回顾上的get就会发现代码会执行到我们的track逻辑

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

  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    (activeEffect as any).deps.push(dep);
  }
}

track会基于传入对象及它的类型做一系列的依赖操作,也就是我们的dep,我们定义的dep可以理解为一个容器.用来收集各种依赖,比如一些函数(activeEffect)

effect.jpg

当触发到我们的set操作时,会遍历取出我们的依赖并执行

const effects: Array<any> = [];
  deps.forEach((dep) => {
    // 这里解构 dep 得到的是 dep 内部存储的 effect
    effects.push(...dep);
  });
  // 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
  // 这里的目前应该是为了 triggerEffects 这个函数的复用
  triggerEffects(createDep(effects));
  
  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();
    }
  }
}

这个过程就对应我们单侧中的num的值发生变化时就会发生我们的触发依赖的操作

let dummy;
    const counter = reactive({ nested: { num: 0 } });
    effect(() => (dummy = counter.nested.num));

    expect(dummy).toBe(0);
    counter.nested.num = 8;
    expect(dummy).toBe(8);

整个触发的流程如下

reactive流程.jpg

effect初始化.jpg

getinit.jpg

update.jpg

以上就是reactivity的核心流程