2. 实现Vue3源码 —— computed与watch

140 阅读4分钟

1. 实现computed源码

实现computed源码首先需要知道computed有那些特性,我们通过案例使用一下

1.1 computed的使用

import { computed, effect, reactive } from "@hpstream/reactivity";

const state = reactive({ flag: true, name: "jw", age: 30 });
let valueStr = computed(() => {
  return `姓名:${state.name},年龄:${state.age}`;
});

// console.log(valueStr);

setTimeout(() => {
  // state.age = 100;
  // state.flag = false;
}, 1000);

effect(() => {
  document.body.innerHTML = valueStr.value;
});


根据上面的例子运行例子可知,我们可以得到如下特性:

  1. computed 也是一个effect, 但是只收集computed里面函数使用的变量。
  2. computed 拥有缓存的效果,只有当依赖的值发生改变,函数才会重新执行。

根据此特性,我们开始实现computed:

  1. 由于computed函数的参数有可能是一个函数(代表getter),也有可能是一个对象,即有getter也有setter, 所以我们要对这两种类型做判断处理,如下面代码的情况。
// 使用方式一
let valueStr = computed(() => {
  return `姓名:${state.name},年龄:${state.age}`;
});

// 使用方式二

let valueStr = computed({
    get:() => {
      return `姓名:${state.name},年龄:${state.age}`;
    },
  	set:(val)=>{
      	return val;
    }
});

1.2 computed源码实现

  1. 判断参数不同的情况
export const isFunction = (value) => {
  return typeof value === "function";
};

export function computed(getterOrOptions: any) {
  var setter, getter;
  // 判断是函数,还是对象
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions;
    setter = () => {};
  } else {
    setter = getterOrOptions.set;
    getter = getterOrOptions.get;
  }
  return new ComputedRefImpl(getter, setter);
}

  1. 实现核心逻辑ComputedRefImpl

实现逻辑与reactive类似,进行依赖收集,但使用了dir变量标记依赖值是否发生了变化。

class ComputedRefImpl {
  public effect;
  public _value;
  public dirty = true;
  public deps = new Set();
  constructor(public getter, public setter) {
    
    // 初始化依赖收集函数
    this.effect = new ReactiveEffect(getter, () => {
      // 当收集值发生变化时,触发此方法;
      if (!this.dirty) { 
        this.dirty = true;// 修改为true表示需要重新计算;
        triggerEffects(this.deps);
      }
    });
  }

  get value() {
    // 获取值的时候进行依赖收集
    trackEffects(this.deps);
    if (this.dirty) {
      this.dirty = false;
      this._value = this.effect.run();
    }
    return this._value;
  }

  set value(val) {
    this.setter(val);
  }
}
  1. ReactiveEffect 的实现

ReactiveEffect 的第二个函数,我们称之为调度器,当第二个函数存在时,触发更新的时,会直接走调度器函数。

export class ReactiveEffect {
  public parent = null;
  public deps = [];
  public active = true;
  constructor(public fn, public scheduler) {}

  run() {
    if (!this.active) {
      return this.fn();
    }
    try {
      this.parent = activeEffect;
      activeEffect = this;
      cleanupEffect(this);
      return this.fn();
    } catch (error) {
    } finally {
      activeEffect = this.parent;
    }
  }
  stop() {
    if (this.active) {
      this.active = false;
      cleanupEffect(this); // 停止effect的收集
    }
  }
}

export function trackEffects(dep: any) {
  if (!activeEffect) return;
  let sholdTrack = dep.has(activeEffect);
  if (!sholdTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

export function triggerEffects(effects: any) {
  if (effects) {
    effects = [...effects];
    effects.forEach((effect) => {
      if (effect !== activeEffect) {
        if (effect.scheduler) {
          effect.scheduler();// 触发调度器函数
        } else {
          effect.run(); // 防止循环
        }
      }
    });
  }
}

2. watch 源码实现

2.1 watch 的使用

下面代码是我们使用watch的常见的三种方式,解释下他们之间的区别:

  1. 方式一:直接传递state,或者()=>state,由于他们是引用类型,所以新值和老值他们最终的结果是一样的,也就是说我们无法获取到老值了。
  2. 方式二:是我们最常用的方式,是可以获取到老值的。
  3. 方式三:在异步获取请求的时候,保证顺序展示是正常的,不会出现乱序的现象;(详解:第一次发送请求回来需要3s中,第二次发送请求回来需要2s中,那么2s次的结果先渲染,第一次的结果后渲染,这不是我们想要的)
import { reactive, watch } from "@hpstream/reactivity";
const state = reactive({ flag: true, name: "jw", age: 30, adds: { age: 3 } });

// 使用一
watch(state,
   async (newValue, oldValue) => {
     // newValue === oldValue 为true
     console.log(newValue, oldValue);
   }
);

// 方式二
watch(()=>stage.age,
   async (newValue, oldValue) => {
     // newValue === oldValue 为 false
     console.log(newValue, oldValue);
   }
);


// 方式三 onCleanUp的使用
const state = reactive({ flag: true, name: "jw", age: 30 });
let i = 5000;
function getData(timer) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(timer);
    }, timer);
  });
}
watch(
  () => state.age,
  async (newValue, oldValue, onCleanup) => {
    let clear = false;
    onCleanup(() => {
      console.log(1);
      clear = true;
    });
    i -= 1000;
    let r: any = await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
    if (!clear) {
      document.body.innerHTML = r;
    }
  }
);
state.age = 31;
state.age = 32;
state.age = 33;
state.age = 34;

2.2 源码实现

watch 源码的基础实现

export function watch(source, cb: Function) {
  let getter;
  if (isReactive(source)) {
    getter = () => source; // 存在问题
  }
  if (isFunction(source)) {
    getter = source;
  }
  
  let oldValue; // 存储老值
  const job = () => {
    const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
    cb(newValue, oldValue);
    oldValue = newValue;
  };
  const effect = new ReactiveEffect(getter, job);
  oldValue = effect.run();
}

但是上诉代码存在问题,无法进行对象的深度收集,因为我们在收集依赖的时候没有去遍历对象的每个属性,所以我们对上面的代码稍作修改

// .....
 let getter;
 if (isReactive(source)) {
   
    getter = () => traverse(source); 
  }
  if (isFunction(source)) {
    getter = source;
  }
//....

// 遍历对象的每个属性
function traverse(value: any, seen = new Set()) {
  if (!isObject(value)) {
    return value;
  }
  if (seen.has(value)) {
    return value;
  }
  seen.add(value);
  for (const k in value) {
    // 递归访问属性用于依赖收集
    traverse(value[k], seen);
  }
  return value;
}

这样子我们就完成了深度依赖收集的处理。

2.3 处理方式三的代码

watch的监听函数增加第三个参数,用来取消直接的代码逻辑。其补充代码如下:

// ....
let oldValue; // 存储老值
let cleanup;
let onCleanup = (fn) => {
    cleanup = fn;
 };
  const job = () => {
   	const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
   	if (cleanup) cleanup();
   	cb(newValue, oldValue, onCleanup);
    oldValue = newValue;
  };
  const effect = new ReactiveEffect(getter, job);
  oldValue = effect.run();
// ...

大功造成~!!!

致谢

如果感觉我的文章有用,请关注下我的公众号: 前端小黄。 我将不定期更新我的原创文章。

本文章源码地址:github.com/hpstream/vu…