使副作用函数可调度执行

调度执行

为什么要调度执行

可调度性是响应式系统的重要特性。所谓可调度性,指的是trigger动作触发副作用函数重新执行的时候,有能力决定副作用函数执行的时机、次数以及方式。

如何决定副作用函数执行方式

以下面代码为例:

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

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

obj.foo++

console.log('结束了')

输出结果如下:

1
2
'结束了'

现在假设需求有变,输出顺序需要调整为:

1
'结束了'
2

实现思路

在不修改编码顺序的情况下实现需求,这时候就需要可调度执行。
那么我们可以设计一个选项参数options,允许用户指定调度器:

effect(
  () => {
    console.log(obj.foo)
  },
  // options
  {
    // 调度器 scheduler 是一个函数
    scheduler(fn) {
      // ...
    }
  }
)

effect函数中,可以传递第二个参数options,他是一个对象,其中允许指定一个 scheduler 调度函数,同时在effect函数内部我们把options挂载在副作用函数的options选项上:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 将 options 挂载到 effectFn 上
  effectFn.options = options  // 新增
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

那么有了调度函数,我们在trigger函数中触发副作用函数执行时,可以调用用户传递的调度器函数,从而把控制权交给用户:

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

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {  // 新增
      effectFn.options.scheduler(effectFn)  // 新增
    } else {
      // 否则直接执行副作用函数(之前的默认行为)
      effectFn()  // 新增
    }
  })
}

在遍历执行副作用函数时,我们应当判断当前执行的副作用函数是否有调度器,如果存在,需直接调用调度器,把当前副作用函数当作参数传进去调度器中。那么此时,可以实现上边的需求了:

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

effect(
  () => {
    console.log(obj.foo)
  },
  // options
  {
    // 调度器 scheduler 是一个函数
    scheduler(fn) {
      // 将副作用函数放到宏任务队列中执行
      setTimeout(fn)
    }
  }
)


obj.foo++

console.log('结束了')

用setTimeOut将副作用函数推入宏任务队列,那么此时可以得到期望的输出顺序了:

1
'结束了'
2

决定函数的执行次数

当我们运行如下代码:

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

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

obj.foo++
obj.foo++

由于做了两次自增操作,如果没有调度器,那么会连续输出两次:

1
2
3

但是从结果可知,foo的最初状态是1,foo的最终状态是3,那么中间的2可以看作过渡状态。那么我们期望的输出结果是:

1
3

实现思路

其实还是基于调度器:

// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return
  // 设置为 true,代表正在刷新
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    // 结束后重置 isFlushing
    isFlushing = false
    jobQueue.clear();  // 清空 jobQueue 避免下一次调度的时候执行上次遗留的副作用函数
  })
}


effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    // 每次调度时,将副作用函数添加到 jobQueue 队列中
    jobQueue.add(fn)
    // 调用 flushJob 刷新队列
    flushJob()
  }
})

obj.foo++
obj.foo++

其中定义一个jobQueue的集合,集合的去重特性,在同一轮事件循环内加入相同的函数时,会被去重,只保留一个副作用函数。那么在这个调度设置中,宏任务的每一次设置都不会立刻执行副作用函数,而是统一等到清空微任务队列时再遍历执行副作用函数。虽然有一定的延迟,但是正是这个延迟,正好避免了中间过渡状态时不需要的重复的副作用函数调用。

另一个关键在于控制位isFlushing,用于阻止正在刷新队列函数的重复执行。

这个例子就是调度器的另一种应用,本质上,响应式的各种额外的实现都是基于使用调度器传入自定义函数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值