本文主要内容
学习 Vue3 的调度器原理。 了解 nextTick 的实现、为 何在 nextTick 中可以获取到修改后的DOM属性。 PR e、post、和普通 任务 的执行过程。 watch 的 实现原理 。
调度器
1.添加任务(queueJobs)
调度器想要运转需要添加任务到调度器队列当中,我们需要 知道 Vue调度器队列一共有两种,分别为 queue 、 PE ndingPostFlushCbs 。 queue :装载前置任务和普通任务的队列。 pendingPostFlushCbs :装载后置任务的队列。下面我们来看看对于前置任务和普通任务添加到 queue 中的函数 queueJobs 。
//递归:当前父亲正在执行一个任务,在执行任务 //期间又添加了一个新的任务,这个新的任务与当前 //执行的任务是同一个任务,跳过去重的 检验 //如果不允许递归,那么任务不会被添加到队列中 function queueJob(job) { //job自身允许递归,那么跳过去重检查(只跳过当前执行任务的去重检查) if ( !queue.length || !queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex ) ) { //如果任务没有id代表没有优先级 //放到任务队列的最后面 if (job.id == null) { queue.push(job); } // 利用 二分法找到任务优先级需要插入的位置 else { queue. splice (findInsertionIndex(job.id), 0, job); } //执行任务 queueFlush(); } }这里我们需要知道一个概念-->递归,这里的递归是指:当前正在执行的任务和需要添加的任务是同一个任务,如果设置了需要递归 (job.allowRecurse=true) 那么就允许这个任务进入queue队列中,否则不允许进入。 job :我们还需要知道一个任务的格式。首先job必须是一个函数,他还可以具有以下属性。
const job = function(){} job.id:Number //用于设置当前任务的优先级越小的值优先级越高。 job.allowRecurse:Boolean // 是否 允许递归。 job.pre:Boolean //用于判断是否是前置任务。 job.active:Boolean //当前任务是否可以执行。为false在执行 阶段 跳过执行。queueJobs 执行流程:根据任务的id(优先级)利用二分法找到需要插入的位置,插入到queue队列当中,调用 queueFlush 推入执行任务的函数到微任务队列。
2.二分法找到插入位置(findInsertionIndex)
这个函数比较 简单 ,大家看看代码就可以啦!//找到插入的位置 //例如[1,2,3,8,9,10,100] //当前插入的id为20 //插入后 应该 为[1,2,3,8,9,10,20,100] //也就是说最终返回的start=6 //插入流程解析: //1.假设当前执行到第二个任务即flushIndex=2 //那么start = 2;end = 7 ;m iddle=4; // middleJobId=9;9<20 start=5; //继续循环:middle=6;middleJobId=100;end=6 //结束循环start = 6;这就是需要插入的位置 function findInsertionIndex(id) { let start = flushIndex + 1; let end = queue.length; while (start < end) { // 1000>>>1=>100 8=>4 // 1100>>>1=>110 12=>6 // 1010>>>1=>101 10=>5 // 1001>>>1=>100 9=>4 //计算出中间值,向下 取整 const middle = (start + end) >>> 1; //获取job的id const middleJobId = getId(queue[middle]); middleJobId < id ? (start = middle + 1) : (end = middle); } return start; } //获取当前任务的id const getId = (job) => (job.id == null ? Infin IT y : job.id);
3.将执行任务的函数推入微任务队列(queueFlush)
function queueFlush() { //当前没有执行任务且没有任务可执行 if (!isFlushing & am p; amp;& !isFlushPending) { //等待任务执行 isFlushPending = true; //将flushJobs放入微任务队列 current FlushP rom ise = resolvedPromise.then(flushJobs); } }isFlushing :判断当前是否正在执行任务。 isFlushPending :判断当前是否有等待任务,任务的执行是一个微任务,它将会被放到微任务队列,那么对于渲染主线程来说,当前还没有执行这个微任务,在执行这个微任务之前都属于等待阶段。 queueFlush 执行流程:判断当前是否没有执行任务、且任务队列当中没有任务,如果是那么设置当前为等待阶段。最后将flushJobs(执行任务的函数)推入微任务队列。
4.执行普通任务(flushJobs)
function flushJobs(seen) { isFlushPending = false; //当前不是等待状态 isFlushing = true; //当前正在执行任务 seen = seen || new Map(); //原文译文: //在flush之前对queue排序这样做是为了: //1.组件更新是重父组件到子组件(因为父组件总是在子组件之前创建 //所以父组件的render副作用将会有更低的优先级 //2.如果子组件在父组件更新期间并未挂载,那么可以跳过 queue.sort(comparator); //监测当前任务是否已经超过了最大递归层数 const check = (job) => checkRecursiveUpdates(seen, job); try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex]; if (job && job.active !== false) { if (check(job)) { continue; } callWithErrorHandling(job, null, 14); } } } finally { //执行完所有的任务之后,初始化queue //调用post任务,这些任务调用完折后 //可能在执行这些任务的途中还有新的 //任务加入所以需要继续执行flushJobs flushIndex = 0; queue.length = 0; 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 (!sha red .isArray(cb)) { if ( !activePostFlushCbs || !activePostFlushCbs.includes( cb, cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex ) ) { pendingPostFlushCbs.push(cb); } } else { pendingPostFlushCbs.push( .. .cb); } queueFlush(); }与添加普通任务到队列中一样,添加完成后调用 queueFlush 开启调度。
6.queuePostRenderEffect
function queueEffectWithSuspense(fn, suspense) { //对suspense的处理,暂时不详细解释 if (suspense && suspense.pending br anch) { 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) { //克隆等待执行的pendingPost const deduped = [...new Set(pendingPostFlushCbs)]; pendingPostFlushCbs.length = 0; //设置为0 //当前函数是后置队列的任务发起的,那么不能 //直接运行任务,而是将任务放到avtivePostFlushCbs任务之后 if (activePostFlushCbs) { activePostFlushCbs.push(...deduped); return; } activePostFlushCbs = deduped; seen = seen || new Map(); //排序(post依然有优先级) activePostFlushCbs.sort((a, b) => getId(a) - getId(b)); for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) { //检测执行深度 if (checkRecursiveUpdates(seen, activePostFlushCbs[postFlushIndex])) { continue; } //调用这个postJob activePostFlushCbs[postFlushIndex](); } //初始化 activePostFlushCbs = null; postFlushIndex = 0; } }flushPostFlushCbs 执行流程:和 flushJobs 差不多,拿到 pendingPostFlushCbs 队列中的任务并执行他们,在执行完成后初始化postFulshIndex指针。 之所以后置队列一定会在完成普通任务和前置任务后执行,是因为无论你是通过 queueJobs 添加任务发起调度还是通过 queuePostFlushCb 添加任务发起调度,都总是调用 flushJobs ,而在 flushJobs 的实现中,总是先清空 queue 队列在执行 pendingPostFlushCbs 。 activePostFlushCbs 作用:想象一个场景,如果我直接通过调用 flushPostFlushJobs 发起调度那么任务将不会是异步的,并且会打乱调度器的执行顺序,所以有了这个属性。若当前已经存在了 activePostFlushCbs 表示正在执行后置队列的任务,在任务中调用 flushPostFlushJobs 并不会直接执行,而是会把 pendingPostFlushcbs 中的任务放到 avtivePostFlushCbs 任务的后面。这样就保证了调度器的顺序执行。
8.执行前置任务队列(flushPreFlushCbs)
function flushPreFlushCbs(seen, i = isFlushing ? flushIndex + 1 : 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, 1); i--; cb(); } } }添加前置任务的方法:对添加的任务函数Job添加pre属性。
job.pre = true这里需要注意,对于前置任务和普通任务都会被添加到 queue 当中,如果调用的 flushJobs 触发任务执行,那么前置任务和普通任务都会被执行。他们的执行顺序为高优先级的先执行(id小的先执行)。相同优先级的前置任务先执行。 flushPreFlushCbs 执行流程:在 queue 中找到带有 pre 属性的任务,执行并在 queue 中删除这个任务。 对于处于执行后置任务的状态,同 时调 用了 flushPostFlushCbs 发起后置任务的调度,那么会将新增的任务加到 activePostFlushCbs 中。但是对于前置任务是不需要这么做的,如果通过调用 flushPreFlushCbs 发起调度那么前置任务将会是同步执行。我们来看这样一个例子。
function a(){ console. LOG (222) } function b(){ console.log(111) } a.pre = true queueJobs(a) queueJobs(b) flushPreFlushCbs() //打印:222 111如何理解呢?首先 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: 0 } }, methods: { async increment() { this.count++ // DOM 还未更新 // 0 console.log(document.getElementById('counter').textContent) aw ai t nextTick() // DOM 此时已经更新 console.log(document.getElementById('counter').textContent) // 1 } } } </script> <template> <button id="counter" @click="increment">{{ count }}</button> </template>nextTick 实现:
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> e xp ort default { watch{ a(){}, b:"meth"//在methods中声明的方法 c:{ handler(val,oldVal){}, deep:true,//开启深度监视 immediate:true//立即调用handler }, "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的实现
//这一段代码在Vue3 源码分析 (7)中出现过 //不了解的可以看看上一篇 文章 //对每一个watch选项添加 watcher if (watchOptions) { for (const key in watchOptions) { createWatcher(watchOptions[key], ctx, p ub licThis, key); } }这里的 watchOptions 就是用户写的选项式api的watch对象。
创建watch对象(createWatc hr )
function createWatcher(raw, ctx, publicThis, key) { //可以监听深度数据例如a.b.c const getter = key.includes(".") ? createPathGetter(publicThis, key) : () => publicThis[key]; //raw可以是字符串,会读取methods中的方法 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)) { // 数组遍历 ,获取每一个监听器在执行createWatcher if (shared.isArray(raw)) { raw.for each ((r) => createWatcher(r, ctx, publicThis, key)); } //对象 else { //handler可能是字符串重ctx上获取 //也可能是函数 //获取到handler后调用watch 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) { //cb必须是函数 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, //getter ()=>[监听的数据] cb, //回调函数 //获取当前watch的选项 { immediate, deep, flush, onTrack, onTrigger } = shared.EMPTY_OBJ ) { //immediate和deep属性必须有cb if (!cb) { if (immediate !== un define d) { 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; //是否多个数据 //判断监听的数据是否是ref 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; //source中有一个是响应式的 //就需要触发 forceTrigger = source.some( (s) => reactivity.isReactive(s) || reactivity.isShallow(s) ); //()=>[ Proxy ,()=>proxy,ref] getter = () => source.map((s) => { if (reactivity.isRef(s)) { return s.value; } else if (reactivity.isReactive(s)) { //遍历响应式对象s 这个getter会作为ReactiveEffect的 //第一个参数,在调用run的时候遍历所有的值 //确保能让每一个变量都能收集到effect return traverse(s); } //调用监听的函数 else if (shared.isFunction(s)) { return callWithErrorHandling(s, instance, 2); } else { //提示非法source信息 warnInvalidSource(s); } }); } //省略第三部分代码如果监听的数据是ref类型,包装成 getter 形式。 如果监听的数据是reactive类型,需要设置为深度监听。 如果监听的数据是数组,设置变量 isMultiSource=true 表示当前监听了多个变量,同时判断监听的所有数据中是否有相应式对象,如果有就必须强制触发。设置 getter 。 我们可以发现所有的监听数据源都会被包装成 getter ,这是因为底层都是调用 reactivity库 的 watchEffect ,而第一个参数必须是函数,当调用这个函数访问到的变量都会收集依赖。所以如果当前元素为 reactive 元素的时候需要遍历这个元素的所有值以便所有的变量都能收集到对应的依赖。
//()=>[proxy]传入的是一个函数 else if (shared.isFunction(source)) { if (cb) { //让getter为这个函数 getter = () => callWithErrorHandling(source, instance, 2); } else { //如果没有回调函数 getter = () => { if (instance && instance.isUn mount ed) { return; } if (cleanup) { cleanup(); } return callWithAsyncErrorHandling(source, instance, 3, [onCleanup]); }; } } //省略第四部分代码如果监听的数据是函数,先判断是否有 cb ,如果有 cb 则将监听源函数作为 getter 。 如果没有传递 cb ,那么这个函数将会作为 getter 和回调函数cb。 我们来详细说说 cleanup 的作用。先来看看 官方 的测试用例:
watch(async (onCleanup) => { const { response, cancel } = doAsyncWork(id.value) // `cancel` 会在 `id` 更改时调用 // 以便取消之前 // 未完成的请求 onCleanup(cancel) data.value = await response })它被用来做副作用清除。第一次调用 getter 的时候是作为收集依赖,所以 cleanup 为空不执行,然后调用 source函数 ,在这个函数中会收到 onCleanup 的参数,如果你在 source 函数中调用了 onCleanup 函数那么 cleanup 将会被赋值。当 id 发生改变之后再次调用 getter函数 (此时作为cb),这时候 cleanup 就会被调用,也就是官方说的cancle函数会在id更改时调用。 我们继续第四部分代码的分析:
//不是以上情况,让getter为空函数 else { getter = shared.NOOP; //警告 warnInvalidSource(source); } //省略第五部分代码这表示没有需要监听的数据源,将 getter 设置为空函数,同时警告用户。
const INITIAL_WATCHER_VALUE = {} //getter作为参数传入ReactiveEffect //调用run的时候会调用getter,确保 //所有的属性都能够收集到依赖 if (cb && deep) { const baseGetter = getter; getter = () => traverse(baseGetter()); } let cleanup; //调用effect.stop的时候触发这个函数 let onCleanup = (fn) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, 4); }; }; let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE; //省略第六部分代码对于含有 deep 属性的需要深度遍历,只要在getter中访问了所有变量的值那么这些值都会收集到依赖。 接下来便是 onCleanup 的实现,大家可以按照上面我说的进行理解。 我们知道在watch可以监听多个数据,那么对应的cb回调函数的参数要收集到这些改变的值。所以如果监听了多个数据源那么 oldValue 会被设置为 数组 否则为 对象 。
//回调函数 const job = () => { if (!effect.active) { return; } //传递了cb函数 if (cb) { //watch([a,b],()=>{}) //newValue=[a,b] const newValue = effect.run(); //未设置deep属性的 //旧值和新值要发生改变才会调用cb回调函数 if ( deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => shared.hasChanged(v, oldValue[i])) : shared.hasChanged(newValue, oldValue)) ) { //这里的作用上面我们已经讲过了,不在赘述。 if (cleanup) { cleanup(); } callWithAsyncErrorHandling(cb, instance, 3, [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup, ]); oldValue = newValue; } } else { //没有cb就只调用getter函数(watchEffect) effect.run(); } }; //省略第七部分代码这个 job 代表的是要传递给Vue调度器的任务,所以这是在创建一个调度器任务。 同时还需要注意这个 job 是监听的变量发生了改变后才会调用。 这里的 effect 代表的是 ReactiveEffect类的实例 ,如果还不了解这个类的请阅读 Vue3源码分析(2) 。 如果没有传递 cb 那么会调用 effect.run() 这个函数会去执行 getter函数 。因为没有传递 cb 所以回调函数就是 getter函数 。 如果存在 cb ,那么会先调用 getter函数 获取最新的 value ,然后再调用 cb ,所以不太建议自己将第一个参数写成函数,这样改变值的时候会调用getter和cb两个函数,如果你在getter中写了副作用那么就会多次调用。 同样 cleanup 用于清除副作用这里就不再赘述了。
//只要有cb则允许递归 job.allowRecurse = !!cb; let scheduler; //设置了sync则同步调度,不放入queue进行异步调度(同步) if (flush === "sync") { scheduler = job; } //设置了post放到DOM渲染之后执行(异步) else if (flush === "post") { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense); } //默认值为pre,放入到queue中执行(异步) //带有pre的会在DOM渲染前执行 else { job.pre = true; //给当前的job设置优先级 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); //将用户传递的onTrack和onTrigger赋值到effect上 //便于在track和trigger的时候调用 effect.onTrack = onTrack; effect.onTrigger = onTrigger; //省略第九部分代码onTrack :是reactivity库实现的 api 。当被追踪的时候调用这个函数。 onTrigger :当监视的变量改变的时候触发的函数。 创建ReactiveEffect实例对象,对变量进行监视。
//调用了watch之后 //需要立刻执行getter,处理不同的flush参数 if (cb) { if (immediate) { //有immediate参数立即执行job job(); } //否则就只收集依赖调用getter函数 //并且获取监听的变量 else { oldValue = effect.run(); } } //flush为post需要将收集依赖函数getter //放到postQueue中 else if (flush === "post") { queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ); } //没有设置则收集依赖 else { effect.run(); } //省略第十部分代码如果含有 immediate 参数则需要立刻执行 job任务 ,否则调用 effect.run() 方法(调用 getter )收集依赖。 如果 flush 设置为 post 那么收集依赖的操作也需要移动到后置队列当中。
//watch的停止函数,调用后不再依赖更新 return () => { effect.stop(); };watch 会返回一个方法用于取消监听。
watch总结
为了兼容选项式 watch 处理了不同的配置选项最终调用函数式的watch来实现的监视效果。 watch 拥有三个参数: source、cb、options 。 source 是监听源,可以传递函数,值,数组。但是最后都是包装成getter函数。实现的理念就是通过调用getter函数,访问响应式变量收集依赖,当响应式数据发生改变的时候调用cb。 options 中比较重要的配置是 flush ,他决定了何时收集依赖和触发依赖。当 flush为post 的时候需要知道收集依赖和触发依赖都将会推入到后置队列当中(DOM更新后触发)。以上就是Vue3源码分析调度器与watch用法原理的详细内容,更多关于Vue3调度器与watch的资料请关注其它相关文章!
您可能感兴趣的文章: 手摸手教你实现Vue3 Reactivity Vue3中正确使用ElementPlus的示例代码 vue3中的对象时为proxy对象如何获取值(两种方式) vue中下拉框组件的封装方式 vue3源码分析reactivity实现原理
总结
以上是 为你收集整理的 Vue3源码分析调度器与watch用法原理 全部内容,希望文章能够帮你解决 Vue3源码分析调度器与watch用法原理 所遇到的问题。
如果觉得 网站内容还不错, 推荐好友。
查看更多关于Vue3源码分析调度器与watch用法原理的详细内容...
声明:本文来自网络,不代表【好得很程序员自学网】立场,转载请注明出处:http://www.haodehen.cn/did203849