【Vue3源码】第二章 effect功能的完善下

【Vue3源码】第二章 effect功能的完善下

前言

上一章节我们实现了effect函数的runner 和 scheduler,这一章我们继续完善effect函数的功能,stop和onstop。

1、实现effect的stop功能

顾名思义,stop就是让effect停下来的函数。那么怎么才能让effect停下呢?很简单,我们触发run方法时,清空targetMap(前几章中我把它比作仓库)中的dep(收集到的ReactiveEffect),那么run方法调用时由于dep中没有ReactiveEffect,自然就无法触发依赖实现响应式啦~

如果你忘了dep是什么看看这张图帮你回忆起来

在这里插入图片描述

先看下我们effect.spec.ts文件中stop函数的测试代码

it("stop",() => {
    
    
    let dummy;
    const obj = reactive({
    
    prop:1})
    const runner = effect(() => {
    
    
      dummy = obj.prop
    })
    //修改响应式对象
    obj.prop = 2
    expect(dummy).toBe(2)
  	//停止响应式
    stop(runner)
  	// 继续修改响应式对象
    obj.prop = 3
  	// 响应式停止了,为什么没有变化呢?我们来实现stop功能!
    expect(dummy).toBe(2)

    //stopped effect should still be manually callable
    runner()
    expect(dummy).toBe(3)

  })

根据上文中测试代码,我们开始实现这个多出来的stop函数,它接收的参数是effect的返回值:runner

  1. 第一步我们先从修改effect函数。
//响应式函数
export const effect = (fn, options: any = {
    
    }) => {
    
    
  const _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();
  // 给runner一个any类型逃脱类型检查(本文不做类型上的考虑)
  const runner :any = _effect.run.bind(_effect);
  // 给runner添加一个属性属性effect
  runner.effect = _effect
  return runner;
};

上面的代码很巧妙的在runner里添加了一个ReactiveEffect实例(_effect),我们的stop函数要接收effect的返回值runner作为参数,而runner又可以把ReactiveEffect实例带进自己的effect属性中。

为什么要这么带呢?

因为runner携带上effect实例作为属性后,就可以在runner中调用ReactiveEffect类中的方法了!

怎么实现呢?

我们接下来就要改造ReactiveEffect类了,写一个stop方法,这样我们就可以在stop函数中通过runner.effect.stop去调用ReactiveEffect类写好的stop方法。

注意stop方法属于ReactiveEffect类,stop函数是独立的函数不要搞混了哦~看下面我们马上修改ReactiveEffect类

  1. 修改track函数实现反向收集

要实现stop方法和stop函数前,我们需要收集到dep依赖(之前的文字中我把它比作二级分类),然后清空dep(二级分类存放的是最终的货物)收集到的依赖。~

vue3源码又很巧妙的在ReactiveEffect中新增了deps属性,即activeEffect(全局变量)中添加了一个deps属性(下面修改ReactiveEffect类会写)

runner新增了一个effect属性巧妙,activeEffect新增了一个deps属性,他们都巧妙的串联了起来

看到这我不禁感叹:优雅!实在是太优雅了~

let activeEffect; // 现在它又多了个deps属性


//依赖收集
export function track(target, key) {
    
    
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    
    
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let dep = depsMap.get(key);
  if (!dep) {
    
    
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect);
  //浅拷贝反向收集到dep
  activeEffect.deps.push(dep)
}
  1. 修改ReactiveEffect类实现stop方法和stop函数

好了dep都被收集到了activeEffect.deps中,runner也可以通过runner.effect.stop调用stop方法了

那么stop方法和stop函数就可以闪亮登场了~

class ReactiveEffect {
    
    
  private _fn;
  deps = []//新增deps数组
  constructor(fn, public scheduler?) {
    
    
    this._fn = fn;
    this.scheduler = scheduler;
  }

