vue3的模块组成

mini-vue3实现记录 - reactivity

  • vue:即咱们平常导入运用的vue
  • compiler-sfc 依靠于 compiler-domcompiler-coretemplate模板转化成对应的render函数,
  • runtime 模块担任履行render函数

effect & reactive & 依靠搜集 & 触发依靠

  • 关于一个呼应式目标,其内部存在一个容器寄存依靠,经过effect函数搜集依靠
  • effect函数接纳一个参数函数fn,即为依靠,运转fn会触发封装的署理目标proxygetget 除了完结赋值操作,还有进行track依靠搜集
  • track函数的作用便是保存依靠。咱们需求两个表,一个根据target获得该target一切key即key对应的deps,一个Set根据key获得对应deps,当搜集依靠时,咱们将当时传入的fnadd到该set即可? 但是咱们的入参只要targetkey,怎么获得这个fn呢? 咱们能够创建一个大局目标activeEffect,因为咱们是先履行的fn,再触发track,这意味着咱们能够先保存这个fn,再在track中获得。
export function track(target, key) {
 // 咱们需求一个容器,存储呼应式目标的特点对应的一切依靠,关于target
 // 那么这个对应关系便是: target -> key -> deps
 // 所以咱们需求一个Map,寄存一切target, 还需求一个表来寄存该`target`对应`key`的一切dep
 // 考虑到一个`key`可能有相同依靠,关于`dep`的搜集,咱们运用Set数据结构
 let depsMap = targetMap.get(target);
 // depsMap: `key`为呼应式目标的键`key`, `value`为这个`key`对应的依靠
 if (!depsMap) {
  // init
  depsMap = new Map();
  targetMap.set(target, depsMap);
 }
 let dep = depsMap.get(key);
 if (!dep) {
  // init
 dep = new Set();
  depsMap.set(key, dep);
 }
 // 咱们需求将fn存入,怎么获得?
 // 因为咱们是先进行fn的履行,所以咱们能够创建一个大局目标,在fn履行时使其指向当时的ReactiveEffect目标,然后在track中即可获得
 dep.add(activeEffect);
}
  • 当咱们修正呼应式目标时,则需求触发依靠触发依靠的逻辑很简略,取出一切dep循环调用即可。
// 触发依靠,根据`target`和`key`去除dep表,遍历履行即可
export function trigger(target, key) {
 let depsMap = targetMap.get(target);
 let dep = depsMap.get(key);
 for (const effect of dep) {
  effect.run();
 }
}

runner

runner用于保存传入effectfn函数,当调用effect时,fn会作为回来值回来。

scheduler

scheduler作为effect函数的第二个参数,能够到达以下作用:

  • effect函数第一次履行时,仍会履行fn函数,进行依靠搜集
  • 当改动呼应式目标的值时,不会触发fn,而是会履行scheduler
  • 因为runner保存了fn,所以能够经过runner调用fn

stop功用

stop函数能够完结将依靠从依靠表中删去的功用,先从单测下手

mini-vue3实现记录 - reactivity
能够看到, 调用stop(runner)后,改动呼应式目标的值不会导致dummy的值变化,这是因为依靠现已被删去,而从头履行runner后,因为依靠被从头搜集,改动呼应式目标的值能够使dummy同步变化。

完结

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity

  • runner.effect保存了当时履行的依靠目标,调用该目标上的stop办法
  • stop办法主要完结两件事:1. 履行cleanupEffect函数,铲除当时依靠 2. 假如scheduler目标中有传入回调函数就履行该函数
  • cleanupEffect函数接纳一个目标effect,即当时的依靠目标,要在依靠表中删去该目标,咱们首先要在依靠目标上绑定依靠表,activeEffect指向当时的依靠目标,创建特点deps数组来保存dep依靠, 在track搜集依靠时保存。
  • 咱们还能够进行优化:当用户重复调用stop时,咱们只履行一次逻辑,在这儿咱们创建active特点并初始化为true,当stop被调用,将其赋为false防止其再次履行

修正stop上的bug

还是从单测下手

mini-vue3实现记录 - reactivity
假如咱们运用obj.prop++,而不是obj.prop = 3这一简略赋值操作的话(只触发set),咱们会一起触发setget,因为obj.prop++ => obj.prop = obj.prop + 1,那么这导致的问题便是,调用stop(runner)删去的依靠从头被搜集,即stop函数失效。

完结

mini-vue3实现记录 - reactivity

  • 由所以Track的问题,咱们能够考虑在Track前添加是否需求搜集依靠的逻辑判别
  • 何时不需求搜集依靠?1. 仅仅单纯的拜访呼应式目标的特点时,即未履行effect时,activeEffect=== undefined; 2. 当现已调用过stop时,即this.active = false时, 当这两者符合任一种,咱们在Track前直接return
  • 因此在run中,当当时处于stop状态时,咱们直接履行fn而不进行下面的逻辑, 此时Track通道处于封闭状态,而当处于非stop状态时,咱们打开Track通道,因为履行_fn会触发get->Track,所以能够正常搜集

ref

