SiguienteTick Introducción
Según la breve introducción en el sitio web oficial, nextTick
es un método de herramienta para esperar la próxima actualización de DOM.
La definición de tipo es la siguiente:
function nextTick(callback?: () => void): Promise<void> {}
Luego, de acuerdo con la introducción detallada en el sitio web oficial, podemos conocer nextTick
las ideas generales de implementación y el uso:
Cuando
Vue
cambia el estado reactivo en un archivo, las actualizaciones finalesDOM
no se sincronizan, sino que seVue
almacenan en caché en una cola hasta el siguiente "tic". Esto es para garantizar que cada componente solo realice una actualización independientemente de cuántos cambios de estado ocurran.
nextTick()
Se puede utilizar inmediatamente después de un cambio de estado para esperar a queDOM
se complete la actualización. Puede pasar una función de devolución de llamada como argumento oawait
devolverlaPromise
.
La explicación en el sitio web oficial ya es muy detallada, así que no la sobreinterpretaré, el siguiente paso es el análisis.
Algunos detalles y uso de nextTick
Uso de nextTick
Primero, según la introducción en el sitio web oficial, podemos saber que nextTick
hay dos formas de usarlo:
- Pasar la función de devolución de llamada
nextTick(() => {// DOM 更新了
})
- Devolver un
Promise
nextTick().then(() => {// DOM 更新了
})
Entonces, ¿se pueden combinar estos dos métodos?
nextTick(() => {// DOM 更新了
}).then(() => {// DOM 更新了
})
siguientefenómeno de garrapatas
Escribí uno muy simple demo
, descubrí que se puede mezclar y descubrí un fenómeno interesante:
const {createApp, h, nextTick} = Vue;
const app = createApp({data() {return {count: 0};},methods: {push() {nextTick(() => {console.log('callback before');}).then(() => {console.log('promise before');});this.count++;nextTick(() => {console.log('callback after');}).then(() => {console.log('promise after');});}},render() {console.log('render', this.count);const pushBtn = h("button", {innerHTML: "增加",onClick: this.push});const countText = h("p", {innerHTML: this.count});return h("div", {}, [pushBtn, countText]);}
});
app.mount("#app");
Lo uso aquí por simplicidad
vue.global.js
, el método de uso esVue3
el mismo, pero seESM
introduce sin usarlo.
Los resultados de ejecución son los siguientes:
En mi ejemplo, al hacer clic en el botón Agregar se count
realizará una operación de incremento. Este método se puede dividir en tres partes:
1. Utilice nextTick
y utilice Promise
el uso mixto de la función de devolución de llamada y
2. count
Agregue uno al par
3. Utilice nextTick
y utilice Promise
el uso mixto de la función de devolución de llamada y
El primer registrado se ejecuta nextTick
antes count
de agregar uno y el segundo registrado se ejecuta nextTick
después de count
agregar uno.
Pero el resultado final es muy interesante:
callback before
render 1
promise before
callback after
promise after
La primera nextTick
función de devolución de llamada registrada se render
ejecuta antes, Promise
pero render
después.
La segunda nextTick
función de devolución de llamada registrada se render
ejecuta después, Promise
pero render
después.
Y ambas nextTick
funciones de devolución de llamada tienen prioridad sobre Promise
la ejecución.
¿Cómo explicar este fenómeno? Comenzaremos nextTick
con la implementación del análisis.
Implementación de nextTick
nextTick
Solo hay más de 200 líneas de código fuente en packages/runtime-core/src/scheduler.ts
el archivo. Si está interesado, puede ir directamente al ts
código fuente de la versión. Seguiremos mirando el código fuente empaquetado.
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
function nextTick(fn) {const p = currentFlushPromise || resolvedPromise;return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
A primera vista, la gente se queda estupefacta: ¿ nextTick
existe sólo una cantidad tan pequeña de código? Si miramos más de cerca, encontramos nextTick
que la implementación es en realidad una Promise
encapsulación.
Ignorando otras cosas por el momento, basta con mirar este código, podemos saber:
nextTick
Lo que se devuelve es unPromise
nextTick
La función de devolución de llamada se ejecuta en el métodoPromise
dethen
Ahora, volviendo a lo que dijimos antes demo
, en realidad hemos encontrado parte de la respuesta:
nextTick(() => {console.log('callback before');
}).then(() => {console.log('promise before');
});
this.count++;
La secuencia de ejecución final anterior, expresada en código, es:
function nextTick(fn) {// 2. 返回一个 Promise, 并且在 Promise 的 then 方法中执行回调函数return Promise.resolve().then(fn);
}
// 1. 调用 nextTick,注册回调函数
const p = nextTick(() => {console.log('callback before');
})
// 3. 在 Promise 的 then 方法注册一个新的回调
p.then(() => {console.log('promise before');
});
// 4. 执行 count++
this.count++;
Del código desensamblado, lo que podemos ver es:
nextTick
Lo que se devuelve es unPromise
nextTick
La función de devolución de llamada se ejecuta en el métodoPromise
dethen
Según Promise
las características de, sabemos Promise
que se puede llamar en cadena, por lo que podemos escribir así:
Promise.resolve().then(() => {// ...
}).then(() => {// ...
}).then(() => {// ...
});
Y según las características de, Promise
cada vez Promise
se devuelve uno nuevo Promise
;
Al mismo tiempo, también sabemos Promise
que then
el método se ejecuta de forma asincrónica, por lo que tenemos algunas conjeturas sobre el orden de ejecución del código anterior, pero no podemos sacar una conclusión ahora, sigamos profundizando.
Detalles de implementación de nextTick
Aunque el código fuente anterior es muy corto, hay una currentFlushPromise
variable en él, y esta variable se let
declara usando. Todas las variables se const
declaran usando. Esta variable se let
usa para declarar y debe estar en stock.
A través de la búsqueda, podemos encontrar dónde se usa esta variable y encontrar que hay dos métodos para usar esta variable:
queueFlush
: securrentFlushPromise
establecerá en unPromise
flushJobs
: securrentFlushPromise
establecerá ennull
colaVaciar
// 是否正在刷新
let isFlushing = false;
// 是否有任务需要刷新
let isFlushPending = false;
// 刷新任务队列
function queueFlush() {// 如果正在刷新,并且没有任务需要刷新if (!isFlushing && !isFlushPending) {// 将 isFlushPending 设置为 true,表示有任务需要刷新isFlushPending = true;// 将 currentFlushPromise 设置为一个 Promise, 并且在 Promise 的 then 方法中执行 flushJobscurrentFlushPromise = resolvedPromise.then(flushJobs);}
}
De hecho, estos códigos son fáciles de entender sin necesidad de escribir comentarios. Puedes saber el significado viendo el nombre. De hecho, ya puedes verlo aquí:
queueFlush
Es un método utilizado para actualizar la cola de tareas.isFlushing
Indica si es refrescante, pero no se utiliza en este método.isFlushPending
Indica si hay tareas que deben actualizarse, que son tareas en cola.currentFlushPromise
Indica tareas que actualmente deben actualizarse
Ahora combinado con la nextTick
implementación anterior, encontraremos un punto muy interesante: resolvedPromise
ambos están en uso:
const resolvedPromise = Promise.resolve();
function nextTick(fn) {// nextTick 使用 resolvedPromise return resolvedPromise.then(fn);
}
function queueFlush() {// queueFlush 也使用 resolvedPromisecurrentFlushPromise = resolvedPromise.then(flushJobs);
}
Simplificando el código anterior, en realidad se ve así:
const resolvedPromise = Promise.resolve();
resolvedPromise.then(() => {// ...
});
resolvedPromise.then(() => {// ...
});
De hecho, el método se utiliza Promise
para then
registrar múltiples funciones de devolución de llamada, y todas las tareas que deben actualizarse se registran en el mismo Promise
método then
, de modo que se pueda garantizar el orden de ejecución de estas tareas, que es una cola.
trabajos de descarga
En el queueFlush
método anterior, sabemos que queueFlush
es un método utilizado para actualizar la cola de tareas;
Entonces, ¿qué tareas deberían actualizarse? De todos modos, lo último que se pasa es un flushJobs
método, y también se usa en este método currentFlushPromise
. ¿No es solo una cadena? Echemos un vistazo:
// 任务队列
const queue = [];
// 当前正在刷新的任务队列的索引
let flushIndex = 0;
// 刷新任务
function flushJobs(seen) {// 将 isFlushPending 设置为 false,表示当前没有任务需要等待刷新了isFlushPending = false;// 将 isFlushing 设置为 true,表示正在刷新isFlushing = true;// 非生产环境下,将 seen 设置为一个 Mapif ((process.env.NODE_ENV <img src="https://github.com/evanw/esbuild/issues/1610)const check = (process.env.NODE_ENV !== 'production')? (job) => checkRecursiveUpdates(seen, job): NOOP;// 检测递归调用是一个非常巧妙的操作,感兴趣的可以去看看源码,这里不做讲解try {for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {const job = queue[flushIndex];if (job && job.active !== false) {if ((process.env.NODE_ENV !== 'production') && check(job)) {continue;}// 执行任务callWithErrorHandling(job, null, 14 /* ErrorCodes.SCHEDULER */);}}}finally {// 重置 flushIndexflushIndex = 0;// 快速清空队列,直接给 数组的 length属性 赋值为 0 就可以清空数组queue.length = 0;// 刷新生命周期的回调flushPostFlushCbs(seen);// 将 isFlushing 设置为 false,表示当前刷新结束isFlushing = false;// 将 currentFlushPromise 设置为 null,表示当前没有任务需要刷新了currentFlushPromise = null;// pendingPostFlushCbs 存放的是生命周期的回调,// 所以可能在刷新的过程中又有新的任务需要刷新// 所以这里需要判断一下,如果有新添加的任务,就需要再次刷新if (queue.length || pendingPostFlushCbs.length) {flushJobs(seen);}" style="margin: auto" />
}
flushJobs
Primero, se configurará isFlushPending
en false
, el lote actual de tareas ya comenzó a actualizarse, por lo que no hay necesidad de esperar, y luego se configurará isFlushing
en true
, lo que indica que se está actualizando.
Esto queueFlush
es exactamente lo opuesto al método, pero sus funciones se complementan entre sí, queueFlush
lo que significa que actualmente hay una tarea que requiere atributos y flushJobs
que la tarea se está actualizando actualmente.
La ejecución de tareas se callWithErrorHandling
realiza a través de métodos, el código interno es muy simple, que consiste en ejecutar el método y capturar errores durante la ejecución, y luego entregar los errores al onErrorCaptured
método para su procesamiento.
Las tareas de actualización se almacenan en queue
atributos. Esta queue
es la cola de tareas que mencionamos anteriormente. Lo que se almacena en esta cola de tareas es la tarea que necesitamos actualizar.
Finalmente, borre queue
y ejecute flushPostFlushCbs
el método. flushPostFlushCbs
El método generalmente almacena devoluciones de llamada del ciclo de vida, como mounted
, updated
etc.
Adición de tareas a la cola
Como se mencionó anteriormente queue
, queue
¿cómo se agregan tareas?
A través de la búsqueda, podemos localizar queueJob
el método que se utiliza para agregar tareas:
// 添加任务,这个方法会在下面的 queueFlush 方法中被调用
function queueJob(job) {// 通过 Array.includes() 的 startIndex 参数来搜索任务队列中是否已经存在相同的任务// 默认情况下,搜索的起始索引包含了当前正在执行的任务// 所以它不能递归地再次触发自身// 如果任务是一个 watch() 回调,那么搜索的起始索引就是 +1,这样就可以递归调用了// 但是这个递归调用是由用户来保证的,不能无限递归if (!queue.length ||!queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) {// 如果任务没有 id 属性,那么就将任务插入到任务队列中if (job.id == null) {queue.push(job);}// 如果任务有 id 属性,那么就将任务插入到任务队列的合适位置else {queue.splice(findInsertionIndex(job.id), 0, job);}// 刷新任务队列queueFlush();}
}
Aquí hay una función, que es la tarea que job
necesitamos actualizar, pero esta función expandirá algunos atributos, como id
, etc.pre
active
Hay un par de definiciones de tipos en ts
el código fuente job
:
export interface SchedulerJob extends Function {// id 就是排序的依据id?: number// 在 id 相同的情况下,pre 为 true 的任务会先执行// 这个在刷新任务队列的时候,在排序的时候会用到,本文没有讲解这方面的内容pre?: boolean// 标识这个任务是否明确处于非活动状态,非活动状态的任务不会被刷新active?: boolean// 标识这个任务是否是 computed 的 gettercomputed?: boolean/** * 表示 effect 是否允许在由 scheduler 管理时递归触发自身。 * 默认情况下,scheduler 不能触发自身,因为一些内置方法调用,例如 Array.prototype.push 实际上也会执行读取操作,这可能会导致令人困惑的无限循环。 * 允许的情况是组件更新函数和 watch 回调。 * 组件更新函数可以更新子组件属性,从而触发“pre”watch回调,该回调会改变父组件依赖的状态。 * watch 回调不会跟踪它的依赖关系,因此如果它再次触发自身,那么很可能是有意的,这是用户的责任来执行递归状态变更,最终使状态稳定。 */allowRecurse?: boolean/** * 在 renderer.ts 中附加到组件的渲染 effect 上用于在报告最大递归更新时获取组件信息。 * 仅限开发。 */ownerInstance?: ComponentInternalInstance
}
queueJob
El método primero determina queue
si la misma tarea ya existe y, si existe, no es necesario volver a agregarla.
Esto trata principalmente el problema de las llamadas recursivas, porque la mayoría de las tareas almacenadas aquí se activan cuando modificamos los datos;
Al modificar datos, se utilizan métodos de matriz, como forEach
, map
etc. Estos métodos se activarán cuando se ejecuten getter
y el método getter
se activará a su vez queueJob
, lo que provocará llamadas recursivas.
Por lo tanto, se juzgará aquí isFlushing
. Si es refrescante, se establecerá flushIndex
en +1
;
flushIndex
Es el índice de la tarea que se está actualizando actualmente +1
y luego la búsqueda comienza desde la siguiente tarea, de modo que la misma tarea no se agregue repetidamente y provoque llamadas recursivas.
La watch
devolución de llamada se puede llamar de forma recursiva, porque esto está controlado por el usuario, por lo que hay un allowRecurse
atributo adicional aquí. Si es watch
una devolución de llamada, se establecerá allowRecurse
en true
.
Esto puede evitar el problema de las llamadas recursivas, que es un diseño muy inteligente.
queueJob
El último se exporta, este se utiliza para que otros módulos agreguen tareas, como watchEffect
, watch
etc.
descargaPostFlushCbs
flushPostFlushCbs
Los métodos se utilizan para ejecutar devoluciones de llamadas del ciclo de vida, como mounted
, updated
etc.
flushPostFlushCbs
No entraré en demasiados detalles, el proceso general es flushJobs
más o menos el mismo;
La diferencia es que flushPostFlushCbs
se realizará una copia de seguridad de las tareas y luego se ejecutarán secuencialmente, y las excepciones no se detectarán, sino que se llamarán directamente.
Los estudiantes interesados pueden comprobar el código fuente ellos mismos.
El comienzo del problema.
Volviendo a la pregunta original, que es demo
el ejemplo al principio del artículo, revisemos demo
primero el código:
nextTick(() => {console.log('callback before');
}).then(() => {console.log('promise before');
});
this.count++;
nextTick(() => {console.log('callback after');
}).then(() => {console.log('promise after');
});
El resultado impreso es:
callback before
render 1
promise before
callback after
promise after
De hecho, al mirar el código fuente queda muy claro: nextTick
cuando registramos el primero, queue
no había ninguna tarea en él;
Y nextTick
no se llamará al método queueJob
, ni flushJobs
se llamará al método, por lo que la cola de tareas no se actualizará en este momento.
Pero resolvedPromise
es un éxito promise
, por lo que nextTick
la función de devolución de llamada pasada se colocará en la cola de microtareas, esperando su ejecución.
nextTick
También se devolverá uno promise
, por lo que la función de devolución de llamada que devolvemos también se colocará en la cola de microtareas, pero definitivamente quedará rezagada con respecto promise
a la función de devolución de llamada.then
nextTick
Luego lo ejecutamos this.count++
. No hemos tocado la lógica de implementación interna aquí todavía. Solo necesitamos saber que activará queueJob
el método y agregará la tarea a la cola de tareas.
Finalmente lo ejecutamos nuevamente nextTick
, en este momento queue
ya había una tarea, por lo que flushJobs
se llamó al método para ejecutar las tareas en la cola de tareas en secuencia.
Énfasis: Y currentFlushPromise
hay un valor en este momento. El valor se resolvedPromise
devuelve después de que se completa la ejecución Promise
.
La diferencia con la primera vez es que la primera vez que se ejecutó nextTick
, currentFlushPromise
usó ;undefined
resolvedPromise
Se puede entender que la primera nextTick
vez que se ejecuta flushJobs
se utiliza la misma tarea que el método de registro Promise
.
Cuando se ejecuta por segunda vez nextTick
, la tarea utilizada no es la misma que currentFlushPromise
la registrada por el método .Promise
flushJobs
Promise
Esto garantiza que nextTick
la función de devolución de llamada registrada se flushJobs
ejecutará después de la función de devolución de llamada registrada del método.
El proceso específico se puede ver en el siguiente ejemplo de código:
const resolvedPromise = Promise.resolve();
let count = 0;
// 第一次注册 nextTick
resolvedPromise.then(() => {console.log('callback before', count);
}).then(() => {console.log('promise before', count);
});
// 执行 this.count++
// 这里会触发 queueJob 方法,将任务添加到任务队列中
const currentFlushPromise = resolvedPromise.then(() => {count++;console.log('render', count);
});
// 第二次注册 nextTick
currentFlushPromise.then(() => {console.log('callback after', count);
}).then(() => {console.log('promise after', count);
});
Puede ejecutar el resultado del código anterior en su navegador y encontrará que cumple con nuestras expectativas.
El proceso específico se puede ver en la siguiente figura:
graph TD
A[resolvedPromise] -->|注册 nextTick 回调| B[nextTick callback before]
B -->|在 nextTick 返回的 promise 注册 then 的回调| C[nextTick promise then]
A -->|执行 value++ 会触发 queueJob| D[value++]
D -->|执行 flushJobs 会将 resolvedPromise 返回的 promise 赋值到 currentFlushPromise| E[currentFlushPromise]
E -->|注册 nextTick 回调使用的是 currentFlushPromise| F[nextTick callback after]
F -->|在 nextTick 返回的 promise 注册 then 的回调| G[nextTick promise after]
Se ha ejecutado la macro tarea sincronizada anterior y el siguiente paso es la cola de micro tareas, el proceso es el siguiente:
graph TD
A[resolvedPromise] -->|直接调用 then 里面注册的回调函数| B[then callbacks]
B -->|注册了多个,依次执行| C[nextTick callback before]
C -->|注册了多个,依次执行| D[value++]
De esta manera, la segunda ola de tareas también terminó, esta vez la tarea es principalmente actualizar la cola de tareas, lo que se ejecuta aquí nextTick
es en realidad la tarea anterior tick
(ahora entiendes 直到下一个“tick”才一起执行
lo que significa en el sitio web oficial).
Luego ejecuta el siguiente tick
(a eso me refiero, cabeza de perro manual), el proceso es el siguiente:
graph TD
A[nextTick promise then] -->|因为是先注册的,所以先执行| B[nextTick promise before]
Se acabó, sí, la tarea esta vez es ejecutar la función de devolución de llamada nextTick
devuelta ;promise
then
Debido a que nextTick
las sumas devueltas no promise
son currentFlushPromise
las mismas promise
, lo que se nextTick
devuelve es una sola tarea y la prioridad es mayor .promise
then
currentFlushPromise
Una vez finalizada esta misión, habrá otra tick
. El proceso es el siguiente:
graph TD
A[currentFlushPromise then] -->|因为是后注册的,所以相对于上面的后执行| B[nextTick callback after]
Esta vez la tarea es ejecutar currentFlushPromise
la then
función de devolución de llamada, y también es una llamada flushJobs
, a la que flushJobs
se le asigna el valor resolvedPromise
devuelto .Promise
currentFlushPromise
Esta misión ha terminado y es la última tick
, el proceso es el siguiente:
graph TD
A[nextTick promise after] -->|最后一个| B[nextTick promise after]
En este punto, el proceso ha terminado. El proceso consume mucho cerebro, pero después de comprenderlo, lo encontré muy inteligente. Ha mejorado enormemente mi capacidad de pensamiento y mi comprensión de lo asincrónico.
Resumir
Este artículo analiza principalmente los principios de implementación. Al analizar el código fuente, encontramos Vue3
que los principios de implementación son muy inteligentes.nextTick
nextTick
nextTick
El principio de implementación lo Promise
implementa , nextTick
que devolverá uno Promise
y nextTick
la función de devolución de llamada se colocará en la cola de microtareas, esperando su ejecución.
Si se registra mientras hay tareas en cola nextTick
, nextTick
la función de devolución de llamada se ejecutará después de que se ejecuten las tareas en la cola de tareas.
La idea utilizada aquí es muy simple: aprovechar Promise
la función de llamada encadenable. Es posible que todos la hayan usado en el desarrollo diario, pero no esperaba que se usara de esta manera. Es realmente muy inteligente.
por fin
Hemos recopilado 75 preguntas de entrevistas de alta frecuencia de JS y proporcionado respuestas y análisis, lo que básicamente puede garantizar que pueda responder a las preguntas del entrevistador sobre JS.
Los amigos necesitados pueden hacer clic en la tarjeta a continuación para recibirla y compartirla gratis.