实现 effect 函数

未匹配的标注

第 1 步

packages/reactivity/src/effect.ts

// 用户在调用 effect 的时候,传进来的是个 fn 函数
export function effect(fn) {
  const _effect = new ReactiveEffect(fn)

  _effect.run()
}

// 需要记录 effect 依赖了哪些属性,同时让属性记录依赖了哪个effect
class ReactiveEffect {
  // 表示是否是响应式
  active = true
  // 表示 effect 依赖了哪些属性,同时当前属性依赖了哪个 effect
  deps = []
  constructor(public fn) {

  }

  // 调用 run 的时候,让 fn 执行
  run() {
    if (!this.active) {
      // 如果不是响应式的,则直接执行 fn 函数,不用往下走了
      return this.fn()
    }
  }
}

第 2 步

问题描述:需要考虑怎么让 effect 和属性相互记录,同时,需要考虑 effect 函数的用法,如下的伪代码

effect(() => {
  state.name
  effect(() => {
    state.age
  })
  state.school
})

假如最外层的 effect 是 effect1,里面的 effect 是 effect2,那么 name 需要和 effect1 相互记录,age 需要和 effect2 相互记录,school 需要和 effect1 相互记录,即访问 name 和 school 的时候,触发的是 effect1,访问 age 的时候,触发的是 effect2。
这个时候可以使用栈结构来存储,当执行 effect1 的时候,先将 effect1 放入到栈中,访问 name 属性的时候,则将栈顶的 effect(即 effect1)和 name 相互记录;当执行 effect2 的时候,将 effect2 再放入栈中,访问 age 属性的时候,同样找栈顶的 effect(即 effect2)和 age 相互记录;当 effect2 执行完的时候,再将 effect2 从栈顶弹出;接着访问 school 属性,同样找栈顶的 effect(即 effect1)和 school 相互记录。这样就完成了 effect 和属性相互记录的过程。

packages/reactivity/src/effect.ts

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

  _effect.run()
}

// --------------------------新增 start----------------------------
// 存储相互依赖的 effect 和属性
let effectStack = []
// 当前激活的 effect
let activeEffect = null
// --------------------------新增 end----------------------------

// 需要记录 effect 依赖了哪些属性,同时让属性记录依赖了哪个effect
class ReactiveEffect {
  // 表示是否是响应式
  active = true
  // 表示 effect 依赖了哪些属性,同时当前属性依赖了哪个 effect
  deps = []
  constructor(public fn) {

  }

  // 调用 run 的时候,让 fn 执行
  run() {
    if (!this.active) {
      // 如果不是响应式的,则直接执行 fn 函数,不用往下走了
      return this.fn()
    }

   // --------------------------新增 start----------------------------
   // 避免出现同一个 effect 被收集多次
   if (!effectStack.includes(this)) {
     try {
        effectStack.push(this)
        activeEffect = this
        // 此时会执行用户传入的 fn 函数,则会去取值,也就是走到了 reactive 函数中 proxy 的 get 函数中(依赖收集)
        return this.fn()
      } finally {
        // 当当前 effect 函数执行完之后,需要将该 effect 函数从栈中删除,同时将当前激活的 effect 重新赋值
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }
        }
    // --------------------------新增 end----------------------------
  }
}

第 3 步

问题描述:由于第 2 步中调用 fn() 函数的时候,会进入到 get 函数中,所以可以在 get 函数中完成依赖收集
依赖收集是靠 track 函数来完成的

packages/reactivity/src/effect.ts

...

// --------------------------新增 start----------------------------
// 判断是否是在 effect 中访问了对象的属性
export function isTracking() {
  return activeEffect !== undefined
}

// 一个属性可能对应多个 effect,一个 effect 也可能对应多个属性,所以他们两者之间的关系是多对多关系
// 所以数据结构的格式就是 {对象:{属性: [effect1, effect2]}},即上面的 targetMap
// 用来存储 effect 和属性
// 保存的是指定对象到指定属性再到指定 effect 函数的绑定
const targetMap = new WeakMap()
export function track(target, key) {
  // 只有写在 effect 中的属性才需要收集,所以这里判断一下
  if (!isTracking) {
    return
  }
  // 判断 weakmap 中是否已经保存过该对象(target 在 weakmap 中是 key)
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 判断 weakmap 中是否已经保存过当前属性(当前属性在 weakmap 中是 value)
  let dep = depsMap.get(key)
  if (!dep) {
    // 用 Set 类型是为了防止重复
    dep = new Set()
    depsMap.set(key, dep)
  }
  // 判断当前 key 中是否已经有了当前的 effect
  const shouldTrack = dep.has(activeEffect)
  if (!shouldTrack) {
    dep.add(activeEffect)

    // 让 effect 来记录当前属性
    activeEffect.deps.push(dep)
  }
  console.log(targetMap);
}
// --------------------------新增 end----------------------------

