Skip to content

手写computed

回顾computed的用法

首先回顾一下 computed 的基本用法:

js
const state = reactive({
  a: 1,
  b: 2
})

const sum = computed(() => {
  return state.a + state.b
})
js
const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value
  },
  set(newValue) {
    ;[firstName.value, lastName.value] = newValue.split(' ')
  }
})

实现computed方法

1.参数归一化

首先第一步,我们需要对参数进行归一化,如下所示:

js
function normalizeParameter(getterOrOptions) {
  let getter, setter;
  if (typeof getterOrOptions === "function") {
    getter = getterOrOptions;
    setter = () => {
      console.warn(`Computed property was assigned to but it has no setter.`);
    };
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  return { getter, setter };
}

上面的方法就是对传入 computed 的参数进行归一化,无论是传递的函数还是对象,统一都转换为对象。

2.建立依赖关系

接下啦就是建立依赖关系,如何建立呢?

无外乎就是将传入的 getter 函数运行一遍,getter 函数内部的响应式数据和 getter 产生关联:

js
// value 用于记录计算属性的值,dirty 用于标识是否需要重新计算
let value,
  dirty = true;
// 将 getter 传入 effect,getter 里面的响应式属性就会和 getter 建立依赖关系
const effetcFn = effect(getter, {
  lazy: true,
});

这里的 value 用于缓存计算的值,dirty 用于标记数据是否过期,一开始标记为过期方便一开始执行一次计算到最新的值。

lazy 选项标记为 true,因为计算属性只有在访问的之后,才会进行计算。

3. 返回一个对象

接下来向外部返回一个对象:

js
let value;
  //建立一个返回对象
  const obj = {
    get value() {
      value = effectFn();
      return value;
    },

    set value(newVal) {
      setter(newVal);
    },
  };
  return obj;

该对象有一个 value 访问器属性,当访问 value 值的时候,会根据当前是否为脏值来决定是否重新计算。

优化细节

1.计算属性缓存

我们都知道vue的计算属性是有缓存的,即依赖不改变,compued就不会再次运行;

js
//但是我们现在实现的是不论依赖是否修改,都会进行运行。
const state = reactive({
  a: 1,
  b: 2,
});

const sum = computed(() => {
  console.log("运行了计算属性");
  return state.a + state.b;
});
console.log(sum.value);//("运行了计算属性"); 3
console.log(sum.value);//("运行了计算属性"); 3
console.log(sum.value);//("运行了计算属性"); 3
console.log(sum.value);//("运行了计算属性"); 3

这个时候,我们只需要将计算属性的值进行一下缓存即可

js
export default function computed(getterOrOptions) {
   //...
    
  let value;
  let dirty = true; //+++ 设置是否是脏数据,用来判断是否重新计算
  const obj = {
    get value() {
      //+++ 设置计算属性缓存
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      return value;
    },
      
    //...
  };
  return obj;
}

但是仍旧会出现问题:

js
console.log(sum.value);//3
state.a = 20;//修改了a的值
console.log(sum.value);//打印出来依旧是3

原因就是我们在执行完value = effectFn();后将dirty设置为了false,我们只需要在effect函数的scheduler的配置项中将dirty修改为true即可

js
  // 建立响应式数据和computed之间的依赖关系
  const effectFn = effect(getter, {
    lazy: true,
    //scheduler是在实现effect函数的时候预留的配置项,作用是将要执行的依赖函数作为参数返回给用户,让用户自行处理,而不自己再自动执行trigger
    scheduler() {
      dirty = true;//+++
    },
  });

此时结果就正常了。

2. 处理渲染函数的依赖

目前为止,我们的计算属性工作一切正常,但是这种情况,某一个函数依赖计算属性的值,例如渲染函数。那么此时计算属性值的变化,应该也会让渲染函数重新执行才对。例如:

js
const state = reactive({
  a: 1,
  b: 2,
});
const sum = computed(() => {
  console.log("computed");
  return state.a + state.b;
});

effect(() => {
  // 假设这个是渲染函数,依赖了 sum 这个计算属性
  console.log("render", sum.value);
});

state.a++//当响应式数据修改了,依赖computed的渲染函数也应该重新执行

执行结果如下:

js
computed
render 3
computed

可以看到 computed 倒是重新执行了,但是渲染函数并没有重新执行。

怎么办呢?很简单,内部让渲染函数和计算属性的值建立依赖关系即可。

js
const obj = {
  // 外部获取计算属性的值
  get value() {
    //+++ 相当于计算属性的 value 值和渲染函数之间建立了联系
    track(obj, TrackOpTypes.GET, "value");
    // ...
  },
 	// ...
};
return obj;

首先在获取依赖属性的值的时候,我们进行依次依赖收集,这样因为渲染函数里面用到了计算属性,因此计算属性 value 值就会和渲染函数产生依赖关系。

js
const effetcFn = effect(getter, {
  lazy: true,
  scheduler() {
    dirty = true;
    //+++ 派发更新,执行和 value 相关的函数,也就是渲染函数。
    trigger(obj, TriggerOpTypes.SET, "value");
  },
});

接下来添加配置项 scheduler,之后无论是 state.a 的变化,还是 state.b 的变化,都会进入到 scheduler。进入scheduler就意味着响应式数据变化了,派发更新重新执行记录的依赖函数。而在 scheduler 中,手动派发和 value 相关的更新即可。

此时,依赖cpmputed的渲染函数也会进行更新了。

完整代码

js
// 实现computed函数

import { effect } from "../../effect/effect.js";
import track from "../../effect/track.js";
import trigger from "../../effect/trigger.js";
import { TriggerOpTypes, TrackOpTypes } from "../../utils/index.js";

/**
 * 参数归一化
 * @param param
 * @returns {object} 归一化后的参数对象
 */
function normalizeParam(param) {
  /* 思路:用户传递的可能是配置对象或者一个getter函数
        我们需要统一一下用户的参数格式
*/
  let getter, setter;
  if (typeof param === "function") {
    getter = param;
    setter = () => {
      console.warn("computed setter is not defined");
    };
  } else if (typeof param === "object") {
    getter = param.get;
    setter = param.set;
  }

  return { getter, setter };
}

/**
 * 计算属性computed API
 * @param getterOrOptions getter函数或者配置对象
 * @returns {object} 计算属性结果 {value}
 */
export default function computed(getterOrOptions) {
  // 先进行参数归一化
  const { getter, setter } = normalizeParam(getterOrOptions);

  let value;
  let dirty = true; //脏数据,用来判断是否重新计算

  // 建立响应式数据和computed之间的依赖关系
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true;
      trigger(obj, TriggerOpTypes.SET, "value");
    },
  });

  //返回对象
  const obj = {
    get value() {
      track(obj, TrackOpTypes.GET, "value");
      // 设置计算属性缓存
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      return value;
    },

    set value(newVal) {
      setter(newVal);
    },
  };
  return obj;
}

-EOF-

MIT License