目录
- 本文主要内容
- 调度器
- 1.添加任务(queueJobs)
- 2.二分法找到插入位置(findInsertionIndex)
- 3.将执行任务的函数推入微任务队列(queueFlush)
- 4.执行普通任务(flushJobs)
- 5.添加后置任务(queuePostFlushCb)
- 6.queuePostRenderEffect
- 7.执行后置队列任务(flushPostFlushJobs)
- 8.执行前置任务队列(flushPreFlushCbs)
- 9.nextTick
- 调度器总结
- watch用法
- 选项式watch Api的实现
- 创建watch对象(createWatchr)
- 选项式watch Api总结
- 函数式watch的实现(下面统称watch)
- 1.watch
- 2.doWatch
- watch总结
本文主要内容
- 学习Vue3的调度器原理。
- 了解nextTick的实现、为何在nextTick中可以获取到修改后的DOM属性。
- pre、post、和普通任务的执行过程。
- watch的实现原理。
调度器
1.添加任务(queueJobs)
- 调度器想要运转需要添加任务到调度器队列当中,我们需要知道Vue调度器队列一共有两种,分别为queue、pendingPostFlushCbs。
- queue:装载前置任务和普通任务的队列。
- pendingPostFlushCbs:装载后置任务的队列。
下面我们来看看对于前置任务和普通任务添加到queue中的函数queueJobs。
| |
| |
| |
| |
| function queueJob(job) { |
| |
| if ( |
| !queue.length || |
| !queue.includes( |
| job, |
| isFlushing && job.allowRecurse ? flushIndex + : flushIndex |
| ) |
| ) { |
| |
| |
| if (job.id == null) { |
| queue.push(job); |
| } |
| |
| else { |
| queue.splice(findInsertionIndex(job.id),, job); |
| } |
| |
| queueFlush(); |
| } |
| } |
- 这里我们需要知道一个概念-->递归,这里的递归是指:当前正在执行的任务和需要添加的任务是同一个任务,如果设置了需要递归(job.allowRecurse=true)那么就允许这个任务进入queue队列中,否则不允许进入。
- job:我们还需要知道一个任务的格式。首先job必须是一个函数,他还可以具有以下属性。
| const job = function(){} |
| job.id:Number |
| job.allowRecurse:Boolean |
| job.pre:Boolean |
| job.active:Boolean |
- queueJobs执行流程:根据任务的id(优先级)利用二分法找到需要插入的位置,插入到queue队列当中,调用queueFlush推入执行任务的函数到微任务队列。
2.二分法找到插入位置(findInsertionIndex)
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function findInsertionIndex(id) { |
| let start = flushIndex +; |
| let end = queue.length; |
| while (start < end) { |
| |
| |
| |
| |
| |
| const middle = (start + end) >>>; |
| |
| const middleJobId = getId(queue[middle]); |
| middleJobId < id ? (start = middle +) : (end = middle); |
| } |
| return start; |
| } |
| |
| const getId = (job) => (job.id == null ? Infinity : job.id); |
3.将执行任务的函数推入微任务队列(queueFlush)
| function queueFlush() { |
| |
| if (!isFlushing && !isFlushPending) { |
| |
| isFlushPending = true; |
| |
| currentFlushPromise = resolvedPromise.then(flushJobs); |
| } |
| } |
- isFlushing:判断当前是否正在执行任务。
- isFlushPending:判断当前是否有等待任务,任务的执行是一个微任务,它将会被放到微任务队列,那么对于渲染主线程来说,当前还没有执行这个微任务,在执行这个微任务之前都属于等待阶段。
- queueFlush执行流程:判断当前是否没有执行任务、且任务队列当中没有任务,如果是那么设置当前为等待阶段。最后将flushJobs(执行任务的函数)推入微任务队列。
4.执行普通任务(flushJobs)
| function flushJobs(seen) { |
| isFlushPending = false; |
| isFlushing = true; |
| seen = seen || new Map(); |
| |
| |
| |
| |
| |
| queue.sort(comparator); |
| |
| const check = (job) => checkRecursiveUpdates(seen, job); |
| try { |
| for (flushIndex =; flushIndex < queue.length; flushIndex++) { |
| const job = queue[flushIndex]; |
| if (job && job.active !== false) { |
| if (check(job)) { |
| continue; |
| } |
| callWithErrorHandling(job, null,); |
| } |
| } |
| } finally { |
| |
| |
| |
| |
| flushIndex =; |
| queue.length =; |
| flushPostFlushCbs(seen); |
| isFlushing = false; |
| currentFlushPromise = null; |
| if (queue.length || pendingPostFlushCbs.length) { |
| flushJobs(seen); |
| } |
| } |
| } |
- seen:这是一个Map,用于缓存job的执行次数,如果超过了RECURSION_LIMIT的执行次数,将会警用户。
- RECURSION_LIMIT:Vue默认值为100。这个值不可以让用户修改(常量值)。
- flushJobs执行流程:获取queue队列中的每一个任务,检测这个任务是否嵌套执行了100次以上,超过了则警告用户。然后执行当前任务直到flushIndex === queue.length。(queue的长度可能会持续增加)。调用flushPostFlushCbs执行后置队列的任务。
- 由于在执行后置队列任务的时候可能又向queue中添加了新的任务,那么就需要执行完后置队列后再调用flushJobs。
5.添加后置任务(queuePostFlushCb)
| function queuePostFlushCb(cb) { |
| if (!shared.isArray(cb)) { |
| if ( |
| !activePostFlushCbs || |
| !activePostFlushCbs.includes( |
| cb, |
| cb.allowRecurse ? postFlushIndex + : postFlushIndex |
| ) |
| ) { |
| pendingPostFlushCbs.push(cb); |
| } |
| } else { |
| pendingPostFlushCbs.push(...cb); |
| } |
| queueFlush(); |
| } |
- 与添加普通任务到队列中一样,添加完成后调用queueFlush开启调度。
6.queuePostRenderEffect
| function queueEffectWithSuspense(fn, suspense) { |
| |
| if (suspense && suspense.pendingBranch) { |
| if (shared.isArray(fn)) { |
| suspense.effects.push(...fn); |
| } else { |
| suspense.effects.push(fn); |
| } |
| } else { |
| |
| queuePostFlushCb(fn); |
| } |
| } |
- 如果传递了suspense那么调用suspense的api。
- 没有传递suspense当作一般的后置任务即可。
7.执行后置队列任务(flushPostFlushJobs)
| function flushPostFlushCbs(seen) { |
| if (pendingPostFlushCbs.length) { |
| |
| const deduped = [...new Set(pendingPostFlushCbs)]; |
| pendingPostFlushCbs.length =; |
| |
| |
| if (activePostFlushCbs) { |
| activePostFlushCbs.push(...deduped); |
| return; |
| } |
| activePostFlushCbs = deduped; |
| seen = seen || new Map(); |
| |
| activePostFlushCbs.sort((a, b) => getId(a) - getId(b)); |
| for ( |
| postFlushIndex =; |
| postFlushIndex < activePostFlushCbs.length; |
| postFlushIndex++ |
| ) { |
| |
| if (checkRecursiveUpdates(seen, activePostFlushCbs[postFlushIndex])) { |
| continue; |
| } |
| |
| activePostFlushCbs[postFlushIndex](); |
| } |
| |
| activePostFlushCbs = null; |
| postFlushIndex =; |
| } |
| } |
- flushPostFlushCbs执行流程:和flushJobs差不多,拿到pendingPostFlushCbs队列中的任务并执行他们,在执行完成后初始化postFulshIndex指针。
- 之所以后置队列一定会在完成普通任务和前置任务后执行,是因为无论你是通过queueJobs添加任务发起调度还是通过queuePostFlushCb添加任务发起调度,都总是调用flushJobs,而在flushJobs的实现中,总是先清空queue队列在执行pendingPostFlushCbs。
- activePostFlushCbs作用:想象一个场景,如果我直接通过调用flushPostFlushJobs发起调度那么任务将不会是异步的,并且会打乱调度器的执行顺序,所以有了这个属性。若当前已经存在了activePostFlushCbs表示正在执行后置队列的任务,在任务中调用flushPostFlushJobs并不会直接执行,而是会把pendingPostFlushcbs中的任务放到avtivePostFlushCbs任务的后面。这样就保证了调度器的顺序执行。
8.执行前置任务队列(flushPreFlushCbs)
| function flushPreFlushCbs(seen, i = isFlushing ? flushIndex + : 0) { |
| seen = seen || new Map(); |
| for (; i < queue.length; i++) { |
| const cb = queue[i]; |
| if (cb && cb.pre) { |
| if (checkRecursiveUpdates(seen, cb)) { |
| continue; |
| } |
| queue.splice(i,); |
| i--; |
| cb(); |
| } |
| } |
| } |
- 添加前置任务的方法:对添加的任务函数Job添加pre属性。
job.pre = true
- 这里需要注意,对于前置任务和普通任务都会被添加到queue当中,如果调用的flushJobs触发任务执行,那么前置任务和普通任务都会被执行。他们的执行顺序为高优先级的先执行(id小的先执行)。相同优先级的前置任务先执行。
- flushPreFlushCbs执行流程:在queue中找到带有pre属性的任务,执行并在queue中删除这个任务。
- 对于处于执行后置任务的状态,同时调用了flushPostFlushCbs发起后置任务的调度,那么会将新增的任务加到activePostFlushCbs中。但是对于前置任务是不需要这么做的,如果通过调用flushPreFlushCbs发起调度那么前置任务将会是同步执行。我们来看这样一个例子。
| function a(){ |
| console.log() |
| } |
| function b(){ |
| console.log() |
| } |
| a.pre = true |
| queueJobs(a) |
| queueJobs(b) |
| flushPreFlushCbs() |
| |
- 如何理解呢?首先a任务是前置任务,a、b任务都被添加到了queue队列中,同时发起了调度,但是这是一个微任务,而当前执行的任务还未执行完成,所以会先调用flushPreFlushCbs。那么就会调用前置任务也就是a任务。调用完成后删除queue队列中的a任务,此时queue队列中只有b任务了。然后执行微任务,进一步调用b任务。
9.nextTick
- 场景:在修改了响应式数据后,想要获取到最新DOM上的数据,因为只修改了相应式数据,目前DOM还未发生改变所以获取不到改变后的DOM属性。
| <script> |
| import { nextTick } from 'vue' |
| export default { |
| data() { |
| return { count: } }, |
| methods: { |
| async increment() { |
| this.count++ |
| |
| console.log(document.getElementById('counter').textContent) |
| await nextTick() |
| console.log(document.getElementById('counter').textContent) |
| } |
| } |
| } |
| </script> |
| <template> |
| <button id="counter" @click="increment">{{ count }}</button> |
| </template> |
| function nextTick(fn) { |
| const p = currentFlushPromise || resolvedPromise; |
| return fn ? p.then(this ? fn.bind(this) : fn) : p; |
| } |
- currentFlushPromise:在调用queueFlush时会创建一个微任务,将flushJobs推入微任务队列。
| function queueFlush() { |
| if (!isFlushing && !isFlushPending) { |
| isFlushPending = true; |
| currentFlushPromise = resolvedPromise.then(flushJobs); |
| } |
| } |
- resolvedPromise:状态为fulfilled的Promise。
- 如果当前队列中没有任务则p=resolvedPromise,直接将fn推入微任务队列。因为调度器队列中无任务所以不存在DOM的更新。
- 如果当前队列中有任务则p=currentFlushPromise,若当前正在执行flushJobs那么currentFlushPromise的状态为fulfilled则会将fn推入微任务队列,当然前提是flushJobs已经执行完才有可能执行fn,而只要flushJobs执行完毕DOM也已经完成了更新。若当前没有执行flushJobs,那么currentFlushPromise的状态为pending,就不可能将fn推入微任务队列。综上就保证了fn一定在DOM更新后触发。
调度器总结
- 调度器的调度队列分为后置队列和普通队列。
- 普通队列中包含了前置任务和普通任务。如果通过flushPreFlushCbs调用那么前置任务为同步任务。执行完成后删除普通队列中相对应的任务。如果通过flushJobs调用,那么调用顺序按照优先级高低排列,相同优先级的前置任务先调用。
- 后置队列任务一定在普通队列清空后执行。
- 普通任务和后置任务为异步,前置任务可能为同步可能为异步。
- 在将任务放入队列当中时就已经自动发起了调度,用户可以不通过手动调用。如果手动调用flushPostFlushCbs实际上是将任务放到队列中,而不是重新开启调度。
watch用法
| <script> |
| export default { |
| watch{ |
| a(){}, |
| b:"meth" |
| c:{ |
| handler(val,oldVal){}, |
| deep:true, |
| immediate:true |
| }, |
| "d.a":function(){} |
| } |
| } |
| </script> |
| const callback = ([aOldVal,aVal],[bOldVal,bVal])=>{} |
| |
| watch(["a","b"], callback, { |
| flush: 'post', |
| onTrack(e) { |
| debugger |
| }, |
| deep:true, |
| immediate:true, |
| }) |
选项式watch Api的实现
| |
| |
| |
| if (watchOptions) { |
| for (const key in watchOptions) { |
| createWatcher(watchOptions[key], ctx, publicThis, key); |
| } |
| } |
- 这里的watchOptions就是用户写的选项式api的watch对象。
创建watch对象(createWatchr)
| function createWatcher(raw, ctx, publicThis, key) { |
| |
| const getter = key.includes(".") |
| ? createPathGetter(publicThis, key) |
| : () => publicThis[key]; |
| |
| if (shared.isString(raw)) { |
| const handler = ctx[raw]; |
| if (shared.isFunction(handler)) { |
| |
| watch(getter, handler); |
| } else { |
| warn(`Invalid watch handler specified by key "${raw}"`, handler); |
| } |
| } |
| |
| else if (shared.isFunction(raw)) { |
| watch(getter, raw.bind(publicThis)); |
| } |
| |
| else if (shared.isObject(raw)) { |
| |
| if (shared.isArray(raw)) { |
| raw.forEach((r) => createWatcher(r, ctx, publicThis, key)); |
| } |
| |
| else { |
| |
| |
| |
| const handler = shared.isFunction(raw.handler) |
| ? raw.handler.bind(publicThis) |
| : ctx[raw.handler]; |
| if (shared.isFunction(handler)) { |
| watch(getter, handler, raw); |
| } else { |
| warn( |
| `Invalid watch handler specified by key "${raw.handler}"`, |
| handler |
| ); |
| } |
| } |
| } else { |
| warn(`Invalid watch option: "${key}"`, raw); |
| } |
| } |
- 选项式watch的键可以是"a.b.c"这样的形式也可以是普通的"a"形式,它的值可以是字符串,函数,对象,数组。此函数主要对不同形式的参数做重载。最终都是调用watch函数。
- 对于键为"a.b"形式的需要调用createPathGetter创建一个getter函数,getter函数返回"a.b"的值。
- 对于值为字符串的我们需要从methods中获取对应的方法。因为之前许多重要属性都代理到ctx上了所以只需要访问ctx即可。
- 对于值为函数的我们只需要将key作为watch的第一个参数,值作为watch的第二个参数即可。
- 对于值为对象的获取handler作为watch第二个参数,将raw作为第三个参数(选项)传入watch即可。
- 对于值为数组的,表示需要开启多个监听,遍历数组递归调用createWatcher即可。
选项式watch Api总结
- 对于选项式watch Api本质上还是调用的函数式watch Api进行实现的。这里只是做了重载,对于不同的配置传递不同的参数给watch。所以接下来我们重点分析函数式watch Api的实现。
函数式watch的实现(下面统称watch)
1.watch
| function watch(source, cb, options) { |
| |
| if (!shared.isFunction(cb)) { |
| console.warn(); |
| } |
| return doWatch(source, cb, options); |
| } |
- source:监听源,可以是数组(代表监听多个变量)。
- cb:监听源发生改变时,调用的回调函数。
- options:watch函数的可选项。
- 如果传递的cb不是函数需要警告用户,这可能导致错误。
2.doWatch
- 这个函数非常长,也是watch的实现核心,我们分多个部分讲解。
- 大致原理:收集source中响应式元素包装成getter,在new ReactiveEffect中传递调用run方法执行getter就会收集到依赖,然后当触发依赖更新的时候就会调用scheduler,在根据flush参数,选择同步执行scheduler还是加入调度器。
| function doWatch( |
| source, |
| cb, |
| |
| { immediate, deep, flush, onTrack, onTrigger } = shared.EMPTY_OBJ |
| ) { |
| |
| if (!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.` |
| ); |
| } |
| } |
| |
| } |
- 第一部分的代码主要是检测参数。对于没有cb参数但是又有immediate和deep选项的需要警告用户。
| |
| const instance = getCurrentInstance(); |
| let getter; |
| let forceTrigger = false; |
| let isMultiSource = false; |
| |
| if (reactivity.isRef(source)) { |
| getter = () => source.value; |
| forceTrigger = reactivity.isShallow(source); |
| } |
| |
| else if (reactivity.isReactive(source)) { |
| getter = () => source; |
| deep = true; |
| } |
| |
| else if (shared.isArray(source)) { |
| isMultiSource = true; |
| |
| |
| forceTrigger = source.some( |
| (s) => reactivity.isReactive(s) || reactivity.isShallow(s) |
| ); |
| |
| getter = () => |
| source.map((s) => { |
| if (reactivity.isRef(s)) { |
| return s.value; |
| } else if (reactivity.isReactive(s)) { |
| |
| |
| |
| return traverse(s); |
| } |
| |
| else if (shared.isFunction(s)) { |
| return callWithErrorHandling(s, instance,); |
| } else { |
| |
| warnInvalidSource(s); |
| } |
| }); |
| } |
| |
- 如果监听的数据是ref类型,包装成getter形式。
- 如果监听的数据是reactive类型,需要设置为深度监听。
- 如果监听的数据是数组,设置变量isMultiSource=true表示当前监听了多个变量,同时判断监听的所有数据中是否有相应式对象,如果有就必须强制触发。设置getter。
- 我们可以发现所有的监听数据源都会被包装成getter,这是因为底层都是调用reactivity库的watchEffect,而第一个参数必须是函数,当调用这个函数访问到的变量都会收集依赖。所以如果当前元素为reactive元素的时候需要遍历这个元素的所有值以便所有的变量都能收集到对应的依赖。
| |
| else if (shared.isFunction(source)) { |
| if (cb) { |
| |
| getter = () => callWithErrorHandling(source, instance,); |
| } else { |
| |
| getter = () => { |
| if (instance && instance.isUnmounted) { |
| return; |
| } |
| if (cleanup) { |
| cleanup(); |
| } |
| return callWithAsyncErrorHandling(source, instance,, [onCleanup]); |
| }; |
| } |
| } |
| |
- 如果监听的数据是函数,先判断是否有cb,如果有cb则将监听源函数作为getter。
- 如果没有传递cb,那么这个函数将会作为getter和回调函数cb。
- 我们来详细说说cleanup的作用。先来看看官方的测试用例:
| watch(async (onCleanup) => { |
| const { response, cancel } = doAsyncWork(id.value) |
| |
| |
| |
| onCleanup(cancel) |
| data.value = await response |
| }) |
- 它被用来做副作用清除。第一次调用getter的时候是作为收集依赖,所以cleanup为空不执行,然后调用source函数,在这个函数中会收到onCleanup的参数,如果你在source函数中调用了onCleanup函数那么cleanup将会被赋值。当id发生改变之后再次调用getter函数(此时作为cb),这时候cleanup就会被调用,也就是官方说的cancle函数会在id更改时调用。
- 我们继续第四部分代码的分析:
| |
| else { |
| getter = shared.NOOP; |
| |
| warnInvalidSource(source); |
| } |
| |
- 这表示没有需要监听的数据源,将getter设置为空函数,同时警告用户。
| const INITIAL_WATCHER_VALUE = {} |
| |
| |
| |
| if (cb && deep) { |
| const baseGetter = getter; |
| getter = () => traverse(baseGetter()); |
| } |
| let cleanup; |
| |
| let onCleanup = (fn) => { |
| cleanup = effect.onStop = () => { |
| callWithErrorHandling(fn, instance,); |
| }; |
| }; |
| let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE; |
| |
- 对于含有deep属性的需要深度遍历,只要在getter中访问了所有变量的值那么这些值都会收集到依赖。
- 接下来便是onCleanup的实现,大家可以按照上面我说的进行理解。
- 我们知道在watch可以监听多个数据,那么对应的cb回调函数的参数要收集到这些改变的值。所以如果监听了多个数据源那么oldValue会被设置为数组否则为对象。
| |
| const job = () => { |
| if (!effect.active) { |
| return; |
| } |
| |
| if (cb) { |
| |
| |
| const newValue = effect.run(); |
| |
| |
| if ( |
| deep || |
| forceTrigger || |
| (isMultiSource |
| ? newValue.some((v, i) => shared.hasChanged(v, oldValue[i])) |
| : shared.hasChanged(newValue, oldValue)) |
| ) { |
| |
| if (cleanup) { |
| cleanup(); |
| } |
| callWithAsyncErrorHandling(cb, instance,, [ |
| newValue, |
| oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, |
| onCleanup, |
| ]); |
| oldValue = newValue; |
| } |
| } else { |
| |
| effect.run(); |
| } |
| }; |
| |
- 这个job代表的是要传递给Vue调度器的任务,所以这是在创建一个调度器任务。
- 同时还需要注意这个job是监听的变量发生了改变后才会调用。
- 这里的effect代表的是ReactiveEffect类的实例,如果还不了解这个类的请阅读Vue3源码分析(2)。
- 如果没有传递cb那么会调用effect.run()这个函数会去执行getter函数。因为没有传递cb所以回调函数就是getter函数。
- 如果存在cb,那么会先调用getter函数获取最新的value,然后再调用cb,所以不太建议自己将第一个参数写成函数,这样改变值的时候会调用getter和cb两个函数,如果你在getter中写了副作用那么就会多次调用。
- 同样cleanup用于清除副作用这里就不再赘述了。
| |
| job.allowRecurse = !!cb; |
| let scheduler; |
| |
| if (flush === "sync") { |
| scheduler = job; |
| } |
| |
| else if (flush === "post") { |
| scheduler = () => queuePostRenderEffect(job, instance && instance.suspense); |
| } |
| |
| |
| else { |
| job.pre = true; |
| |
| if (instance) job.id = instance.uid; |
| scheduler = () => queueJob(job); |
| } |
| |
- 当监视的数据发生改变的时候会调用job任务,但是job任务是异步调用还是同步调用是可以通过flush参数改变的。
- 当flush为sync的时候:会同步的执行job任务。
- 当flush为post的时候:会将job任务推入后置任务队列,也就是会等queue队列任务执行完成之后执行。
- 当flush为pre的时候:会将job任务设置为前置任务,在调用flushPreFlushCbs的时候执行。执行完成后删除这个任务。当然如果一直不调用flushPreFlushCbs,将会作为普通任务执行,这时候就是异步的了。
- 最终getter和scheduler都得到了。他们会作为reactiveEffect类的两个参数。第一个为监听的getter函数,在这里面访问的值都会收集到依赖,当这些监听的值发生改变的时候就会调用schgeduler。
| const effect = new reactivity.ReactiveEffect(getter, scheduler); |
| |
| |
| effect.onTrack = onTrack; |
| effect.onTrigger = onTrigger; |
| |
- onTrack:是reactivity库实现的api。当被追踪的时候调用这个函数。
- onTrigger:当监视的变量改变的时候触发的函数。
- 创建ReactiveEffect实例对象,对变量进行监视。
| |
| |
| if (cb) { |
| if (immediate) { |
| |
| job(); |
| } |
| |
| |
| else { |
| oldValue = effect.run(); |
| } |
| } |
| |
| |
| else if (flush === "post") { |
| queuePostRenderEffect( |
| effect.run.bind(effect), |
| instance && instance.suspense |
| ); |
| } |
| |
| else { |
| effect.run(); |
| } |
| |
- 如果含有immediate参数则需要立刻执行job任务,否则调用effect.run()方法(调用getter)收集依赖。
- 如果flush设置为post那么收集依赖的操作也需要移动到后置队列当中。
| |
| return () => { |
| effect.stop(); |
| }; |
watch总结
- 为了兼容选项式watch处理了不同的配置选项最终调用函数式的watch来实现的监视效果。
- watch拥有三个参数:source、cb、options。
- source是监听源,可以传递函数,值,数组。但是最后都是包装成getter函数。实现的理念就是通过调用getter函数,访问响应式变量收集依赖,当响应式数据发生改变的时候调用cb。
- options中比较重要的配置是flush,他决定了何时收集依赖和触发依赖。当flush为post的时候需要知道收集依赖和触发依赖都将会推入到后置队列当中(DOM更新后触发)。