[Código fuente y biblioteca] El principio detrás de la magia de nextTick en Vue3

SiguienteTick Introducción

Según la breve introducción en el sitio web oficial, nextTickes 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 nextTicklas ideas generales de implementación y el uso:

Cuando Vuecambia el estado reactivo en un archivo, las actualizaciones finales DOMno se sincronizan, sino que se Vuealmacenan 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 que DOMse complete la actualización. Puede pasar una función de devolución de llamada como argumento o awaitdevolverla Promise.

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 nextTickhay dos formas de usarlo:

  • Pasar la función de devolución de llamada
nextTick(() => {// DOM 更新了
}) 
  • Devolver unPromise
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 es Vue3el mismo, pero se ESMintroduce sin usarlo.

Los resultados de ejecución son los siguientes:

En mi ejemplo, al hacer clic en el botón Agregar se countrealizará una operación de incremento. Este método se puede dividir en tres partes:

1. Utilice nextTicky utilice Promiseel uso mixto de la función de devolución de llamada y
2. countAgregue uno al par
3. Utilice nextTicky utilice Promiseel uso mixto de la función de devolución de llamada y

El primer registrado se ejecuta nextTickantes countde agregar uno y el segundo registrado se ejecuta nextTickdespués de countagregar uno.

Pero el resultado final es muy interesante:

callback before
render 1
promise before
callback after
promise after 

La primera nextTickfunción de devolución de llamada registrada se renderejecuta antes, Promisepero renderdespués.

La segunda nextTickfunción de devolución de llamada registrada se renderejecuta después, Promisepero renderdespués.

Y ambas nextTickfunciones de devolución de llamada tienen prioridad sobre Promisela ejecución.

¿Cómo explicar este fenómeno? Comenzaremos nextTickcon la implementación del análisis.

Implementación de nextTick

nextTickSolo hay más de 200 líneas de código fuente en packages/runtime-core/src/scheduler.tsel archivo. Si está interesado, puede ir directamente al tscó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: ¿ nextTickexiste sólo una cantidad tan pequeña de código? Si miramos más de cerca, encontramos nextTickque la implementación es en realidad una Promiseencapsulación.

Ignorando otras cosas por el momento, basta con mirar este código, podemos saber:

  • nextTickLo que se devuelve es unPromise
  • nextTickLa función de devolución de llamada se ejecuta en el método Promisedethen

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:

  • nextTickLo que se devuelve es unPromise
  • nextTickLa función de devolución de llamada se ejecuta en el método Promisedethen

Según Promiselas características de, sabemos Promiseque se puede llamar en cadena, por lo que podemos escribir así:

Promise.resolve().then(() => {// ...
}).then(() => {// ...
}).then(() => {// ...
}); 

Y según las características de, Promisecada vez Promisese devuelve uno nuevo Promise;

Al mismo tiempo, también sabemos Promiseque thenel 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 currentFlushPromisevariable en él, y esta variable se letdeclara usando. Todas las variables se constdeclaran usando. Esta variable se letusa 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: se currentFlushPromiseestablecerá en unPromise
  • flushJobs: se currentFlushPromiseestablecerá 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í:

  • queueFlushEs un método utilizado para actualizar la cola de tareas.
  • isFlushingIndica si es refrescante, pero no se utiliza en este método.
  • isFlushPendingIndica si hay tareas que deben actualizarse, que son tareas en cola.
  • currentFlushPromiseIndica tareas que actualmente deben actualizarse

Ahora combinado con la nextTickimplementación anterior, encontraremos un punto muy interesante: resolvedPromiseambos 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 Promisepara thenregistrar múltiples funciones de devolución de llamada, y todas las tareas que deben actualizarse se registran en el mismo Promisemé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 queueFlushmétodo anterior, sabemos que queueFlushes 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 flushJobsmé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" />
} 

flushJobsPrimero, se configurará isFlushPendingen false, el lote actual de tareas ya comenzó a actualizarse, por lo que no hay necesidad de esperar, y luego se configurará isFlushingen true, lo que indica que se está actualizando.

Esto queueFlushes exactamente lo opuesto al método, pero sus funciones se complementan entre sí, queueFlushlo que significa que actualmente hay una tarea que requiere atributos y flushJobsque la tarea se está actualizando actualmente.

