第四章、响应系统的作用与实现

副作用函数

改变obj的时候希望effect函数能自动执行。

const obj = { text: 'hello world' }
function effect() {
  document.body.innerText = obj.text
}
1
2
3
4

执行effect函数的时候会触发obj.text的读取操作,修改obj.text的时候会触发obj.text的设置操作。所以可以在读取obj.text的时候找个地方把effect函数存起来,修改obj.text的时候再把存起来的effect函数取出来执行。

Vue3 使用 Proxy 来实现对象读写操作的代理。

// 副作用函数
function effect() {
  document.body.innerText = obj.text
}
// 存储副作用函数
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, newValue) {
    target[key] = newValue
    // 取出副作用函数执行
    bucket.forEach((fn) => fn())
    return true
  },
})

// 修改数据可以发现浏览器内容变化
effect()
setTimeout(() => {
  obj.text = 'hello vue3'
}, 1000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

目前副作用函数的名称被写死为effect无法修改,我们希望即使传入的是一个匿名函数也能被正确收集。

// 定义一个全局变量来储存被注册的副作用函数
let activeEffect
// effect修改为对副作用函数进行注册
function effect(fn) {
  activeEffect = fn
  fn()
}
// 存储副作用函数
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, newValue) {
    target[key] = newValue
    // 取出副作用函数执行
    bucket.forEach((fn) => fn())
    return true
  },
})

// 修改数据可以发现浏览器内容变化
effect(() => {
  document.body.innerText = obj.text
})
setTimeout(() => {
  obj.text = 'hello vue3'
}, 1000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

现在解决了副作用函数名称的问题,但是我们还没有在副作用函数与被操作的目标字段之间建立联系,也就是说即使修改了 obj 上面的其他字段,副作用函数也会被执行,需要将obj.text字段和调用过它的副作用函数联系起来。

// 定义一个全局变量来储存被注册的副作用函数
let activeEffect
// effect修改为对副作用函数进行注册
function effect(fn) {
  activeEffect = fn
  fn()
}
// 使用WeakMap存储副作用函数,key为target对象,值为键名key和键名key对应的副作用函数组成的Map类型
const bucket = new WeakMap()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据进行代理
const obj = new Proxy(data, {
  // 代理读取操作
  get(target, key) {
    // 没有副作用函数可以直接返回值
    if (!activeEffect) return target[key]
    // 取出当前target的副作用函数Map
    let depsMap = bucket.get(target)
    // 如果没有副作用函数Map,则新建一个关联target
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    // 再根据当前key,从副作用函数Map取出对应的最终储存副作用函数的Set
    let deps = depsMap.get(key)
    // 如果没有副作用函数,则新建一个并关联当前key
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    // 最终将当前副作用函数添加到Set中
    deps.add(activeEffect)
    return target[key]
  },
  // 代理设置操作
  set(target, key, newValue) {
    target[key] = newValue
    // 从WeakMap中根据当前target取出副作用函数的Map
    const depsMap = bucket.get(target)
    // 没有则不需要执行
    if (!depsMap) return true
    // 从副作用函数的Map中根据当前key取出最终副作用函数Set执行
    const effects = depsMap.get(key)
    effects && effects.forEach((fn) => fn())
    return true
  },
})

// 修改数据可以发现浏览器内容变化
effect(() => {
  document.body.innerText = obj.text
})
setTimeout(() => {
  obj.text = 'hello vue3'
  // 修改不相关的值副作用函数不会执行
  // obj.asd = 'hello vue3'
}, 1000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

提取一下 get 和 set 操作中操作副作用函数的逻辑,提高灵活性。

// 对原始数据进行代理
const obj = new Proxy(data, {
  // 代理读取操作
  get(target, key) {
    // 封装
    track(target, key)
    return target[key]
  },
  // 代理设置操作
  set(target, key, newValue) {
    target[key] = newValue
    // 封装
    trigger(target, key)
    return true
  },
})

// 追踪变化
function track(target, key) {
  // 没有副作用函数直接return
  if (!activeEffect) return
  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())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

读取代码可能会存在不同的分支情况,例如:

effect(() => {
  // 当obj.ok为false时,并不会触发obj.text的读取,也就不需要对副作用函数做收集。
  document.body.innerText = obj.ok ? obj.text : 'not'
})

// 修改obj.ok为false
obj.ok = false
// 再修改obj.text的值,理论上副作用函数不需要执行,但是现在依然会执行
obj.text = 'hello vue3'
1
2
3
4
5
6
7
8
9

所以需要在每次副作用函数执行的时候,先将其从所有与它关联的依赖集合中删除,要删除的话,就需要明确知道哪些依赖集合中包含这个副作用函数。

// 对effect函数进行修改,在内部定义副作用函数,并给他关联依赖数组
function effect(fn) {
  const effectFn = () => {
    // 清除关联的依赖
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
  }
  // 定义一个数组来储存与当前副作用函数关联的依赖
  effectFn.deps = []
  // 初始化执行
  effectFn()
}

// 在track中对副作用函数的依赖进行收集
function track(target, key) {
  if (!activeEffect) return
  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)

  // deps中add了activeEffect,表示deps就是activeEffect的一个依赖集合
  // 所以将deps push到activeEffect的依赖集合数组中,互相引用
  activeEffect.deps.push(deps)
}

function cleanup(effectFn) {
  // 遍历依赖deps数组,将effectFn从这些依赖中删除
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  // 删除后重置effectFn.deps数组
  effectFn.deps.length = 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

执行后会发现无限循环,原因出在trigger函数中。

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  // 这里遍历了effects集合,因为cleanup函数中会对该Set集合中的副作用函数进行删除
  // 但是副作用函数的执行又会触发track操作,将其重新添加到effects集合中。
  // 这样就会造成Set集合不断的执行删除添加操作,造成无限循环
  effects && effects.forEach((fn) => fn())
}

// 理论上导致无限循环的原因可以总结为这样的代码
// const set = new Set([1])
// set.forEach((item) => {
//   set.delete(1)
//   set.add(1)
//   console.log('无限循环。。。')
// })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

解决方法是构造一个新的 Set 集合来代替原来的 Set 进行遍历操作。

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

  // 构造一个新的Set进行遍历
  const effectsToRun = new Set()
  effects && effects.forEach((fn) => effectsToRun.add(fn))
  effectsToRun.forEach((fn) => fn())
}
1
2
3
4
5
6
7
8
9
10