为什么咱们需求ref? 咱们知道,对根本数据类型进行呼应式处理的时分,咱们都会运用ref而不是reactive,这是reactive底层基于的proxy只能署理目标,那怎么处理根本数据类型呢?咱们仍需求将其转化为一个目标,这便是ref.value的必要性。

完结

从单测下手

mini-vue3实现记录 - reactivity

  • 能够看到,想要完结ref,实际上便是要完结一个只要value这一个keyreactive目标即可
  • 值得注意的是,当第2次为a.value赋值为2时,不会从头触发依靠,因为其与旧值相同,所以咱们需求添加新旧值的判别
  • 据此,咱们能够创建一个目标,经过对其的键value进行getset的阻拦, 当令的进行相关依靠搜集和触发依靠即可
class RefImpl {
 private _value: any;
 public dep;
 private _rawValue: any;
 constructor(value) {
  this._rawValue = value;
  this._value = convert(value);
  this.dep = new Set();
 }
 get value() {
  // 依靠搜集
  /**
  * 因为`ref`目标只要`value`一个key
  * 所以咱们搜集时只需求一个Set存储每次的`activeEffect`即可
  * 假如咱们不需求进行依靠搜集,就直接return this._value即可,不然会存入`activeEffect = undefined`
  */
  trackRefValue(this);
  return this._value;
 }
 set value(newValue) {
  // 假如set的值与本来的值相同,则无需重复触发依靠
  if (hasChanged(newValue, this._rawValue)) {
   this._rawValue = newValue;
   this._value = convert(newValue);
   // 触发依靠
   triggerEffects(this.dep);
  }
 }
}
function trackRefValue(ref) {
 if (isTracking()) {
  trackEffects(ref.dep);
 }
}
  • 咱们需求对其依靠搜集和触发依靠进行特别处理,因为该目标只对一个key上的依靠进行处理,所以咱们创建私有特点dep进行保存。 然后运用从reactive抽离出来的逻辑进行依靠处理即可
  • 针对get的阻拦,咱们需求对是否需求进行依靠搜集进行判别,不然可能dep存入undefined
  • 针对set的阻拦,咱们运用变量rawValue对保存转化前的值(因为假如传入目标需求将其进行reactive处理), 和新值进行比照,假如改动了再进行触发依靠

mini-vue3实现记录 - reactivity

  • 当咱们传入目标时,咱们也需求将其转化为呼应式,这儿咱们直接将传入的value运用reactive包裹使其成为呼应式即可。

computed

computed特点在于其的缓存特性,即只要第一次触发getcomputed``依靠的值产生改动时才会进行触发依靠操作。

mini-vue3实现记录 - reactivity

mini-vue3实现记录 - reactivity
从单测可得,咱们首先要完结的是,经过.value拜访到computed目标的值,且再不拜访computed目标的值时,咱们传入的getter不会被调用。
其次,当依靠的呼应式目标值未被改动时,咱们在阻拦对computed目标的get时直接回来value即可,无需触发依靠, 即无需调用getter
最后,当依靠的呼应式值被改动时,getter需求从头被触发。

import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
 private _getter: any;
 private _dirty: boolean = true;
 private _value: any;
 private _effect: any;
 constructor(getter) {
  this._getter = getter;
  this._effect = new ReactiveEffect(getter, () => {
   if (!this._dirty) this._dirty = true;
  });
 }
 get value() {
  // computed取值时,经过get对其进行一个阻拦
  /**
  * 何时需求调用`getter`?
  * 1. 第一次触发`get` 2. 依靠呼应式目标的值改动后
  * 怎么知道依靠的呼应式值产生改动? 经过引进effect
  * 流程:
  * 1. 第一次进入,经过用effect上的`run`函数完结`getter`的调用,完结赋值操作,并封闭调用`getter`的开关,到达缓存作用
  * 2. 当依靠变化时,因为trigger, 咱们传入的scheduler被触发,`getter`触发的通道从头被打开
  * 3. 再次拜访computed目标,触发get value()阻拦,再次调用`getter`完结赋值操作,并封闭调用`getter`的开关。
  */
  if (this._dirty) {
   this._dirty = false;
   this._value = this._effect.run();
  }
  return this._value;
 }
}
export function computed(getter) {
 return new ComputedRefImpl(getter);
}
  • 为了到达控制依靠是否被触发的作用,且咱们需求知道依靠呼应式的值产生改动,咱们需求在computed目标内运用effect,而因为咱们还需求进行其他特别处理,这儿咱们创建ReactiveEffect目标, 经过调用其上的run和传入scheduler到达该作用。
  • 咱们需求一个变量控制是否调用getter函数,这儿咱们运用dirty变量,初始化为true,当其为true时调用getter函数
  • 当第一次触发getdirtytrue,调用getter,将dirty设为false, 之后当依靠的呼应式目标值未改动时,因为dirtyfalse,只会直接回来value
  • 当依靠的呼应式值产生改动, computed目标上的ReactiveEffect目标触发trigger, 因为传入scheduler,不会履行getter,而是履行scheduler,然后将dirty设为true,当下次computed目标get被触发时,会再次履行getter