侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vue3源码学习-5-分支切换

蜗牛
2022-06-11 / 0 评论 / 0 点赞 / 8 阅读 / 6951 字 / 正在检测是否收录...

前言

上篇回顾,核心代码逻辑是通过reactive中的Proxy()来代理一个对象,然后通过get收集依赖,主要操作放在来effect中。那么当我们回顾上一篇的问题。当用户有一个这样当操作

const {effect,reactive} = VueReactivity;
let target = {name:"david",age:12,address:{num:567},flag:true}
const state = reactive(target)
effect(() => {
    console.log("render")
    document.getElementById("app").innerHTML = state.flag ? "姓名:"+state.name : "年龄:"+state.age
    })
setTimeout(() => {
  state.flag = false
  setTimeout(() => {
    console.log("修改了name,原则不重新渲染")
    state.name = "jack"
  },1000)
},1000)

第一次,执行来用户的渲染操作,然后在之后的操作中修改来flag。这个时候,依赖收集的应该是flag和name,如果采用上篇中的代码,那么实际上,旧的name依赖未被清除,还是会留在deps中,那么你修改name的时候会触发渲染。

effect 分支删除

上面的问题已经很清晰来,那么如果解决呢。可以在用户函数执行之前,把旧的依赖全部清空,再收集一次这个依赖不就行了。这样第一次收集了flag,name依赖。第二次flag变成flase,清空依赖,收集flag和age以来,这样第三次修改name值的时候就不会触发渲染了。
前一篇中定义的deps也派上了用场,由于之前做了双向收集,那么在执行用户操作之前,清空依赖就行了。
定义一个clearupEffect()函数。

function clearupEffect(effect: ReactiveEffect) {
  let { deps } = effect
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect)
  }
  effect.deps.length = 0
}

放入到 this.fn()被执行之前,来清除依赖。但是这里会有个新问题,看代码。

8f376f8a3e3126b04ef5d.png

这里执行了清空,下面执行了this.fn()又会触发渲染,然后由于使用的是Set()来存储关系的, Set()一边清空一边添加依赖,导致了死循环,会一直触发渲染。为此依赖触发的方法要进行修改,我们拷贝一份Set(),然后在他的基础上删除清空,这样就不会造成死循环了。

// 此处做逻辑修改,因为set在删除之后,再做添加,那么会造成死循环,有些方法会对数据拷贝之后再做修改
  // 可以避免这个问题
  if (effects) {
    effects = new Set(effects)
    effects.forEach((effect) => {
      if (activeEffect !== effect) effect.run() // 如果这里直接就写effect.run(),那么会遇到这种情况,在模版中赋值,那么也会触发这个,
      // 然后又通过了依赖收集的时候,运行它的第一次run()。就会导致循环调用,爆栈,
      //所以这里需要加一个判断是否是当前的effect,如果是的话,就忽略这一次的赋值触发的run();
      //注意目前的代码是不支持异步的
    })
  }

完整的effect代码

export let activeEffect = undefined

function clearupEffect(effect: ReactiveEffect) {
  let { deps } = effect
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect)
  }
  effect.deps.length = 0
}

class ReactiveEffect {
  // 这里代表在实例上新增active属性
  public active = true // 这个effect默认是激活状态
  public parent = null // 记录当前effect的父亲是谁,用作返回
  public deps = [] // 记录当前的effect都记录了哪些属性
  constructor(public fn) {} // 用户传递的参数也会绑定在this上 相当于this.fn = fn;
  run() {
    // run就是执行effect
    if (!this.active) {
      // 如果是非激活状态就是非激活状态,只需要执行函数,不需要进行依赖收集
      this.fn()
    }
    // 这里就要依赖收集了,核心就是当前的effect和稍后渲染的属性关联在一起
    try {
      this.parent = activeEffect
      activeEffect = this

      //在执行用户函数之前把依赖清空,再次收集
      clearupEffect(this)

      return this.fn() // 当稍后调用取只操作的时候就可以获取到这个全局的activeEffect了
    } finally {
      activeEffect = this.parent
    }
  }
}

export function effect(fn) {
  // 这里的fn可以根据状态的变化,重新执行,effect可以嵌套着写
  const _effect = new ReactiveEffect(fn) //创建响应式的effect
  _effect.run() //默认先执行一次
}

// 实例代码
// effect(() => { age =>  e1
//   state.age;

//   effect(() => { name => e2
//     stage.name;
//   })

//   stage.name; name => e1
// })
// 以前呢vue3.0的时候采用栈的方法将对象压栈,然后执行完成之后弹出这样就能关联
// 对应的effect
// 现在的做法是记录effect的父亲是谁,这样每次执行之后就把activeEffect 赋值为父亲对象
let targetMap = new WeakMap()
export function track(target, type, key) {
  // 在effect中的回调函数中,我们通过语句中执行的target属性收集到effect
  // 那么就有了target属性指到哪个effect,
  // 那么我们就明确了对象 某个属性-> 多个effect
  // 对象作为key,那么第一眼想到WeakMap,并且它还有个好处,当value为空的时候会被
  // 垃圾回收机制会回收它
  // 那么上述的数据结构应该是 {对象:Map{name:Set}}
  if (!activeEffect) return // 如果你不是在模版中触发了get,那么这个依赖就不要收集
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  let shouldTrack = !dep.has(activeEffect) //一个属性多次依赖同一个effect那么去重
  if (shouldTrack) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep) // 让deps记录住对应的dep,稍后在清理的地方用到
  }
  // 这里单向收集了这个依赖,对象的属性->effect
  // 但是这样不方便。例如你有这么一个模版渲染
  // effect(() => {flag ? state.age : state.name})
  // 那么在你flag判断为true和false的时候依赖的关联是不一样的
  // 所以我们也需要收集effect -> 属性
  // 在 ReactiveEffect上添加一个数组,来收集当前effect记录了哪些属性
}

export function trigger(target, type, key, value, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return //触发的值不在模版中
  let effects = depsMap.get(key)

  // 此处做逻辑修改,因为set在删除之后,再做添加,那么会造成死循环,有些方法会对数据拷贝之后再做修改
  // 可以避免这个问题
  if (effects) {
    effects = new Set(effects)
    effects.forEach((effect) => {
      if (activeEffect !== effect) effect.run() // 如果这里直接就写effect.run(),那么会遇到这种情况,在模版中赋值,那么也会触发这个,
      // 然后又通过了依赖收集的时候,运行它的第一次run()。就会导致循环调用,爆栈,
      //所以这里需要加一个判断是否是当前的effect,如果是的话,就忽略这一次的赋值触发的run();
      //注意目前的代码是不支持异步的
    })
  }
}
git:[@github/MicroMatrixOrg/vue3-plan/tree/effect_schedule]
0

评论区