35.a-nextTick源码解析
vue 渲染 dom 是异步的,这会出现一个问题,修改 ref 类型的值,同时 console 输出 dom 元素值 为原来的值,需要等待下一个 tick
nextTick
为了性能优化
nextTick 就是创建一个异步任务,那么它自然要等到同步任务执行完成后才执行。
<template>
<div ref="xiaoman">
{{ text }}
</div>
<button @click="change">change div</button>
</template>
<script setup lang='ts'>
import { ref,nextTick } from 'vue';
const text = ref('小满开飞机')
const xiaoman = ref<HTMLElement>()
const change = async () => {
text.value = '小满不开飞机'
console.log(xiaoman.value?.innerText) //小满开飞机
await nextTick();
console.log(xiaoman.value?.innerText) //小满不开飞机
}
</script>
<style scoped>
</style>
面试题,小坑
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
})
}
// 5 5 5 5 5
setTimeout 是异步执行的,1000 毫秒后向任务队列里添加一个任务,只有主线上的全部执行完才会执行任务队列里的任务,所以当主线程 for 循环执行完之后 i 的值为 5,这个时候再去任务队列中执行任务,i 全部为 5;
每次 for 循环的时候 setTimeout 都会执行,但是里面的 function 则不会执行被放入任务队列,因此放了 5 次;for 循环的 5 次执行完之后不到 1000 毫秒;
1000 毫秒后全部执行任务队列中的函数,所以就是输出五个 5 啦
假如把 var 换成 let,那么输出结果为 0,1,2,3,4;
因为 let i 的是区块变量,每个 i 只能存活到大括号结束,并不会把后面的 for 循环的 i 值赋给前面的 setTimeout 中的 i;而 var i 则是局部变量,这个 i 的生命周期不受 for 循环的大括号限制;
源码
源码地址 core\packages\runtime-core\src\scheduler.ts
const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
nextTick 接受一个参数 fn(函数)定义了一个变量 P 这个 P 最终返回都是 Promise,最后是 return 如果传了 fn 就使用变量 P.then 执行一个微任务去执行 fn 函数,then 里面 this 如果有值就调用 bind 改变 this 指向返回新的函数,否则直接调用 fn,如果没传 fn,就返回一个 promise,最终结果都会返回一个 promise
在我们之前讲过的 ref 源码中有一段 triggerRefValue 他会去调用 triggerEffects
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
//当响应式对象发生改变后,执行 effect 如果有 scheduler 这个参数,会执行这个 scheduler 函数
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
那么 scheduler 这个函数从哪儿来的 我们看这个类 ReactiveEffect
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null, //我在这儿
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
scheduler 作为一个参数传进来的
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
))
他是在初始化 effect 通过 queueJob 传进来的
//queueJob 维护job列队,有去重逻辑,保证任务的唯一性,每次调用去执行,被调用的时候去重,每次调用去执行 queueFlush
export function queueJob(job: SchedulerJob) {
// 判断条件:主任务队列为空 或者 有正在执行的任务且没有在主任务队列中 && job 不能和当前正在执行任务及后面待执行任务相同
// 重复数据删除:
// - 使用Array.includes(Obj, startIndex) 的 起始索引参数:startIndex
// - startIndex默认为包含当前正在运行job的index,此时,它不能再次递归触发自身
// - 如果job是一个watch()回调函数或者当前job允许递归触发,则搜索索引将+1,以允许他递归触发自身-用户需要确保回调函数不会死循环
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
queueJob 维护 job 列队 并且调用 queueFlush
function queueFlush() {
// 避免重复调用flushJobs
if (!isFlushing && !isFlushPending) {
isFlushPending = true
//开启异步任务处理flushJobs
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
queueFlush 给每一个队列创建了微任务