vue3 - mini-vue(一)实现reactive & effect & 依赖收集 & 触发依赖

208 阅读4分钟

实现effect & reactive & 依赖收集 & 依赖触发

**开始前请先了解响应式篇梗概响应式篇总结

最近在学习vue3源码,越发感觉尤大牛逼,所以将掘金的处女作献给vue3源码。之前也有写一些东西,不过是在自己的博客上写写东西,当笔记记录下方便查看。由于自己表达能力较差,一开始是拒绝在掘金上发文章的,但是因为欠缺,所以才更要锻炼。文章我会尽量写的细一些,希望能帮到对源码感兴趣的同学。
下面内容都是按照以下流程做的,先根据测试用例和功能描述了解每个模块是做什么的,怎么用,然后再去看它的实现代码,实现完成测试通过后再去优化它。没有将文字描述写在外面,而是通过注释写在了代码块中,觉着看每一行代码的时候可以保持思路清晰,也是我个人的一些小习惯。

  • 测试用例
  • 功能描述
  • 具体实现
  • 优化

前置知识:ProxyReflect


文章中的测试用例执行需要配置环境:vsCode构建基于ts及jest的测试环境,使用webStrom的话只需要安装相关的依赖包即可。

抽象概念:响应式对象中包含一个容器,这个容器需要收集这个对象所有的依赖(通过effect收集)。

reactive

// 测试用例
describe('reactive', () => {
  it('happy path', () => {
    const original = { foo: 1 } // 原始对象
    const observed = reactive(original) // 创建代理对象
    expect(observed).not.toBe(original) // 代理对象不等于原始对象
    expect(observed.foo).toBe(1) // 代理对象的foo属性等于1
  })
})

创建代理对象

const proxyInstance = new Proxy(target, handlers)

创建代理对象的核心是处理器,关键的逻辑就在处理器中

先实现处理器的 get 和 set

  • get
    • 返回代理对象中key对应的值
    • 收集依赖
  • set
    • 设置代理对象中key对应的值

    • 触发依赖

实现思路

创建代理对象并将其返回,核心在于收集和触发依赖

// 实现reactive
function reactive (raw) {
  return new Proxy(
    raw,
    {
      get (target, key) {
        // todo 收集依赖(待实现)
        track(target, key)
        return Reflect.get(target, key)
      },
      set (target, key, value) {
        // todo 触发依赖(待实现)
        trigger(target, key)
        return Reflect.set(target, key, value)
      }
    }
  )
}

effect

effect接受一个函数 fn,每次开始会调用 fn,调用 fn 时访问user.age,

触发了user对象的get方法,当触发get操作的时候,user对象就可以收集到这个fn,这个流程就是依赖收集。

当修改对象的时候,会触发代理对象的set方法,在set中会将所有的依赖调用一次,这个流程就是触发依赖。

// 测试用例
describe('effect', () => {
  it('happy path', () => {
    // 1.user是一个响应式对象,响应式对象包含一个容器,需要收集它所有的依赖
    const user = reactive({ age: 10 })
    let nextAge
    // 2.通过effect收集依赖,传入一个cb并调用它
    effect(() => {
      // 3.调用cb会调用user这个响应式对象的get方法,触发get的时候
      // user响应式对象就可以把这个依赖收集起来
      nextAge = user.age + 1
    })
    expect(nextAge).toBe(11)
    // 更新触发依赖
    // 4.当触发set操作的时候,会把所有收集的依赖调用一遍
    user.age++
    expect(nextAge).toBe(12)
  })
})
// 实现effect - 首次执行
// 抽象出一个类,保存fn,run方法执行fn
class ReactiveEffect {
  private _fn: any
  constructor (fn) {
    this._fn = fn
  }
  run () {
    this._fn()
  }
}

// 调用effect的时候,创建一个对应的reactiveEffect实例,然后调用它的run方法完成首次执行
function effect (fn) {
  const _effect = new ReactiveEffect(fn)
  effect.run()
}
track - 收集依赖
// 收集依赖
const targetMap = new Map() // 所有对象的依赖保存在这个全局变量中
let activeEffect // 需要将传入effct的fn保存起来,利用一个全局变量来获取它
export function track (target, key) {
  // 拿到这个对象对应的依赖项
  let depsMap = targetMap.get(target)
  if (!depsMap) { // 判空
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  // 拿到这个key对应的依赖项(dep中保存的是所有activeEffect实例而不是fn)
  let dep = depsMap.get(key)
  if (!dep) { // 判空
    // 因为要保证每个传入effect的fn都是唯一的,所以使用set数据结构
    dep = new Set()
    depsMap.set(key, dep)
  }
  // 将这个key对应的依赖添加到dep中
  dep.add(activeEffect)
}

// 对run方法进行优化
run () {
/*
将activeEffect赋值为activeEffect实例
run方法正在执行的时候将activeEffect实例赋值给activeEffect,然后会在fn执行的时候触发响应式对象的get方法(get方法中包含track),于是通过dep.add(activeEffect)将依赖项添加到了dep中
*/
  activeEffect = this
  this._fn()
}
trigget - 触发依赖
function trigger (target, key) {
  // 基于target和key获取到dep(这个key对应的所有依赖项的集合),然后全部执行
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  for (const effect of dep) {
    effect.run()
  }
}

targetMap存在的问题

现在的targetMap是用的Map数据结构,存在一个问题:
当targetMap引用的对象被清空时,这个对象在Map中的引用仍然存在,可能会导致内存泄漏。源码中使用的weakMap,它的键只能是对象,而且是弱引用,当引用的对象被清空时,weakMap中对应的key-value会被清空。详情见:map与weakMap