vue3 调度执行

90 阅读2分钟

vue.png

该功能类似于在Vue.js中连续多次修改响应式数据但只会触发一次更新。Vue.js内部实现了一个更加完善的调度器,思路大致相同。

试例

const data = { foo: 1 }
const obj = new Proxy(data, { /*....*/ })

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

obj.foo++
obj.foo++

输出 1 2 3

字段obj.foo的值一定会从1自增到3,2只是过渡状态。如果我们只关心结果,而不关心过程,那么执行三次打印是多余的,期望打印结果是: 1 3

调度器

// jobQueue 任务队列,利用Set数据结构自动去重能力
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个promise实例,用它将一个任务添加到微任务队列 
const p = Promise.resolve()

let isFlushing = false
function flushJob() {
    if (isFlushing) return
    
    isFlushing = true
    // 在微任务队列中遍历jobQueue队列
    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        isFlushing = false
    })
}

effect(() => {
    console.log(obj.foo)
}, {
    scheduler(fn) {
        jobQueue.add(fn)
        flushJob()
    }
})
obj.foo++
obj.foo++

定义一个 jobQueue 任务队列,Set数据结构,利用Set数据结构自动去重能力
scheduler 调度器,在每次调度执行时,先将当前副作用函数添加到jobQueue队列中,再调用flushJob函数刷新队列。
flushJob函数,通过isFlushing标志判断是否需要执行,在一个周期内只会执行一次。flushJob函数中通过p.then将一个函数添加到微任务队列,并对jobQueue遍历执行。

整段代码效果,因为Set的去重能力,最终jobQueue函数中只会有一项,但flushJob会被同步执行两次,由于isFlushing标志存在。在flushJob函数在一个事件循环内只会执行一次,即微任务也只会执行一次。当微任务开始执行时,宏任务(obj.foo++)值已经是3了,然后输出obj.foo.

整体代码

const bucket = new WeakMap()

function track(target, key) {
    if (!activeEffect) return target[key]
    let depsMap = bucket.get(target)

    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }

    let deps = depsMap.get(key)

    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }

    deps.add(activeEffect) 
}

function trigger(target, key) { 
    const depsMap = bucket.get(target)
    if (!depsMap) return

    const effects = depsMap.get(key)
    
    // effects && effects.forEach(fn => fn())
    // **新增**
    effects && effects.forEach(fn => {
      if (fn.options.scheduler) {
        fn.options.scheduler(fn)
      } else {
        fn()
      }
    })
}

const obj = new Proxy(data, { 
    get(target, key) { 
        track(target, key) 
        return target[key] 
     }, 
     set(target, key, newVal) { 
        target[key] = newVal
        trigger(target, key) 
     } 
})

function effect(fn, options = {}) {
  activeEffect = fn
   // **新增**
  fn.options = options
  fn()
}

effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})

obj.foo++
obj.foo++