effect 副作用函数还会存在嵌套的情况,例如:

effect(() => {
  effect(() => {
    document.body.innerText = obj.ok ? obj.text : 'not'
  })
})
1
2
3
4
5

在 Vue.js 中,渲染函数其实是在一个 effect 中执行的,当组件发生嵌套时,就会出现 effect 的嵌套。嵌套就会出现副作用函数错乱的情况,例如:

let temp1, temp2
effect(() => {
  console.log('1执行')
  effect(() => {
    console.log('2执行')
    temp2 = obj.bar
  })
  temp1 = obj.foo
})

// 修改obj.foo应该执行外层副作用函数,输出 1执行,但是却输出了 2执行,副作用函数执行出现了错乱
obj.foo = 'xxx'
1
2
3
4
5
6
7
8
9
10
11
12

原因出在 effect 函数的定义中

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    // activeEffect变量储存当前注册的副作用函数,意味着同一时间只能存在一个注册的副作用函数
    // 副作用函数一旦出现嵌套的情况,内层的副作用函数就会把外层的给覆盖掉。此时收集依赖的话,收集到的都会是内层副作用函数
    activeEffect = effectFn
    fn()
  }
  effectFn.deps = []
  effectFn()
}
1
2
3
4
5
6
7
8
9
10
11

解决这个问题需要定义一个副作用函数后进先出执行栈,并且保证全局变量activeEffect始终指向栈顶的副作用函数。

let activeEffect
// 副作用执行栈
const effectStack = []
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    // 调用副作用函数之前,进行入栈操作
    effectStack.push(effectFn)
    fn()
    // 执行完毕后将当前副作用函数弹出栈,并改变 activeEffect 的指向
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.deps = []
  effectFn()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

当出现类似obj.a++的操作时,会引起栈溢出,因为obj.a++等价于obj.a = obj.a + 1,这条语句同时访问了obj.a和设置了obj.a,就会导致副作用函数的无限递归调用。 解决方法为:如果 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((fn) => {
      // trigger触发的副作用函数和当前正在注册的副作用函数相同的话就不执行
      if (fn !== activeEffect) {
        effectsToRun.add(fn)
      }
    })
  effectsToRun.forEach((fn) => fn())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

为副作用函数添加可调度性,可以在 trigger 触发副作用函数执行的时候,决定副作用函数执行的时机。

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 将外部传入的配置挂载到副作用函数上
  effectFn.options = options
  effectFn.deps = []
  effectFn()
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set()
  effects &&
    effects.forEach((fn) => {
      if (fn !== activeEffect) {
        effectsToRun.add(fn)
      }
    })
  effectsToRun.forEach((fn) => {
    // 当用户传入了自定义调度器时,执行用户的调度器
    if (fn.options.scheduler) {
      fn.options.scheduler(fn)
    } else {
      fn()
    }
  })
}

effect(
  () => {
    console.log(obj.bar)
  },
  {
    scheduler(fn) {
      // 副作用函数添加到宏任务中执行
      setTimeout(fn)
    },
  }
)
// 会输出1 结束 2
obj.bar++
console.log('结束')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

