【mini-vue】实现最简版的 reactive / effect

277 阅读3分钟

Vue reactive / effect

熟悉 Vue 3 的同学们应该知道,vue 3 有 reactiveeffect 两个 API,

reactive 接收一个 Object 对象,返回一个通过 Proxy 包装后的响应式对象,effect 接收一个函数,可用于触发响应式,我们来看看下面的例子

import { reactive, effect } from 'vue'

const counter = reactive({
	value: 1
})
let doubleCounter;
watchEffect(() => {
	doubleCounter = counter.value++
	console.log(doubleCounter)
})
counter.value++

如果执行上面的代码,你将会在控制台看到两次输出,一次是 2,一次是 4,那么我们就知道了 effect 的机制:

  • 首先会执行一次函数,在每次 reactive 对象进行 setter 操作时,也会执行函数

下面我们将动手来试试这个 reactiveeffect

实现 reactive

通过上面的例子我们已经知道:reactive 接收一个对象,返回通过 Proxy 包装后的这个对象,那么我们就可以直接来写了:

// src/reactive.js
export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
	  // 通过 Reflect.get 来获取对象的值
      const res = Reflect.get(target, key)
      return res
    },
    // foo.value = 10
    set(target, key, value) {
	  // 通过 Reflect.set 来更新对象中某个属性的值
      const res = Reflect.set(target, key, value)
      return res
    },
  })
}

通过简单的测试:(这里使用 jest 作为测试框架),证明我们写的代码是没问题的

// src/tests/reactive.spec.js
import { reactive } from '../reactive'

describe('result', () => {
  it('should ', () => {
    const foo = {
      value: 0,
    }
    const observe = reactive(foo)
    // 测试包装后的对象和源对象不等
    expect(observe).not.toBe(foo)
    // 测试包装后的对象的某个属性的值和源对象相同
    expect(observe.value).toBe(0)
  })
})

实现 effect

查看需求

effect 就是本文章的最难点了,我们先统计一下需求:

  • 立即执行一次传入的函数
  • 当被 reactive 包装后的对象执行 setter 时,触发 effect 传入的函数

下面,我们来写一个测试:

import { reactive } from '../reactive'
import { effect } from '../effect'

describe('result', () => {
  it('should ', () => {
    const foo = reactive({
      value: 1,
    })
    let newFoo
    effect(() => {
      newFoo = foo.value * 10
    })
    // 首先会执行一次函数
    expect(newFoo).toBe(10)
    foo.value = 20
    // 当进行 setter 的时候会再次执行一次函数
    expect(newFoo).toBe(200)
  })
})

当执行 setter 的时候,该如何知道自己需要执行哪些函数呢?下面,将是 effect 的两个最重要的功能:

  • 依赖收集 【在 getter 的时候将与整个属性有关的依赖收集起来】
  • 触发依赖【在 setter 的时候就触发所有有关的依赖】
+   import { track, trigger } from './effect'

    export function reactive(raw) {
      return new Proxy(raw, {
        get(target, key) {
          const res = Reflect.get(target, key)
+         track(target, key)
          return res
        },
        // foo.value = 10
        set(target, key, value) {
          const res = Reflect.set(target, key, value)
+         trigger(target, key)
          return res
        },
      })
    }

写功能

// 我们先实现最简单的,先执行一次函数,将 effect 抽象一层
class ReactiveEffect {
    constructor(fn) {
        this._fn = fn
    }
    run() {
        // 这一步是为了下面
        activeEffect = this
        this._fn()
    }
}


let activeEffect

export function effect(fn) {
    const _effect = new ReactiveEffect(fn)
    _effect.run()
}

下面我们来写 track 函数

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 Set 中
  dep.add(activeEffect)
}

targetMapdepsMapdep 都是啥???看图

image.png

下面是 trigger 函数

// trigger 函数很简单,获取到当前 key 的所有依赖,并遍历执行
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  // set
  for (const effect of dep) {
    effect.run()
  }
}

所有的 effect.js 代码

class ReactiveEffect {
  private _fn: any
  constructor(fn) {
    this._fn = fn
  }
  run() {
    activeEffect = this
    this._fn()
  }
}

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)
}

let activeEffect

export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  // set
  for (const effect of dep) {
    effect.run()
  }
}

export function effect(fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

经过测试成功,那么我们的最简版的 reactiveeffect 就完成了

最后感谢 崔大(阿崔crx) 的 mini-vue ,感谢【催学社】学习社群,好的学习氛围可以提高学习效率,欢迎你加入一起学习一起成长!也感谢掘金,可以提供这样良好的技术社区一起交流

  • 崔大的 mini-vue
  • 自己的实现 vue-study,欢迎 star 哦,感谢 💓
  • 配套视频,食用起来更佳哦 地址