4、带你一步步实现vue3源码之依赖收集&触发依赖

819 阅读3分钟

依赖收集&触发依赖

在此之前,我们已经实现了reactiveeffect,并且留了一个全局变量activeEffect,我们一起来思考一下,这些东西有什么用,又是怎么联系在一起的。

分析

reactive(obj)返回的是一个proxy代理对象,当程序执行到obj.xx的时候会触发代理对象的get请求,当执行obj.xx = xx的时候则会触发代理对象的set请求

effect(fn)返回的是一个ReactiveEffect实例对象,在函数执行的时候给activeEffect赋值为当前的实例对象,而实例对象存储的有用户传入的fn,并且可通过内部的run方法来执行这个fn

思考

通过上面的准备工作,我们是否可以这样做?当我们在effect函数内部触发obj的get请求的时候,我们去把effect(fn)这个函数存起来,当程序触发它的set请求的时候,我们再从我们存储的变量中取出来,挨个去执行实例对象中的run函数,那不就相当于,当数据更新的时候,会自动去执行effect(fn)中的fn吗?

单测

下面我们再来完善effect的单测,加上更新机制

describe("effect", () => {
  it("happy path", () => {
    ...
    user.age++
    expect(nextAge).toBe(12)
  })
})

当我们的响应式对象其中的某个属性值改变的时候,我们希望nextAge也会同步改变。

编码

我们一步步来实现依赖收集和触发依赖的功能

// src/reactivity/reactive.ts
import {track, trigger} from './effect'
export function reactive (raw) {
  return new Proxy(raw, {
    get (target, key) {
      ...
      track(target, key)
      ...
    },
    set (target, key, value) {
      ...
      trigger(target, key)
      ...
    }
  })
}

上面的track函数便是我们用来收集依赖,trigger用来触发依赖,这两个函数我们都是写在effect.ts文件中。

依赖收集

单纯的说依赖收集,但是到底依赖是什么,我们到底要收集什么,又该怎么存储,请大家仔细观摩下面的图。

img1.jpeg

我们实际要存储的数据结构便是target->key->effect

const targetMap = new Map()
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)
}

在上述的实现代码中,我们通过一个全局变量targetMap来存储我们收集的所有依赖,数据格式是Map,然后依次从中读取target.key的存储容器dep,然后将activeEffect存进去,这样我们就将所有的fn存储起来。

触发依赖

上面,我们已经收集好了所有的依赖,触发依赖就显得简单多了。

export function trigger (target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  dep.forEach((effect) => {
    effect.run()
  })
}

根据trigger(target, key)传入的值,我们从targetMap中取出对应的所有effect实例对象,然后执行所有实例的run方法,这样便是我们的触发依赖。

测试结果

PS D:\user\desktop\mini-vue> yarn test
yarn run v1.22.10
$ jest
 PASS  src/reactivity/tests/effect.spec.ts
 PASS  src/reactivity/tests/index.spec.ts
 PASS  src/reactivity/tests/reactive.spec.ts

Test Suites: 3 passed, 3 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.108 s
Ran all test suites.
Done in 2.36s.

总结

其实vue3的响应式最根本的功能我们已经基本完成,也能达到数据跟随改变了,以前没了解的时候总觉得很神奇,现在看看其实也就这么回事,编码思路真的太重要,哪怕你懂所有的语法api可最后不一定能写出如此精妙的代码,好好学习吧,骚年们!

下一节,我们将来完善effectrunner功能。