vue3源码学习(三)简单的异步更新

220 阅读3分钟

前言

在 上篇文章 vue3源码学习(二)简单的依赖收集和派发更新中实现了简单的vue发布订阅,但是存在一个问题。如果同步多次修改同一个响应式变量的值,则会触发多次该响应式变量订阅的事件,造成性能浪费。修改上篇文章的订阅事件的代码,以更直观的方式展示问题

// 使用示例
const person = reactive({
  name: '张三',
  age: 18,
})
effect(() => {
   document.body.innerText = JSON.stringify(person)
   console.log(person)
})

setTimeout(()=>{
  person.name = '李四'
  person.age = 20
},1000)

此时,我们一开始看到的是页面展示{"name":"张三","age":18}, 控制台打印了1次

{"name":"张三","age":18}

1秒钟之后,页面展示{"name":"李四","age":20}, 控制台打印了3次

{"name":"张三","age":18}
{"name":"李四","age":18}
{"name":"李四","age":20}

问题出来了,页面上只变化了1次,而实际上订阅的事件触发了3次。其中只有第一次和最后一次是有用,中间的几次是没有必要的,造成性能浪费。因此vue引入异步更新来解决这个问题

二、原理

利用js的事件循环机制,将需要执行的订阅的事件去重后放入微任务队列中,这样在同步代码执行完毕后,才会执行订阅的事件

三、实现

let effectQueue = new Set() // 使用 Set 存储需要执行的 effect 函数(去重)
let isFlushing = false // 标识是否正在刷新队列

function queueEffect(effect) {
  effectQueue.add(effect)
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(() => {
      // 在微任务队列中刷新 effectQueue
      effectQueue.forEach((_effect) => _effect())
      effectQueue.clear()
      isFlushing = false
    })
  }
}

四、结合上篇文章的代码

let activeEffect // 建一个全局变量,用于存储当前正在收集依赖的 effect 函数
let effectQueue = new Set() // 使用 Set 存储需要执行的 effect 函数
let isFlushing = false // 标识是否正在刷新队列

function queueEffect(effect) {
  effectQueue.add(effect)
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(() => {
      // 在微任务队列中刷新 effectQueue
      effectQueue.forEach((_effect) => _effect())
      effectQueue.clear()
      isFlushing = false
    })
  }
}

/**
 *  定义一个依赖类,用于存储依赖和派发更新
 */
class Dep {
  constructor() {
    this.effects = new Set()
  }

  /**
   * 依赖收集
   */
  depend() {
    if (activeEffect) {
      this.effects.add(activeEffect)
    }
  }

  /**
   * 派发更新
   */
  notify() {
    this.effects.forEach((effect) => {
      queueEffect(effect)
    })
  }
}

// 所有依赖集合
const targetMap = new WeakMap()

// 收集依赖
function track(target, prop) {
  let depMap = targetMap.get(target)
  if (!depMap) {
    depMap = new Map()
    targetMap.set(target, depMap)
  }
  let dep = depMap.get(prop)
  if (!dep) {
    dep = new Dep()
    depMap.set(prop, dep)
  }
  dep.depend()
}

// 派发更新
function trigger(target, prop) {
  const depMap = targetMap.get(target)
  const dep = depMap.get(prop)
  if (dep) {
    dep.notify()
  }
}

/**
 *  创建一个响应式对象
 * @param {Object} obj
 */
function reactive(obj) {
  const proxy = new Proxy(obj, {
    get(target, prop) {
      const result = Reflect.get(target, prop, proxy)
      track(target, prop)
      return result
    },
    set(target, prop, value) {
      const result = Reflect.set(target, prop, value, proxy)
      trigger(target, prop)
      return result
    },
  })

  return proxy
}

/**
 * 定义一个 effect 函数,用于收集依赖
 * @param {function} fn
 */
function effect(fn) {
  activeEffect = fn
  fn()
  activeEffect = null
}

// 使用示例
const person = reactive({
  name: '张三',
  age: 18,
})
effect(() => {
   document.body.innerText = JSON.stringify(person)
   console.log(person)
})

setTimeout(()=>{
  person.name = '李四'
  person.age = 20
},1000)

此时,我们一开始看到的是页面展示{"name":"张三","age":18}, 控制台打印了1次

{"name":"张三","age":18}

1秒钟之后,页面展示{"name":"李四","age":20}, 控制台打印了2次

{"name":"张三","age":18}
{"name":"李四","age":20}

五、异步更新引发的问题

由于使用异步更新,所以在同步代码中获取的 DOM 还是旧的 DOM

setTimeout(()=>{
  person.name = '李四'
  person.age = 20
  console.log(document.body.innerText)
},1000)

在修改完 person的属性值后,同步获取document.body.innerText, 控制台打印的仍然是{"name":"张三","age":18}

所以vue引入了nextTick,简单模拟实现一下,也是利用js的事件循环机制,将获取dom的操作放入微任务队列中

function nextTick(cb){
  Promise.resolve().then(cb)
}
setTimeout(()=>{
  person.name = '李四'
  person.age = 20
  nextTick(()=>{
    console.log(document.body.innerText)
  })
},1000)

此时控制台打印的就是{"name":"李四","age":20}

五、码上掘金