Эта статья участвовала в "Проект «Звезда раскопок»», чтобы выиграть творческие подарочные наборы и бросить вызов творческим поощрениям.
Мы все используем nextTick, и все мы знаем, что функция nextTick заключается в выполнении отложенного обратного вызова после окончания следующего цикла обновления DOM, после чего вы можете получить обновленную информацию, связанную с DOM.
Так как же это реализовано и в чем разница между Vue2 и Vue3? В этой статье будет представлен принцип выполнения в сочетании с кейсом, а затем углубитесь в исходный код, все комментарии, и вы увидите это с первого взгляда.
Прежде чем перейти к принципу реализации nextTick, давайте немного рассмотрим механизм выполнения JS, потому что он тесно связан с реализацией nextTick.
Механизм выполнения JS
Мы все знаем, что JS является однопоточным и может делать только одну вещь за раз, а именноСинхронизировать, то есть все задачи должны быть поставлены в очередь, а последующие задачи должны ждать выполнения предыдущих задач, прежде чем они смогут быть выполнены.Если предыдущие задачи занимают слишком много времени, последние задачи должны ждать все время , что сильно влияет на пользовательский опыт, поэтому только ПоявилисьасинхронныйКонцепция чего-либо
同步任务: относится к задачам, поставленным в очередь для последовательного выполнения в основном потоке.
异步任务: Задачи, которые не входят в основной поток, но входят в очередь задач, делятся на макрозадачи и микрозадачи.
宏任务: Рендеринг событий, запросов, скриптов, setTimeout, setInterval, setImmediate в узле и т. д.
微任务: Promise.then, MutationObserver (прослушивание DOM), Process.nextTick в узле и т. д.
Когда задача синхронизации в стеке выполнения выполняется, она переходит в очередь задач, чтобы взять задачу макроса и поместить ее в стек выполнения для выполнения.Макрозадача, все микрозадачи, рендеринг, макрозадача, все микрозадачи, рендеринг... (не все микрозадачи потом будут выполнять рендеринг), образуя таким образом цикл, т.е.事件循环(EventLoop)
nextTickЯвляется ли создание асинхронной задачи, тогда она, естественно, будет ждать, пока синхронная задача не будет выполнена перед выполнением
Давайте сначала разберемся с принципом выполнения на примерах, а потом углубимся в исходный код
Vue2
использование nextTick
См. пример, например, когда содержимое DOM изменяется, нам нужно получить последнюю высоту
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
data() {
return {
name: ""
}
},
mounted() {
console.log(this.$el.clientHeight) // 0
this.name = "沐华"
console.log(this.$el.clientHeight) // 0
this.$nextTick(() => {
console.log(this.$el.clientHeight) // 18
});
}
};
</script>
Почему я могу получить последнюю информацию, связанную с DOM, в nextTick? Как у вас получилось, разберем принцип
Принципиальный анализ
в исполненииthis.name = '沐华', это вызоветWatcherОбновление, наблюдатель поставит себя в очередь
Причина использования очереди в том, что, например, если представление обновляется несколько раз с несколькими изменениями данных, производительность не очень хорошая, поэтому для обновления представления делается асинхронная очередь обновления, чтобы избежать повторных вычислений и ненужных операций DOM. , В следующем раунде цикла событий Когда очередь обновляется, выполняется дедуплицированная задача (функция обратного вызова nextTick), и представление обновляется.
тогда позвониnextTick(), исходный код обновления адаптивного дистрибутива вот такой вот в этом блоке, адрес:src/core/observer/scheduler.js - 164行
export function queueWatcher (watcher: Watcher) {
...
// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
nextTick(flushSchedulerQueue)
}
этот параметрflushSchedulerQueueМетод будет помещен в цикл событий, и функция будет выполнена после завершения задачи основного потока, сортировки очереди наблюдателя, обхода, выполнения метода запуска, соответствующего наблюдателю, а затем рендеринга и обновления представления.
то естьthis.name = '沐华'Когда очередь задач может быть просто понята как это[flushSchedulerQueue]
затем следующая строкаconsole.log(...), из-за задач, которые обновляют представлениеflushSchedulerQueueВ очереди задач нет выполнения, поэтому обновленное представление не может быть получено
затем выполните дляthis.$nextTick(fn)При добавлении асинхронной задачи очередь задач в это время может быть просто понята как эта[flushSchedulerQueue, fn]
Затем выполняется задача синхронизации, а затем задачи в очереди задач выполняются по порядку.Первое выполнение задачи обновит представление, а обновленное представление, естественно, будет получено позже.
Анализ исходного кода nextTick
Исходная версия:2.6.14, адрес источника:src/core/util/next-tick.js
Здесь весь исходный код разделен на две части, одна из которых предназначена для определения наиболее подходящей в текущей среде.APIИ сохранить асинхронные функции, другой - вызвать асинхронные функции для выполнения очереди обратного вызова.
экологическое суждение
В основном это нужно для того, чтобы решить, какую макрозадачу или микрозадачу использовать, потому что макрозадача требует больше времени, чем микрозадача, поэтому сначала используется микрозадача, и порядок оценки следующий.
PromiseMutationObserversetImmediatesetTimeout
export let isUsingMicroTask = false // 是否启用微任务开关
const callbacks = [] // 回调队列
let pending = false // 异步控制开关,标记是否正在执行回调函数
// 该方法负责执行队列中的全部回调
function flushCallbacks () {
// 重置异步开关
pending = false
// 防止nextTick里有nextTick出现的问题
// 所以执行之前先备份并清空回调队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行任务队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 用来保存调用异步任务方法
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 保存一个异步任务
const p = Promise.resolve()
timerFunc = () => {
// 执行回调函数
p.then(flushCallbacks)
// ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
// 所以用一个空的计时器来强制刷新任务队列
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 不支持 Promise 的话,在支持MutationObserver的非 IE 环境下
// 如 PhantomJS, iOS7, Android 4.4
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 以上都不支持的情况下,使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
Когда оценка среды закончится, будет получена отложенная функция обратного вызова.timerFunc
Затем введите ядро nextTick
nextTick()
мы используемVue.nextTick()илиthis.$nextTick()все звонкиnextTick()Сюда
Здесь не так много кода, основная логика такова:
- Поместите входящую функцию обратного вызова в очередь обратного вызова
callbacks - Выполнить сохраненную асинхронную задачу
timeFunc, он пройдетcallbacksВыполнить соответствующую функцию обратного вызова
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把回调函数放入回调队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
// 如果异步开关是开的,就关上,表示正在执行回调函数,然后执行回调函数
pending = true
timerFunc()
}
// 如果没有提供回调,并且支持 Promise,就返回一个 Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
Вы можете видеть, что в конце есть возвратPromiseЕго можно использовать, когда мы не передаем параметры, как показано ниже.
this.$nextTick().then(()=>{ ... })
Vue3
использование nextTick
Давайте рассмотрим пример, нажмите кнопку, чтобы обновить содержимое DOM, и получите последнее содержимое DOM.
<template>
<div ref="test">{{name}}</div>
<el-button @click="handleClick">按钮</el-button>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const name = ref("沐华")
const test = ref(null)
async function handleClick(){
name.value = '掘金'
console.log(test.value.innerText) // 沐华
await nextTick()
console.log(test.value.innerText) // 掘金
}
return { name, test, handleClick }
</script>
Эта часть Vue3 была сильно изменена, но принцип работы цикла событий остался прежним, но добавлено несколько методов обслуживания очереди, а такжеeffect, но, к счастью, здесь не так много кода в исходниках, так что будет проще понять, если посмотреть исходники напрямую
Анализ исходного кода nextTick
Исходная версия:3.2.11, адрес источника:packages/runtime-core/src/sheduler.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 принимает в качестве параметра функцию и создает микрозадачу
На нашей странице звонитеnextTickКогда вы выполняете эту функцию, поместите наши параметрыfnназначить наp.then(fn), после завершения задачи очереди выполняется fn
Так как добавлено несколько способов обслуживания очереди, то порядок выполнения следующий:
queueJob -> queueFlush -> flushJobs -> nextTick参数的 fn
Неважно, что ты сейчас делаешь, ты узнаешь через несколько минут
Давайте по порядку, сначала посмотрите на функцию входаqueueJobГде он называется смотрите код
// packages/runtime-core/src/renderer.ts - 1555行
function baseCreateRenderer(){
const setupRenderEffect: SetupRenderEffectFn = (...) => {
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update), // 当作参数传入
instance.scope
)
}
}
существуетReactiveEffectПолученные здесь формальные параметры таковы:scheduler, был наконец использован здесь, и он знаком тем, кто видел отзывчивый исходный код, который является местом для распространения обновлений
// packages/reactivity/src/effect.ts - 330行
export function triggerEffects(
...
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
ПослеqueueJobЧто там происходит? мы приходим один за другим
queueJob()
Этот метод отвечает за ведение основной очереди задач, прием функции в качестве параметра, для постановки задачи в очередь параметр будетpushприбытьqueueВ очереди есть единственное решение. Очередь будет очищена после выполнения текущей задачи макроса.
const queue: SchedulerJob[] = []
export function queueJob(job: SchedulerJob) {
// 主任务队列为空 或者 有正在执行的任务且没有在主任务队列中 && job 不能和当前正在执行任务及后面待执行任务相同
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()
}
}
queueFlush()
Этот метод отвечает за попытку создания микрозадачи и ожидание выполнения очереди задач.
let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行
const resolvedPromise: Promise<any> = Promise.resolve() // 微任务创建器
let currentFlushPromise: Promise<void> | null = null // 当前任务
function queueFlush() {
// 当前没有微任务
if (!isFlushing && !isFlushPending) {
// 避免在事件循环周期内多次创建新的微任务
isFlushPending = true
// 创建微任务,把 flushJobs 推入任务队列等待执行
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
flushJobs()
Этот метод отвечает за обработку задач очереди, основная логика следующая:
- Сначала обработайте очередь задач-предшественников
- согласно с
Idочередь - Обход задач очереди выполнения
- Очистить и сбросить очередь после выполнения
- Выполнение задач после очереди
- Если есть, продолжайте рекурсивно
function flushJobs(seen?: CountMap) {
isFlushPending = false // 是否正在等待执行
isFlushing = true // 正在执行
if (__DEV__) seen = seen || new Map() // 开发环境下
flushPreFlushCbs(seen) // 执行前置任务队列
// 根据 id 排序队列,以确保
// 1. 从父到子,因为父级总是在子级前面先创建
// 2. 如果父组件更新期间卸载了组件,就可以跳过
queue.sort((a, b) => getId(a) - getId(b))
try {
// 遍历主任务队列,批量执行更新任务
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
continue
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0 // 队列任务执行完,重置队列索引
queue.length = 0 // 清空队列
flushPostFlushCbs(seen) // 执行后置队列任务
isFlushing = false // 重置队列执行状态
currentFlushPromise = null // 重置当前微任务为 Null
// 如果主任务队列、前置和后置任务队列还有没被清空,就继续递归执行
if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) {
flushJobs(seen)
}
}
}
flushPreFlushCbs()
Этот метод отвечает за выполнение предзадачной очереди, а инструкция написана в комментариях
export function flushPreFlushCbs( seen?: CountMap, parentJob: SchedulerJob | null = null) {
// 如果待处理的队列不为空
if (pendingPreFlushCbs.length) {
currentPreFlushParentJob = parentJob
// 保存队列中去重后的任务为当前活动的队列
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
// 清空队列
pendingPreFlushCbs.length = 0
// 开发环境下
if (__DEV__) { seen = seen || new Map() }
// 遍历执行队列里的任务
for ( preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex+ ) {
// 开发环境下
if ( __DEV__ && checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])) {
continue
}
activePreFlushCbs[preFlushIndex]()
}
// 清空当前活动的任务队列
activePreFlushCbs = null
preFlushIndex = 0
currentPreFlushParentJob = null
// 递归执行,直到清空前置任务队列,再往下执行异步更新队列任务
flushPreFlushCbs(seen, parentJob)
}
}
flushPostFlushCbs()
Этот метод отвечает за выполнение постзадачной очереди, а инструкция написана в комментариях
let activePostFlushCbs: SchedulerJob[] | null = null
export function flushPostFlushCbs(seen?: CountMap) {
// 如果待处理的队列不为空
if (pendingPostFlushCbs.length) {
// 保存队列中去重后的任务
const deduped = [...new Set(pendingPostFlushCbs)]
// 清空队列
pendingPostFlushCbs.length = 0
// 如果当前已经有活动的队列,就添加到执行队列的末尾,并返回
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
// 赋值为当前活动队列
activePostFlushCbs = deduped
// 开发环境下
if (__DEV__) seen = seen || new Map()
// 排队队列
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 遍历执行队列里的任务
for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) {
if ( __DEV__ && checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])) {
continue
}
activePostFlushCbs[postFlushIndex]()
}
// 清空当前活动的任务队列
activePostFlushCbs = null
postFlushIndex = 0
}
}
Здесь анализируется весь исходный код nextTick.
Прекрасное прошлое
- 12 советов по оптимизации производительности в Vue Development
- Анализ исходного кода принципа адаптивности Vue3.2 и разница с адаптивностью Vue2.x
- Подробное объяснение анализа исходного кода принципа адаптивности Vue2
- Объясните алгоритм виртуального DOM и Diff простыми словами, а также разницу между Vue2 и Vue3.
- 7 видов Vue3 и 12 видов компонентной связи Vue2, достойные коллекции
- Очки продвинутых знаний JavaScript
- Внешний мониторинг исключений и аварийное восстановление
- 20 минут, чтобы помочь вам победить HTTP и HTTPS и укрепить свою систему знаний HTTP
Эпилог
Если эта статья поможет вам немного, пожалуйста, поставьте лайк и поддержите ее, спасибо