  run() {
    
    
    activeEffect = this; //我们会把实例收集到activeEffect中,所以activeEffect中会有deps属性
    return this._fn();
  }
  stop() {
    
    
    // 遍历收集到的deps
    this.deps.forEach((dep:any) => {
    
    
      //因为是浅拷贝收集到的dep,所以这里删掉对应的dep就没有了,没有dep(二级分类)自然就无法触发run方法!
      dep.delete(this)
    });
  }
}

stop函数

很简单,去触发实例中的stop方法即可~

// 停止函数
export const stop = (runner) => {
    
    
  runner.effect.stop()
};

非常的优雅,代码还可以继续优化~

试想一下,我们第一次触发stop函数后是不是已经清空了dep中所有收集到的ReactiveEffect类?

那么我们继续调用stop函数,还有必要继续清空一个空的dep吗??显然是不用的。

为了代码可读性,我们还可以把stop方法中删掉dep的操作抽离成一个函数clearupEffect增加代码可读性

class ReactiveEffect {
    
    
  private _fn;
  active = true; //新增一个变量控制dep是否需要清空
  deps = [];
  constructor(fn, public scheduler?) {
    
    
    this._fn = fn;
    this.scheduler = scheduler;
  }

  run() {
    
    
    activeEffect = this;
    return this._fn();
  }
  stop() {
    
    
    //第一次可以触发
    if(this.active) {
    
    
			// 抽离成clearupEffect
      clearupEffect(this);
      //清空后再次stop就无法操作了,只有下次触发响应式函数充值active才能继续触发依赖~
      this.active = false
    }
  }
}

//封装
function clearupEffect(effect) {
    
    
  effect.deps.forEach((dep: any) => {
    
    
    dep.delete(effect);
  });
}

2.实现onStop功能

和之前的scheduler功能一样,onStop也属于effect第二参数的属性之一,当effect传入onStop函数时,它会在stop函数触发时,跟着触发一次。

我们先看测试用例:

it('onStop',() => {
    
    
    const obj =reactive({
    
    
      foo:1
    })
    const onStop = jest.fn()
    let dummy;
    const runner = effect(
      () => {
    
    
        dummy = obj.foo
      },
      {
    
    
        onStop
      }
    )

    stop(runner)
    expect(onStop).toBeCalledTimes(1)
  })
  1. 第一步修改ReactiveEffect类

    首先我们要在ReactiveEffect类中声明onStop这个属性(函数类型)

    并且在stop方法中,我们通过判断是否传了onStop这个属性来调用执行onStop函数

    实现起来非常简单

class ReactiveEffect {
    
    
  private _fn;
  active = true;
  deps = [];
  onStop? :() => void; //声明它是个函数类型
  constructor(fn, public scheduler?) {
    
    
    this._fn = fn;
    this.scheduler = scheduler;
  }

  run() {
    
    
    activeEffect = this;
    return this._fn();
  }
  stop() {
    
    
    if(this.active) {
    
    
      clearupEffect(this);
      // 判断onstop是否存在,存在就执行这个函数
      if(this.onStop) {
    
    
        this.onStop()
      }
      this.active = false
    }
  }
}

  1. 第二步修改effect函数让它能接受onStop
import {
    
     extend } from "../shared/extend";


//响应式函数
export const effect = (fn, options: any = {
    
    }) => {
    
    
  const _effect = new ReactiveEffect(fn, options.scheduler);
  //extend 相当于 Object.assign,我们把options添加到_effect属性里
  extend(_effect,options)
  _effect.run();
  const runner: any = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
};

vue3源码为了语意化把 Object.assign 改成了 extend 常量

我们在src下新建一个shared文件夹(分享文件夹),并新建一个extend.js文件

在这里插入图片描述

代码如下:

export const  extend = Object.assign

3.单元测试,effect流程图

在这里插入图片描述

好了effect响应式函数的主要功能就基本完成了~

effect的流程图

画这个太累了~不过希望大家会喜欢

在这里插入图片描述

下节预告《vue3源码中的readonly功能》

猜你喜欢

转载自blog.csdn.net/m0_68324632/article/details/129094476