这是 《vue3 源码学习,实现一个 mini-vue》
系列文章 响应式模块 的最后一章,在前面几章我们分别介绍了 reactive
、ref
以及 computed
这三个方法,阅读了 vue
源码并且实现了它们,那么本章我们最后来实现一下 watch
吧~
我们可以点击 这里 来查看 watch
的官方文档。
watch
的实现和 computed
有一些相似的地方,但是作用却与 computed
大有不同。watch
可以监听响应式数据的变化,从而触发指定的函数。
我们直接从下面的代码开始 vue
源码调试:
js<script>
const { reactive, watch } = Vue
const obj = reactive({
name: '张三'
})
watch(obj, (value, oldValue) => {
console.log('watch 监听被触发')
console.log('value', value)
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
</scri
以上代码分析:
reactive
函数构建了响应性的实例watch
proxy
的 setter
摒弃掉之前熟悉的 reactive
,我们从 watch
函数开始源码跟踪:
packages/runtime-core/src/apiWatch.ts
中找到 watch
函数,开始 debugger
:source
cb
options
,最后返回并调用了 doWatch
,我们进入到 doWatch
:doWatch
方法代码很多,上面有一些警告打印的 if
,我们直接来到第 207
行。因为 source
为 reactive
类型数据,所以会执行 getter = () => source
,目前 source
为 proxy
实例,即:getter = () => Proxy{name: '张三'}
。紧接着,指定 deep = true
,即:source
为 reactive
时,默认添加 options.deep = true
。我们继续调试 doWatch
这个方法:if (cb && deep)
,条件满足:创建新的常量 baseGetter = getter
,我们继续调试 doWatch
这个方法:执行 let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
,将 INITIAL_WATCHER_VALUE
赋值给 oldValue
,INITIAL_WATCHER_VALUE = {}
执行 const job: SchedulerJob = () => {...}
,我们知道 Scheduler
是一个调度器,SchedulerJob
其实就是一个调度器的处理函数,在之前我们接触了一下 Scheduler
调度器,但是并没有进行深入了解,那么这里将涉及到调度器的比较复杂的一些概念,所以后面我们想要实现 watch
,还需要 深入的了解下调度器的概念,现在我们暂时先不需要管它。我们继续调试 doWatch
这个方法:
let scheduler: EffectScheduler = () => queuePreFlushCb(job)
,这里通过执行 queuePreFlushCb
函数,将上一步的 job
作为传参,来得到一个完整的调度器函数 scheduler
。我们继续调试 doWatch
这个方法:代码继续执行得到一个 ReactiveEffect
的实例,注意: 该实例包含一个完善的调度器 scheduler
,接着调用了 effect
的 run
方法,实际上是调用了 getter
方法,获取到了 oldValue
,最后返回一个回调函数。
至此 watch 函数的逻辑执行完成。
总结:
watch
函数的代码很长,但是逻辑还算清晰scheduler
在 watch
中很关键scheduler
、ReactiveEffect
两者之间存在互相作用的关系,一旦 effect
触发了 scheduler
那么会导致 queuePreFlushCb(job)
执行job()
触发,那么就表示 watch
触发了一次等待两秒,reactive
实例将触发 setter
行为,setter
行为的触发会导致 trigger
函数的触发,所以我们可以直接在 trigger
中进行 debugger
packages/reactivity/src/effect.ts
中找到 trigger
,进行 debugger
:trigger
最终会触发到 triggerEffect
,所以我们可以 省略中间 步骤,直接进入到 triggerEffect
中:triggerEffect
:scheduler
存在,所以会直接执行 scheduler
,即等同于直接执行 queuePreFlushCb(job)
。所以接下来我们 进入 queuePreFlushCb
函数,看看 queuePreFlushCb
做了什么:queueCb(cb, ..., pendingPreFlushCbs, ...)
函数,此时 cb = job
,即:cb()
触发一次,意味着 watch
触发一次,进入 queueCb
函数:pendingQueue.push(cb)
,pendingQueue
从语义中看表示 队列 ,为一个 数组,接着执行了 queueFlush
函数,我们进入 queueFlush()
函数:queueFlush
函数内部做了两件事:1. 执行了 isFlushPending = true
isFlushPending
是一个 标记,表示 promise
进入 pending
状态。2. 通过 Promise.resolve().then()
这样一种 异步微任务的方式 执行了 flushJobs
函数,
flushJobs
是一个 异步函数,它会等到 同步任务执行完成之后 被触发,我们可以 给 flushJobs
函数内部增加一个断点
至此整个 trigger
就执行完成
总结:
trigger
的执行核心是触发了 scheduler
调度器,从而触发 queuePreFlushCb
函数queuePreFlushCb
函数主要做了以下几点事情:
pendingQueue
Promise.resolve().then
把 flushJobs
函数扔到了微任务队列中同时因为接下来 同步任务已经执行完成,所以 异步的微任务 马上就要开始执行,即接下来我们将会进入 flushJobs
中。
flushJobs
函数代码:flushPreFlushCbs(seen)
函数,这个函数非常关键,我们来看一下:通过截图代码可知,pendingPreFlushCbs
为一个数组,其中第一个元素就是 job
函数(通过 2.2 watch 函数
第 4 步
下面的截图可以看到传参)
执行 for
循环,执行 activePreFlushCbs[preFlushIndex]()
,即从 activePreFlushCbs
这个数组中,取出一个函数,并执行(就是 job 函数!)
到这里,job
** 函数被成功执行**,我们知道 job
执行意味着 watch
执行,即当前 watch
的回调 即将被执行
总结:
flushJobs
的主要作用就是触发 job
,即:触发 watch
job
的执行函数,执行 const newValue = effect.run()
,此时 effect
为 :run
,本质上是执行 fn
,而 traverse(baseGetter())
即为 traverse(() => Proxy{name: 'xx'})
,结合代码获取到的是 newValue
,所以我们可以大胆猜测,测试 fn
的结果等同于:`fn: () => ({name: '李四'})。 接下来执行:callWithAsyncErrorHandling(cb ......):fn
的值为 watch
的第二个参数 cb
。接下来执行 callWithErrorHandling(fn ......)
。这里的代码就比较简单了,其实就是触发了 fn(...args)
,即:watch
的回调被触发,此时 args
的值为:watch
的回调终于 被触发了。总结:
job
函数的主要作用其实就是有两个:
newValue
和 oldValue
fn
函数执行到目前为止,整个 watch 的逻辑就已经全部理完了。整体氛围了四大块:
watch
函数本身reactive
的 setter
flushJobs
job
整个 watch
还是比较复杂的,主要是因为 vue
在内部进行了很多的 兼容性处理,使代码的复杂度上升了好几个台阶,我们自己去实现的时候 会简单很多 的。
经过了 computed
的代码和 watch
的代码之后,其实我们可以发现,在这两块代码中都包含了同样的一个概念那就是:调度器 scheduler
。完整的来说,我们应该叫它:调度系统
整个调度系统其实包含两部分实现:
lazy
:懒执行scheduler
:调度器懒执行相对比较简单,我们来看 packages/reactivity/src/effect.ts
中第 183 - 185 行的代码:
tsif (!options || !options.lazy) {
_effect.run()
}
这段代码比较简单,其实就是如果存在 options.lazy 则 不立即 执行 run 函数。
我们可以直接对这段代码进行实现:
tsexport interface ReactiveEffectOptions {
lazy?: boolean
scheduler?: EffectScheduler
}
/**
* effect 函数
* @param fn 执行方法
* @returns 以 ReactiveEffect 实例为 this 的执行函数
*/
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
// 生成 ReactiveEffect 实例
const _effect = new ReactiveEffect(fn)
// !options.lazy 时
if (!options || !options.lazy) {
// 执行 run 函数
_effect.run()
}
}
那么此时,我们就可以新建一个测试案例来测试下 lazy
,创建 packages/vue/examples/reactivity/lazy.html
:
html<script>
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
// 调用 effect 方法
effect(
() => {
console.log(obj.count)
},
{
lazy: true
}
)
obj.count = 2
console.log('代码结束')
</script>
当不存在 lazy
时,打印结果为:
1 2 代码结束
当 lazy
为 true
时,因为不在触发 run
,所以不会进行依赖收集,打印结果为:
代码结束
调度器比懒执行要稍微复杂一些,整体的作用分成两块:
1. 控制执行顺序
packages/reactivity/src/effect.ts
中:tsexport function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
// 生成 ReactiveEffect 实例
const _effect = new ReactiveEffect(fn)
// 存在 options,则合并配置对象
+ if (options) {
+ extend(_effect, options)
+ }
// !options.lazy 时
if (!options || !options.lazy) {
// 执行 run 函数
_effect.run()
}
}
packages/shared/src/index.ts
中,增加 extend
函数:ts/**
* Object.assign
*/
export const extend = Object.assign
packages/vue/examples/reactivity/scheduler.html
:html<script>
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
// 调用 effect 方法
effect(
() => {
console.log(obj.count)
},
{
scheduler() {
setTimeout(() => {
console.log(obj.count)
})
}
}
)
obj.count = 2
console.log('代码结束')
</script>
最后执行结果为:
1 代码结束 2
说明我们实现了 控制执行顺序
2. 控制执行规则
packages/runtime-core/src/scheduler.ts
:ts// 对应 promise 的 pending 状态
let isFlushPending = false
/**
* promise.resolve()
*/
const resolvedPromise = Promise.resolve() as Promise<any>
/**
* 当前的执行任务
*/
let currentFlushPromise: Promise<void> | null = null
/**
* 待执行的任务队列
*/
const pendingPreFlushCbs: Function[] = []
/**
* 队列预处理函数
*/
export function queuePreFlushCb(cb: Function) {
queueCb(cb, pendingPreFlushCbs)
}
/**
* 队列处理函数
*/
function queueCb(cb: Function, pendingQueue: Function[]) {
// 将所有的回调函数,放入队列中
pendingQueue.push(cb)
queueFlush()
}
/**
* 依次处理队列中执行函数
*/
function queueFlush() {
if (!isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
/**
* 处理队列
*/
function flushJobs() {
isFlushPending = false
flushPreFlushCbs()
}
/**
* 依次处理队列中的任务
*/
export function flushPreFlushCbs() {
if (pendingPreFlushCbs.length) {
let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
pendingPreFlushCbs.length = 0
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]()
}
}
}
packages/runtime-core/src/index.ts
,导出 queuePreFlushCb
函数:tsexport { queuePreFlushCb } from './scheduler'
packages/vue/src/index.ts
中,新增导出函数:tsexport { queuePreFlushCb } from '@vue/runtime-core'
packages/vue/examples/reactivity/scheduler-2.html
:html<script>
const { reactive, effect, queuePreFlushCb } = Vue
const obj = reactive({
count: 1
})
// 调用 effect 方法
effect(
() => {
console.log(obj.count)
},
{
scheduler() {
queuePreFlushCb(() => {
console.log(obj.count)
})
}
}
)
obj.count = 2
obj.count = 3
</script>
最后执行结果为:
1 3 3
说明我们实现了 控制执行规则
懒执行相对比较简单,所以我们的总结主要针对调度器来说明。
调度器是一个相对比较复杂的概念,但是它本身并不具备控制 执行顺序 和 执行规则 的能力。
想要完成这两个能力,我们需要借助一些其他的东西来实现,这整个的一套系统,我们把它叫做 调度系统
那么到目前,我们调度系统的代码就已经实现完成了,这个代码可以在我们将来实现 watch
的时候直接使用。
packages/runtime-core/src/apiWatch.ts
模块,创建 watch
与 doWatch
函数:ts/**
* watch 配置项属性
*/
export interface WatchOptions<Immediate = boolean> {
immediate?: Immediate
deep?: boolean
}
/**
* 指定的 watch 函数
* @param source 监听的响应性数据
* @param cb 回调函数
* @param options 配置对象
* @returns
*/
export function watch(source, cb: Function, options?: WatchOptions) {
return doWatch(source as any, cb, options)
}
function doWatch(
source,
cb: Function,
{ immediate, deep }: WatchOptions = EMPTY_OBJ
) {
// 触发 getter 的指定函数
let getter: () => any
// 判断 source 的数据类型
if (isReactive(source)) {
// 指定 getter
getter = () => source
// 深度
deep = true
} else {
getter = () => {}
}
// 存在回调函数和deep
if (cb && deep) {
// TODO
const baseGetter = getter
getter = () => baseGetter()
}
// 旧值
let oldValue = {}
// job 执行方法
const job = () => {
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (deep || hasChanged(newValue, oldValue)) {
cb(newValue, oldValue)
oldValue = newValue
}
}
}
// 调度器
let scheduler = () => queuePreFlushCb(job)
const effect = new ReactiveEffect(getter, scheduler)
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else {
effect.run()
}
return () => {
effect.stop()
}
}
packages/reactivity/src/reactive.ts
为 reactive
类型的数据,创建 标记:ts export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
function createReactiveObject(
...
) {
...
// 未被代理则生成 proxy 实例
const proxy = new Proxy(target, baseHandlers)
// 为 Reactive 增加标记
proxy[ReactiveFlags.IS_REACTIVE] = true
...
}
ts /**
* 判断一个数据是否为 Reactive
*/
export function isReactive(value): boolean {
return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
packages/shared/src/index.ts
中创建 EMPTY_OBJ
:ts/**
* 只读的空对象
*/
export const EMPTY_OBJ: { readonly [key: string]: any } = {}
在 packages/runtime-core/src/index.ts
和 packages/vue/src/index.ts
中导出 watch
函数
创建测试实例 packages/vue/examples/reactivity/watch.html
:
html<script>
const { reactive, watch } = Vue
const obj = reactive({
name: '张三'
})
watch(
obj,
(value, oldValue) => {
console.log('watch 监听被触发')
console.log('value', value)
}
)
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
此时运行项目,却发现,当前存在一个问题,那就是 watch
监听不到 reactive
的变化。
这个问题的原因是 我们在 setTimeout
中,触发了 触发依赖 操作。但是我们并没有做 依赖收集 的操作导致的。
不知道大家还记不记得,我们之前在看源码的时候,看到过一个 traverse
方法。
之前的时候,我们一直没有看过该方法,那么现在我们可以来说一下它了。
它的源码在 packages/runtime-core/src/apiWatch.ts
中:
查看源代码可以发现,这里面的代码其实有些 莫名其妙,他好像什么都没有做,只是在 循环的进行 xxx.value
的形式,我们知道 xxx.value
这个行为,我们把它叫做 getter
行为。并且这样会产生 副作用,那就是 依赖收集!。
所以我们知道了,对于 traverse
方法而言,它就是一个不断在触发响应式数据 依赖收集 的方法。
我们可以通过该方法来触发依赖收集,然后在两秒之后,触发依赖,完成 scheduler
的回调。
packages/runtime-core/src/apiWatch.ts
中,创建 traverse
方法:ts/**
* 依次执行 getter,从而触发依赖收集
*/
export function traverse(value: unknown) {
if (!isObject(value)) {
return value
}
for (const key in value as object) {
traverse((value as any)[key])
}
return value
}
ts// 存在回调函数和deep
if (cb && deep) {
// TODO
const baseGetter = getter
getter = () => traverse(baseGetter())
}
此时再次运行测试实例, watch
成功监听。
同时因为我们已经处理了 immediate
的场景:
if (cb) { if (immediate) { job() } else { oldValue = effect.run() } } else { effect.run() }
所以,目前 watch
也支持 immediate
的配置选项。
对于 watch
而言本质上还是依赖于 ReactiveEffect
来进行的实现。
本质上依然是一个 依赖收集、触发依赖 的过程。只不过区别在于此时的依赖收集是被 “被动触发” 的。
除此之外,还有一个调度器的概念,对于调度器而言,它起到的的主要作用就是 控制执行顺序、控制执行规则 ,但是大家也需要注意调度器本身只是一个函数,想要完成调度功能,还需要其他的东西来配合才可以。
到这里,mini-vue 的整个 响应系统 就完成了,响应系统分成了:
reactive
ref
computed
watch
四大块来进行分别的实现。
通过之前的学习可以知道,响应式的核心 API
为 Proxy
。整个 reactive
都是基于此来进行实现。
但是 Porxy
只能代理 复杂数据类型,所以延伸除了 get value
和 set value
这样 以属性形式调用的方法, ref
和 computed
之所以需要 .value
就是因为这样的方法。
响应系统 终于结束,接下来可以开始学习新的模块 渲染系统 喽~
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!