packages/reactivity/src/reactive.ts

...

const mutableHandlers:ProxyHandler<Record<any, any>> = {
  // recevier 表示代理对象的本身,即 createdReactiveObject 函数中的 proxy
  get(target, key, recevier) {
    // 判断 target 对象中是否有 __v_isReactive 属性,如果有,则表示该对象被代理过
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true
    }

    // --------------------------新增 start----------------------------
    track(target, key)
    // --------------------------新增 end----------------------------

    // 等价于 target[key]
    // 使用 Reflect 主要是他会对一些边界条件做一些处理,如取不到该属性,同时设置值的时候,还会自动返回 true 或者 false
    const res = Reflect.get(target, key, recevier)
    return res
  },
  ...
}

...

第 4 步

问题描述:上面 track 函数是访问属性,下面 trigger 函数是在设置或修改属性的时候用的。
trigger 函数是完成依赖触发

packages/reactivity/src/effect.ts

...

// --------------------------新增 start----------------------------
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 说明当前修改的对象并没有依赖任何 effect
    return
  }

  const deps = []
  if (key !== undefined) {
    deps.push(depsMap.get(key))
  }

  let effects = []
  for (const dep of deps) {
    effects.push(...dep)
  }

  for (const effect of effects) {
    if (effect !== activeEffect) {
      effect.run()
    }
  }
}
// --------------------------新增 end----------------------------

...

packages/reactivity/src/reactive.ts

const mutableHandlers:ProxyHandler<Record<any, any>> = {
  ...
  set(target, key, value, recevier) {
      // --------------------------新增 start----------------------------
    const oldValue = (target as any)[key]
    // 等价于 target[key] = value,同时会返回 true
    const res = Reflect.set(target, key, value, recevier)
    // 如果值变化了再执行
    if (oldValue !== value) {
      trigger(target, key)
    }
    return res
        // --------------------------新增 end----------------------------
  }
}

packages/reactivity/example/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="../dist/reactivity.global.js"></script>
<script>
  const {reactive, effect} = VueReactivity
  const obj = {
    name: 'xiaoming',
    age: 12
  }
  const state1 = reactive(obj)

  effect(() => {
    console.log(state1.name);
  })

  setTimeout(() => {
    state1.name = 'xiaohong'
  }, 1000)
</script>
</body>
</html>

效果如下:

effect 函数

第 5 步

问题描述:effect 函数需要默认会执行一次,并且,会返回一个函数,还可以手动强制重新调用一下

packages/reactivity/src/effect.ts

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

  _effect.run()

    // --------------------------新增 start--------------------------
  return _effect.run.bind(_effect)
  // --------------------------新增 end----------------------------
}

packages/reactivity/example/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="../dist/reactivity.global.js"></script>
<script>
  const {reactive, effect} = VueReactivity
  const obj = {
    name: 'xiaoming',
    age: 12
  }
  const state1 = reactive(obj)
  const runner = effect(() => {
    console.log(state1.name);
  })
  runner()

</script>
</body>
</html>

效果如下:

effect 函数

第 6 步

问题描述:实现让 effect 停止响应式,让 effect 和属性解耦,如组件销毁的时候就可以用这个

packages/reactivity/src/effect.ts

class ReactiveEffect {
  ...

  // --------------------------新增 start----------------------------
  // 让 effect 和属性解耦
  stop() {
    if (this.active) {
      clearUpEffect(this)
      this.active = false
    }
  }
}


function clearUpEffect(effect) {
  const {deps} = effect
  for (const dep of deps) {
    // 删除属性对应的 effect
    dep.delete(effect)
  }
}
// --------------------------新增 end----------------------------

packages/reactivity/example/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="../dist/reactivity.global.js"></script>
<script>
  const {reactive, effect} = VueReactivity
  const obj = {
    name: 'xiaoming',
    age: 12
  }
  const state1 = reactive(obj)
  const runner = effect(() => {
    console.log(state1.name);
  })
  runner.effect.stop()

  setTimeout(() => {
    state1.name = 'xiaohong'
  }, 1000)
</script>
</body>
</html>

效果如下:

effect 函数

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 查看所有版本


暂无话题~