Vue3 响应式系统实现原理学习总结(一)

158 阅读4分钟

最近,我重新动手实践了《Vue.js设计与实现》这本书中关于响应式系统的部分。其中有一些细节表述可能有所瑕疵,还有代码的实现上,也有值得探讨的细节。整体瑕不掩瑜,作者循序渐进又娓娓道来的文风,让人读过之后如沐春风。

对代码而言,终归还是要实践,我会通过自己的实践版本,来阐述自己对响应式系统的理解,具体代码和书中可能有所出入,以我自己的理解为主。

响应式系统的实现过程

参考小节标题,用我自己的话总结,就是分成11个部分来讲解。

  1. 副作用函数的介绍
  2. 副作用函数和响应式数据的结合
  3. 解决副作用函数硬编码问题
  4. 解决响应式数据字段和副作用函数对应问题
  5. 分支切换问题
  6. effect嵌套问题
  7. 无限循环问题
  8. effect调度器实现
  9. computed的实现
  10. watch的实现
  11. 过期的副作用处理

如果觉得11个部分过于多,可以简单分组。第1点到第4点,可以实现一个相对基础的响应式系统。第5点到第7点,是解决比较难想到的细节问题。第8点到第11点,是主要是computed和watch的实现。接下来,也会按这个分组,来逐步讲解响应式系统的实现。

基础的响应式系统实现

什么是副作用函数

所谓的副作用函数,我的理解,简单来说就是会对函数之外有内容有影响的函数。比如,触发网络请求,触发控制台打印,还有更改了某个全局变量,都算是副作用。

function effect () {
  console.log('hi')
}

副作用函数和响应式数据结合

所谓的副作用函数和响应式数据结合,是说当响应式数据发生变更的时候,副作用函数会触发。这里我们只做一个最简单的实现。

主要使用到Proxy,Set两个API。Proxy用于对象代理,Set数据结构用于副作用函数的收集。

const bucket = new Set
const data = { text: 'hello world' }

const obj = new Proxy(data, {
  get (target, key) {
    bucket.add(effect)
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => {
      fn()
    })
  }
})


function effect () {
  console.log(obj.text)
}

effect()

obj.text = 'changed'

// 打印结果: hello world changed

如上所示,原理很简单,就是Proxy的handler.get用于收集副作用函数,而触发变更的时候,通过Proxy的handler.set触发收集的副作用函数。所以当obj的text变更的时候,会触发effect函数,打印最新的obj.text。

解决副作用函数硬编码问题

副作用函数和响应式数据结合的实现中,可以看到,handler.get收集副作用函数的时候,是硬编码收集effect函数。为了处理硬编码的问题,我们可以通过实现一个注册副作用函数的函数来解决,同时用全局变量activeEffect来存储注册的副作用函数,供handler.get收集。

const bucket = new Set()
const data = { text: 'hello world' }

const obj = new Proxy(data, {
  get(target, key) {
    if (activeEffect) {
      bucket.add(activeEffect)
    }
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => {
      fn()
    })
  }
})


let activeEffect
function effect (fn) {
  activeEffect = fn
  fn()
}

effect(() => {
  console.log(obj.text)
})

obj.text = 'changed'

// 打印结果: hello world changed

解决响应式数据字段和副作用函数对应问题

解决完硬编码问题,还需要考虑响应式数据属性变化,和副作用函数一一对应的问题。现在的实现,还不能做到这一点。任何属性的变更,都会触发和这个响应式数据相关的副作用函数触发。因为只有一个Bucket统一管理所有的副作用。

为了解决这个问题,我们需要重新设计bucket的数据结构,使其支持对应多个响应式数据,且每个副作用函数,都要只是对应上相应的响应式数据属性,只有对应的属性变更,才会触发和这个属性有关联的副作用函数。

可以用WeakMap,Map,Set三种数据结构来设计。WeakMap下面,每一个key是一个响应式数据,value是一个Map,Map里面存储该响应式数据下面属性和对应的副作用Set。

const bucket = new WeakMap()
const data = { text: 'hello world', name: 'Mike' }

const obj = new Proxy(data, {
  get(target, key) {
    let depsMap
    if (!bucket.get(target)) {
      bucket.set(target, new Map())
    }
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) {
      depsMap.set(key, new Set())
    }
    deps = depsMap.get(key)
    if (activeEffect) {
      deps.add(activeEffect)
    }
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    let depsMap
    if (!bucket.get(target)) return
    
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) return
    deps = depsMap.get(key)
    deps = depsMap.get(key)
    deps.forEach(fn => fn())
  }
})

let activeEffect

function effect (fn) {
  activeEffect = fn
  fn()
}
effect(() => {
  console.log(obj.text)
})
obj.text = 'changed'
obj.name = 'John'

// 打印结果: hello world changed

经过改造,可以看到,即使obj.name变更,也不会触发到只涉及obj.text的副作用函数触发。

bucket之所以要用WeakMap,是可以更加便捷解除没有引用的依赖,当响应式对象没有引用的时候,会被垃圾回收。如果没有用WeakMap而使用Map,这个回收的动作,就需要手动实现,不方便。