在 Vue.js 中连续多次修改响应式数据,只会触发一次更新,利用调度器可以实现这一点。

// 定义任务队列,使用Set结构自动去重复任务
const jobQueue = new Set()
// 定义微任务,用来将任务添加到微任务队列
const p = Promise.resolve()

// 当前是否正在执行任务的标识
let isFlusing = false
function flushJob() {
  // 同一时间只能执行一个任务
  if (isFlusing) return
  isFlusing = true

  // 执行微任务队列中的所有任务
  p.then(() => {
    jobQueue.forEach((job) => job())
  }).finally(() => {
    // 结束后修改标识符
    isFlusing = false
  })
}

// 修改数据可以发现浏览器内容变化
effect(
  () => {
    console.log(obj.bar)
  },
  {
    // 使用调度器,自定义执行函数
    scheduler(fn) {
      // 将当前任务添加到任务队列
      jobQueue.add(fn)
      // 执行任务
      flushJob()
    },
  }
)

// 执行两次自增操作,因为副作用函数是同一个,利用Set自动去重,所以只有一个副作用函数会进入到Set中。
// 调度器会执行两次,同样的flushJob函数也会执行两次,但是由于isFlusing的存在,之后的遍历微任务队列在浏览器一次事件循环中只会执行一次。
// 最终输出console.log(obj.foo)的时候,obj.foo已经完成了两次自加,只会输出最初的1和最终值3
obj.bar++
obj.bar++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

computed 计算属性

综上所有内容,可以实现计算属性 computed。先实现支持懒执行的 effect。

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非执行的时候才执行
  if (!options.lazy) {
    effectFn()
  }
  // 将副作用函数返回
  return effectFn
}

const effectFn = effect(
  () => {
    console.log(obj.bar)
  },
  {
    // 懒执行配置
    lazy: true,
  }
)

// 可以手动调用 effectFn的执行
effectFn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

将 effectFn 内部的 fn 计算结果进行返回,可以得到副作用函数的计算结果

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 储存计算结果
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 返回计算结果
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非执行的时候才执行
  if (!options.lazy) {
    effectFn()
  }
  // 将副作用函数返回
  return effectFn
}

const effectFn = effect(() => obj.bar + obj.foo, {
  lazy: true,
})

// 执行后可以得到 obj.bar + obj.foo 的计算结果
const value = effectFn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

实现计算属性 computed

function computed(getter) {
  // 定义一个懒执行的副作用函数
  const effectFn = effect(getter, {
    lazy: true,
  })

  // 定义一个储存结果的对象,他的value属性是一个getter
  const obj = {
    // 读取value属性的时候会执行effectFn
    get value() {
      return effectFn()
    },
  }
  return obj
}

const sum = computed(() => obj.bar + obj.foo)
// 访问value属性,会返回副作用函数的执行结果
console.log(sum.value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

目前还无法缓存计算属性,每次调用sum.value都会重新计算结果,需要添加缓存功能。

function computed(getter) {
  // 储存计算结果
  let value
  // 定义是否需要重新计算的标识
  let dirty = true

  // 定义一个懒执行的副作用函数
  const effectFn = effect(getter, {
    lazy: true,
    // 利用调度器属性,在依赖变化的时候,调度器会执行,会将标识符重新改为true
    scheduler() {
      dirty = true
    },
  })

  // 定义一个储存结果的对象,他的value属性是一个getter
  const obj = {
    // 读取value属性的时候会执行effectFn
    get value() {
      // 只有当标识符为true的时候才需要进行计算
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      // 返回计算结果
      return value
    },
  }
  return obj
}

const sum = computed(() => {
  console.log(123)
  return obj.bar + obj.foo
})
// 多从调用计算数学,只会打印一次console.log(123)
console.log(sum.value)
console.log(sum.value)
console.log(sum.value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

目前还存在一个情况,就是当计算属性被其他副作用函数调用时,修改原数据,副作用函数不会执行。

const sum = computed(() => {
  return obj.bar + obj.foo
})

// 其他副作用函数引用计算属性
effect(() => {
  console.log(sum.value)
})

// 修改原数据,上面的副作用函数不会重新执行
obj.bar++
1
2
3
4
5
6
7
8
9
10
11

因为计算属性内部有一个懒执行的副作用函数,出现了副作用函数嵌套的情况,obj.bar收集到的是内部被懒执行的副作用函数,所以外层调用计算属性的副作用函数不会重新执行,需要手动执行来触发响应。

function computed(getter) {
  let value
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true
      // 手动调用触发响应
      trigger(obj, 'value')
    },
  })

  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      // 手动调用追踪value属性
      track(obj, 'value')
      return value
    },
  }
  return obj
}

const sum = computed(() => {
  return obj.bar + obj.foo
})

// 其他副作用函数引用计算属性
effect(() => {
  console.log(sum.value)
})