La ejecución de tareas se callWithErrorHandlingrealiza 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 onErrorCapturedmétodo para su procesamiento.

Las tareas de actualización se almacenan en queueatributos. Esta queuees la cola de tareas que mencionamos anteriormente. Lo que se almacena en esta cola de tareas es la tarea que necesitamos actualizar.

Finalmente, borre queuey ejecute flushPostFlushCbsel método. flushPostFlushCbsEl método generalmente almacena devoluciones de llamada del ciclo de vida, como mounted, updatedetc.

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 queueJobel 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 jobnecesitamos actualizar, pero esta función expandirá algunos atributos, como id, etc.preactive

Hay un par de definiciones de tipos en tsel 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
} 

queueJobEl método primero determina queuesi 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, mapetc. Estos métodos se activarán cuando se ejecuten gettery el método getterse activará a su vez queueJob, lo que provocará llamadas recursivas.

Por lo tanto, se juzgará aquí isFlushing. Si es refrescante, se establecerá flushIndexen +1;

flushIndexEs el índice de la tarea que se está actualizando actualmente +1y luego la búsqueda comienza desde la siguiente tarea, de modo que la misma tarea no se agregue repetidamente y provoque llamadas recursivas.

La watchdevolución de llamada se puede llamar de forma recursiva, porque esto está controlado por el usuario, por lo que hay un allowRecurseatributo adicional aquí. Si es watchuna devolución de llamada, se establecerá allowRecurseen true.

Esto puede evitar el problema de las llamadas recursivas, que es un diseño muy inteligente.

queueJobEl último se exporta, este se utiliza para que otros módulos agreguen tareas, como watchEffect, watchetc.

descargaPostFlushCbs

flushPostFlushCbsLos métodos se utilizan para ejecutar devoluciones de llamadas del ciclo de vida, como mounted, updatedetc.

flushPostFlushCbsNo entraré en demasiados detalles, el proceso general es flushJobsmás o menos el mismo;

La diferencia es que flushPostFlushCbsse 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 demoel ejemplo al principio del artículo, revisemos demoprimero 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: nextTickcuando registramos el primero, queueno había ninguna tarea en él;

Y nextTickno se llamará al método queueJob, ni flushJobsse llamará al método, por lo que la cola de tareas no se actualizará en este momento.

Pero resolvedPromisees un éxito promise, por lo que nextTickla función de devolución de llamada pasada se colocará en la cola de microtareas, esperando su ejecución.

nextTickTambié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 promisea la función de devolución de llamada.thennextTick

Luego lo ejecutamos this.count++. No hemos tocado la lógica de implementación interna aquí todavía. Solo necesitamos saber que activará queueJobel método y agregará la tarea a la cola de tareas.

Finalmente lo ejecutamos nuevamente nextTick, en este momento queueya había una tarea, por lo que flushJobsse llamó al método para ejecutar las tareas en la cola de tareas en secuencia.

Énfasis: Y currentFlushPromisehay un valor en este momento. El valor se resolvedPromisedevuelve 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, currentFlushPromiseusó ;undefinedresolvedPromise

Se puede entender que la primera nextTickvez que se ejecuta flushJobsse 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 currentFlushPromisela registrada por el método .PromiseflushJobsPromise

Esto garantiza que nextTickla función de devolución de llamada registrada se flushJobsejecutará 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í nextTickes 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 nextTickdevuelta ;promisethen

Debido a que nextTicklas sumas devueltas no promiseson currentFlushPromiselas mismas promise, lo que se nextTickdevuelve es una sola tarea y la prioridad es mayor .promisethencurrentFlushPromise

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 currentFlushPromisela thenfunción de devolución de llamada, y también es una llamada flushJobs, a la que flushJobsse le asigna el valor resolvedPromisedevuelto .PromisecurrentFlushPromise

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 Vue3que los principios de implementación son muy inteligentes.nextTicknextTick

nextTickEl principio de implementación lo Promiseimplementa , nextTickque devolverá uno Promisey nextTickla 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, nextTickla 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 Promisela 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.

Supongo que te gusta

Origin blog.csdn.net/web2022050901/article/details/129406826
Recomendado
Clasificación