测试示例: watch.html,测试vue源码的watch函数
<script src='../../dist/vue.global.js'>script>
<body>
<div id='app'>div>
body>
<script>
const { reactive, watch } = Vue
const obj = reactive({
name: '张三'
})
watch(obj, (val, oldVal) => {
console.log('watch ...')
console.log(val)
})
const timer = setTimeout(() => {
clearTimeout(timer)
obj.name = '李四'
}, 2000)
script>
进入 watch 函数,在源码 packages/runtime-core/src/apiWatch.ts 中
// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && !isFunction(cb)) {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`
)
}
return doWatch(source as any, cb, options)
}
从这里可以看出,它本质上是一个 doWatch 函数的触发
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
// 这个参数解析了options对象
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
}
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
)
}
// 这里引用一下 currentInstance
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// 注意这里,暂不会执行
if (isRef(source)) {
getter = () => source.value // ref 数据访问需要使用 .value的方式
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 注意这里会执行,因为我们定义的就是 reactive 响应式数据
getter = () => source // 这里响应式数据,getter 直接被赋值
deep = true // 这里deep会默认标识为 true, 及时程序员没有传递
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// 2.x array mutation watch compat
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}
// 这里会执行
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter()) // 这里做一层包装,先不管
}
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (__SSR__ && isInSSRComponentSetup) {
// we will also not call the invalidate callback (+ runner is not set up)
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return NOOP
}
// 这里有一个三元运算,会走到 INITIAL_WATCHER_VALUE 这里
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// 核心的job函数
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb // 这里看下 issue #1727
// 这里声明调度器
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// 此时调度器变成一个回调函数,包装了 job 函数
// default: 'pre'
scheduler = () => queuePreFlushCb(job)
}
// 这里同样走了老套路,传入 getter 和 调度器
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// 这里开始最核心的最后一块逻辑
// initial run
if (cb) {
// 这里读取配置
if (immediate) {
job() // 这里可以看出job的执行会代表watch函数的自动触发
} else {
// 这里会执行, 执行run函数中的 fn 函数
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
// 最终会return这个函数
return () => {
effect.stop() // 停止effect监听
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
}
这个watch是比较复杂的,里面核心的几个点:
当watch函数执行完毕,2s之后,测试代码中会触发 setter 行为
const timer = setTimeout(() => {
clearTimeout(timer)
obj.name = '李四'
}, 2000)
setter行为的触发本质上是之前收集的依赖被触发,也就是之前分析过的
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
这个调度器就是watch中的
这个函数 在 scheduler.ts 中
// 注意,这里的 cb 参数,就是我们的核心job函数
export function queuePreFlushCb(cb: SchedulerJob) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}
其本质还是 queueCb 函数,进入
function queueCb(
cb: SchedulerJobs,
activeQueue: SchedulerJob[] | null,
pendingQueue: SchedulerJob[],
index: number
) {
if (!isArray(cb)) {
if (
!activeQueue ||
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
) {
// 这里会执行,
pendingQueue.push(cb)
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
pendingQueue.push(...cb)
}
// 这里进入
queueFlush()
}
进入 queueFlush
function queueFlush() {
// 判断状态,这里会进入
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
注意这里的 resolvedPromise 的定义,它实际就是Promise的resolve 函数
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
也就是说通过异步微任务来处理队列任务,这里通过then函数执行 flushJobs,这个是最后的异步执行函数,会等待所有同步任务执行完成后立即被触发, 进入它
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
// 这里会执行
flushPreFlushCbs(seen)
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
queue.sort((a, b) => getId(a) - getId(b))
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (
queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length
) {
flushJobs(seen)
}
}
}
进入内部的 flushPreFlushCbs 函数
// 这个函数通过任务队列的形式,触发job函数
export function flushPreFlushCbs(
seen?: CountMap,
parentJob: SchedulerJob | null = null
) {
// 检测是否有数据,这里是之前的job函数
if (pendingPreFlushCbs.length) {
currentPreFlushParentJob = parentJob
// 这里 activePreFlushCbs 取代了 pendingPreFlushCbs
activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 去重
pendingPreFlushCbs.length = 0 // 这里清空自己,保证下次不会被触发
if (__DEV__) {
seen = seen || new Map()
}
for (
preFlushIndex = 0;
preFlushIndex < activePreFlushCbs.length;
preFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
) {
continue
}
// 这里会执行,也就是job函数会被触发
activePreFlushCbs[preFlushIndex]()
}
activePreFlushCbs = null
preFlushIndex = 0
currentPreFlushParentJob = null
// recursively flush until it drains
flushPreFlushCbs(seen, parentJob)
}
}
既然上述函数是为了处理队列中的job函数,我们再回到job函数中看看,位置:apiWatch.ts 中,doWatch 函数中的一段逻辑
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
// 这里会执行
if (cb) {
// watch(source, cb)
const newValue = effect.run() // 运行run函数,产生了 newValue, 这里是我们的响应性对象
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 这里是一个通用的 try catch
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
// 更新挂载 oldValue 变量
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
我们看一下这个 通用的 callWithAsyncErrorHandling 函数
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
return res
}
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
}
// 其实这个函数的位置,在 callWithAsyncErrorHandling 之上,目前是拿过来看看
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
也就是说,vue内部,通过一个 callWithAsyncErrorHandling 函数来统一处理 所有可能出现的代码,统一进行try catch的操作
从本质上来说 watch函数触发,是基于job函数触发来执行 effect.run 函数的调用,也就是会执行 我们的 fn 函数,所以,只要job被触发,watch就会被触发,当run() 执行了,就会拿到新的值
1 )懒执行
if (!options || !options.lazy) {
_effect.run()
}
2 ) 调度器
调度器主要分成两部分
测试调度器,新建测试用例 scheduler.html
<script src='../../dist/vue.global.js'>script>
<body>
<div id='app'>div>
body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
effect(() => {
console.log(obj.count)
})
obj.count = 2
console.log('over')
script>
2.1 ) 控制代码执行顺序
我们想,修改这个打印执行顺序,想要的顺序是: 1, over, 2
这时候,我们就需要用到的是调度器,我们修改下测试示例程序
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
effect(() => {
console.log(obj.count)
}, {
scheduler() {
setTimeout(() => {
console.log(obj.count)
})
}
})
obj.count = 2
console.log('over')
在 effect.ts 中的 triggerEffect 函数中,有以下的流程控制
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
存在调度器,则会执行调度器,否则才会执行 run 函数,因为示例程序修改后,传入了 调度器,所以此时run函数不再会执行,因为调度器中传入的是异步任务,所以在同步任务执行完毕后,开始执行调度器里的流程
所以,scheduler可以起到影响代码执行顺序的功能
2.2 ) 控制代码执行规则
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
effect(() => {
console.log(obj.count)
})
obj.count = 2
obj.count = 3
export { nextTick, queuePreFlushCb } from './scheduler'
npm run dev
const { reactive, effect, queuePreFlushCb } = Vue
const obj = reactive({
count: 1
})
effect(() => {
console.log(obj.count)
}, {
scheduler() {
queuePreFlushCb(() => console.log(obj.count))
}
})
obj.count = 2
obj.count = 3
scheduler = () => queuePreFlushCb(job)
1 )实现调度器
let isFlushPending = false // 状态表示
const pendingPreFlushCbs: Function[] = [] // 回调队列
const resolvedPromise = Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null
export function queuePreFlushCb(cb: Function) {
queueCb(cb, pendingPreFlushCbs)
}
function queueCb(cb: Function, pendingQueue: Function[]) {
pendingQueue.push(cb)
queueFlush()
}
// 依次执行队列中的函数
function queueFlush() {
if (isFlushPending) return // 真,什么都不做
// 只有这个状态为假,才继续执行
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
// 处理队列
function flushJobs() {
isFlushPending = false
flushPreFlushCbs()
}
// 用于循环进行队列处理
export function flushPreFlushCbs() {
if (!pendingPreFlushCbs.length) return
// 有队列内容时,使用新的标识,进行内容拷贝
let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
pendingPreFlushCbs.length = 0 // 清空这个旧的队列
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]()
}
}
2 )实现watch函数
首先补充下 effect.ts 中的effect()函数和ReactiveEffect类,添加一个option参数,用于支持懒执行
import { extend } from '@vue/shared' // 这个api就是 Object.assign,目的是通用
export interface ReactiveEffectOptions {
lazy?: boolean,
scheduler?: EffectScheduler
}
export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn)
// 如果存在,则与_effect合并,这就意味着 options包含调度器,_effect中也会有调度器
if (options) {
extend(_effect, options)
}
if (!options || !options.lazy) {
_effect.run()
}
}
// 这个函数主要添加一个stop的空函数
export class ReactiveEffect<T = any> {
computed?: ComputedRefImpl<T>
constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {}
run() {
activeEffect = this
return this.fn()
}
// 后期实现
stop() {}
}
再扩展 reactive.ts 补充 一个 isReactive 判断是否为响应性对象的函数,需要补充如下:
// 定义枚举
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive' // 定义一个是 reactive 对象的私有属性标识
}
// 补充这个createReactiveObject函数
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(target, baseHandlers)
// 添加下面这行
proxy[ReactiveFlags.IS_REACTIVE] = true // 将私有属性标识标记为 true
proxyMap.set(target, proxy)
return proxy
}
// 判断是否是 响应式对象
export function isReactive(value): boolean {
return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
新建 packages/runtime-core/src/apiWatch.ts
import { EMPTY_OBJ, hasChanged } from '@vue/shared'
import { isReactive, ReactiveEffect } from '@vue/reactivity'
import { queuePreFlushCb } from './scheduler'
export interface WatchOptions<immediate = boolean> {
immediate?: immediate,
deep?: boolean
}
export function watch(source, cb: Function, options?: WatchOptions) {
return doWatch(source, cb, options)
}
function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) {
let getter: () => any
// 当前是响应性对象
if (isReactive(source)) {
getter = () => source
deep = true
} else {
// 否则
getter = () => {}
}
// 处理deep场景
if (cb && deep) {
// 这种写法需要注意:这里实际上是内部进行依赖收集的地方
const baseGetter = getter // 浅拷贝引用
getter = () => baseGetter()
}
let oldValue = {}
const job = () => {
if (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()
}
}
导出各项需要的api, 比如watch, 重新 npm run dev 后,创建watch测试程序 watch.html
<script src="../dist/vue.js">script>
<body>
<div id='app'>div>
body>
<script>
const { reactive, watch } = Vue
const obj = reactive({
name: '张三'
})
watch(obj, (val, oldVal) => {
console.log('watch ...')
console.log(val)
}, {
immediate: true // 添加这个配置,会立即执行一次,但是下面2s后的异步回调没有被执行,这里watch没有监听到 reactive 的变化
})
const timer = setTimeout(() => {
clearTimeout(timer)
obj.name = '李四'
}, 2000)
script>
此时,watch是无法监听到reactive变化的, Why?
实际上,我们自己的 apiWatcher.ts 中 getter = () => baseGetter()
这里,和源码是不一样的,Vue源码是 getter = () => traverse(baseGetter())
Vue源码中的 traverse函数就是为了一直收集getter行为,也就是说在测试程序中没有手动触发过getter行为, 我们要在watch内部把整个source对象里的所有属性,主动触发一次getter行为来完成依赖收集
修改上述的 apiWatch.ts 中的getter, 如下
function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) {
// ...
// 处理deep场景
if (cb && deep) {
// 这种写法需要注意:这里是依赖收集的地方
const baseGetter = getter // 浅拷贝引用
getter = () => traverse(baseGetter()) // 内部进行依赖收集触发getter行为
}
// ...
}
export function traverse(value: unknown) {
if (!isObject(value)) {
return value
}
// 循环属性
for (const key in value as object) {
traverse((value as object)[key]) // 访问当前属性,触发 getter 行为
}
return value
}
这样,整个watch函数就可以正常运行了