// 副作用函数能执行
obj.bar++
obj.bar++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

watch 侦听属性

实现侦听属性 watch

function watch(source, cb) {
  // 触发读取操作,从而建立属性和副作用函数之间的联系
  effect(() => source.foo, {
    // 传入自定义调度函数,触发时执行
    scheduler() {
      cb()
    },
  })
}

watch(obj, () => {
  console.log(123)
})

obj.foo++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

目前硬编码了 foo 属性,要让 watch 具有通用性,需要封装一个通用的函数来对属性进行读取。

function watch(source, cb) {
  // 调用通用读取函数对侦听的原数据进行读取
  effect(() => traverse(source), {
    scheduler() {
      cb()
    },
  })
}

function traverse(value, seen = new Set()) {
  // 原始值和已经读取过的值不需要读取
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  // 将当前值添加到Set数据结构中,表示已经读取过,防止重复读取
  seen.add(value)
  // 递归读取所有属性
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}
watch(obj, () => {
  console.log(123)
})

obj.foo++
obj.bar++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

watch 侦听的对象可以是一个 getter

function watch(source, cb) {
  // 定义getter函数
  let getter
  if (typeof source === 'function') {
    // 传入的是个函数的话,表示传入的是getter
    getter = source
  } else {
    // 否则执行之前的操作
    getter = () => traverse(source)
  }
  // 执行定义好的getter
  effect(() => getter(), {
    scheduler() {
      cb()
    },
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

watch 还可以获取变化前后的值,利用 effect 函数的 lazy 选项可以实现这一点。

function watch(source, cb) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  // 定义新旧值
  let oldValue, newValue
  // 储存lazy模式下返回的副作用函数
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler() {
      // 调度函数执行时计算新值
      newValue = effectFn()
      // 新旧值传入回调
      cb(newValue, oldValue)
      // 更新旧值
      oldValue = newValue
    },
  })
  // 初始化计算旧值
  oldValue = effectFn()
}

watch(
  () => obj.foo,
  (newValue, oldValue) => {
    console.log(newValue, oldValue)
  }
)

// 输出 3, 2
obj.foo++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

watch 原本只会在依赖改变后才执行,Vue.js 中 watch 可以通过传入 immediate 属性来指定回调函数是否需要立即执行。

// 新增配置参数options
function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  // 提取调度函数
  const job = () => {
    newValue = effectFn()
    // 初次执行时没有旧值,oldValue为undefined
    cb(newValue, oldValue)
    oldValue = newValue
  }

  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: job,
  })

  // 传入immediate时立即执行调度函数计算结果,否则计算旧值
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

watch(
  () => obj.foo,
  (newValue, oldValue) => {
    console.log(newValue, oldValue)
  },
  {
    immediate: true,
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

回调函数执行的时机可以自定义,改为异步执行。

function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  const job = () => {
    newValue = effectFn()
    cb(newValue, oldValue)
    oldValue = newValue
  }

  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler() {
      // 传入flush为post时,将其放到微任务队列中执行
      if (options.flush === 'post') {
        const p = Promise.resolve()
        p.then(job)
      } else {
        job()
      }
    },
  })

  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

watch(
  () => obj.foo,
  (newValue, oldValue) => {
    console.log(newValue, oldValue)
  },
  {
    flush: 'post',
  }
)

obj.foo++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

当 watch 侦听的回调函数是个异步函数时,会出现竞态的情况,即后执行的回调函数比先执行的回调函数更快的返回了,导致先执行的回调函数的执行结果无效了,需要一个表示过期的变量防止先执行的回调函数的返回结果把后执行的回调函数的返回结果给覆盖了。

function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue, cleanup
  // 定义过期回调的注册函数
  function onInvalidate(fn) {
    cleanup = fn
  }

  const job = () => {
    newValue = effectFn()
    // 存在过期回调的时候执行过期回调
    if (cleanup) {
      cleanup()
    }
    // 传入过期回调的注册函数
    cb(newValue, oldValue, onInvalidate)
    oldValue = newValue
  }

  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler() {
      if (options.flush === 'post') {
        const p = Promise.resolve()
        p.then(job)
      } else {
        job()
      }
    },
  })

  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

watch(
  () => obj.foo,
  async (newValue, oldValue, onInvalidate) => {
    // 定义过期锁,false表示未过期
    let expired = false

    // 注册过期回调,如果过期了会执行过期回调将过期锁置为true
    onInvalidate(() => {
      expired = true
    })

    // 某些异步操作
    const res = await Promise.resolve(123)

    // 只有未过期的时候才执行回调
    if (!expired) {
      console.log(newValue, oldValue, res)
    }
  }
)

// 只会输出第二次obj.foo++的结果,因为第一次的过期了
obj.foo++
obj.foo++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68