Vue nextTick の原則
使用シナリオ:
データを取得した後、新しいビューで次の手順または他の操作を実行する必要があるときに、DOM を取得できないことがわかります。
これには、Vue の非常に重要な概念である非同期更新キュー (JS 実行メカニズム、イベント ループ) が関係します。
非同期更新キュー
気づいていないかもしれませんが、Vue は DOM を更新するときに非同期で実行されます。データ変更が検出される限り、DOM は直接更新されませんが、キューが開かれ、同じイベント ループ内で発生するすべてのデータ変更がバッファリングされます。
同じウォッチャーが複数回トリガーされた場合、キューにプッシュされるのは 1 回だけです。バッファリング中のこの重複排除は、不必要な計算や DOM 操作を回避するために非常に重要です。
次に、次のイベント ループ「ティック」で、Vue はキューをフラッシュし、実際の (重複排除された) 作業を実行します。
したがって、for ループを使用してデータを 100 回動的に変更すると、最後の変更のみが適用されますが、そのようなメカニズムがない場合、DOM は 100 回再描画されることになり、多大なオーバーヘッドとパフォーマンスの損失が発生します。
Vue は内部的にネイティブのPromise.then、MutationObserver、setImmediate を非同期キューに使用しようとします。実行環境がサポートしていない場合は、代わりに setTimeout(fn, 0) を使用します。
のような
// 修改数据
vm.msg = 'Hello'
// 该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,
console.log(vm.$el.textContent) //并不会得到‘hello’
// 这样才可以 nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示) 即不传回调
Vue.nextTick()
.then(function () {
// DOM 更新了
})
// Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例。
Vue.nextTick の公式定義
Vue.nextTick( [コールバック, コンテキスト] )
パラメータ:
- {Function} [callback] //コールバック関数、渡されなかった場合にプロミス呼び出しを提供します
- {Object} [context] // コールバック関数が実行されるコンテキスト。渡されない場合、デフォルトでコールバック関数を呼び出すインスタンスに自動的にバインドされます。
使用法:
次の DOM 更新サイクルの後に遅延コールバックを実行します。データを変更した直後にこのメソッドを使用して、更新された DOM を取得します。
マクロタスク/マイクロタスク
まず、ブラウザーの EventLoop、マクロ タスク、マイクロ タスクの概念を理解する必要があります。
JS の実行はシングルスレッドであり、イベント ループに基づいています。
詳しくはJavaScriptの動作仕組みを詳しく解説:もう一度イベントループの話をしよう
以下は、メイン スレッドにおける後者の 2 つの実行関係を示す図です。
メインスレッドが同期タスクを完了すると、次のようになります。
- エンジンはまずマクロタスク キューから最初のタスクを取り出し、実行後、マイクロタスク キュー内のすべてのタスクを取り出し、それらをすべて順番に実行して、UIrender を実行します。
- 次に、マクロタスクキューから 1 つ取り出し、実行が完了したら、再度マイクロタスクキューにあるものをすべて取り出し、順番に実行します。
- このサイクルは、両方のキュー内のすべてのタスクが取得されるまで繰り返されます。
ブラウザ環境における一般的な非同期タスクの優先順位に応じたタイプ:
- マクロタスク: 同期コード、setImmediate、MessageChannel、setTimeout/setInterval
- マイクロタスク:Promise.then、MutationObserver
これを理解すると、次のことがわかります。
非同期タスクの実行順序は、確立と優先順位に基づいています。Vue の非同期キューは、デフォルトでマイクロ タスクを最初に使用します。これは、その高優先度機能を使用して、キュー内のすべてのマイクロ タスクが 1 サイクルで実行されることを保証します。
nextTick ソース コード内のマイクロ タスクとマクロ タスクの具体的な実装を見てみましょう。
まず、変数を定義します。
// 空函数,可用作函数占位符
import { noop } from 'shared/util'
// 错误处理函数
import { handleError } from './error'
// 是否是IE、IOS、内置函数
import { isIE, isIOS, isNative } from './env'
// 使用 MicroTask 的标识符,这里是因为火狐在<=53时 无法触发微任务,在modules/events.js文件中引用进行安全排除
export let isUsingMicroTask = false
var callbacks = []; // 存放异步执行的回调
var pending = false; // 用来标志是否正在执行回调函数
var timerFunc; // 异步执行函数 用于异步延迟调用 flushCallbacks 函数
次に、$nextTick内で実際に呼び出される関数を作成します。
// 对callbacks进行遍历,然后执行相应的回调函数
function flushCallbacks () {
pending = false;
// 拷贝出函数数组副本
// 这里拷贝的原因是:
// 有的cb 执行过程中又会往callbacks中加入内容
// 比如 $nextTick的回调函数里还有$nextTick
// 后者的应该放到下一轮的nextTick 中执行
// 所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
var copies = callbacks.slice(0);
// 把函数数组清空
callbacks.length = 0;
// 依次执行函数
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
次に、Vue は現在のブラウザ環境に応じてネイティブの Promise.then と MutationObserver の使用を優先します。どちらもサポートしていない場合は、代わりに setTimeout を使用します。その目的は、DOM が更新されるまで関数を遅らせてから使用することです。
マクロタスクはマイクロタスクよりも時間がかかるため、ブラウザがマイクロタスクをサポートしている場合は、マイクロタスクが最初に使用されます。ブラウザがマイクロタスクをサポートしていない場合は、マクロタスクを使用してください。
- Promise.then の呼び出しが遅れました
// 在2.5中,我们使用(宏)任务(与微任务结合使用)。
// 然而,当状态在重新绘制之前发生变化时,就会出现一些微妙的问题
// (例如#6813,out-in转换)。
// 同样,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
// 因此,我们现在再次在任何地方使用微任务。
// 优先使用 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function () {
p.then(flushCallbacks ).catch(logError);
// 用Promise模拟的,但是在iOS UIWebViews中有个bug,Promise.then并不会被触发
// 除非浏览器中有其他事件触发,例如处理setTimeout。所以手动加了个空的setTimeout
if (isIOS) { setTimeout(noop); }
};
}
ブラウザーが Promise をサポートしている場合は、Promise.then メソッドを使用して関数呼び出しを遅延させます。Promise.then メソッドは、関数を現在の関数呼び出しスタックの最後まで遅延させることができます。つまり、関数呼び出しスタックは関数を最初の時点で呼び出します。終わり。それにより遅延が発生します。
- 突然変異観察者
// 当 原生Promise 不可用时,使用 原生MutationObserver
else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
// 创建MO实例,监听到DOM变动后会执行回调flushCallbacks
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true // 设置true 表示观察目标的改变 -> 节点内容或节点文本的变动
});
// 每次执行timerFunc 都会让文本节点的内容在 0/1之间切换
// 切换之后将新值复制到 MO 观测的文本节点上
// 节点内容变化会触发回调->flushCallbacks会被调用
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
}
MutationObserver は h5 で追加された新しい関数で、dom ノードの変更を監視し、すべての dom 変更が完了した後にコールバック関数を実行する機能です。
ミューテーションオブザーバーAPI
方法:
コンストラクターは
、Mutation オブザーバー オブジェクトをインスタンス化するために使用されます。そのパラメーターはコールバック関数です。この関数は、指定された DOM ノードが変更を送信した後に実行される関数で、2 つのパラメーターで渡されます。1 つは次の配列です。変更レコード (MutationRecord)、もう 1 つはオブザーバー オブジェクト自体です
let observer = new MutationObserver(function(records, itself){}); //实例化一个Mutation观察者对象
オブザーバー オブジェクトで監視し、監視する必要がある DOM ノードと対応するパラメーターを登録します。
observer.observe(Node target, optional MutationObserverInit options)
オプションのパラメータ MutationObserverInit のプロパティは次のとおりです。
-
childList:ターゲット ノードの子ノードの追加と削除を観察します。
-
属性:ターゲット ノードの属性の変更を観察します。
-
CharacterData: ノード コンテンツまたはノード テキストの変更
-
subtree: ブール値、すべての下位ノード (子ノードおよび子ノードの子ノードを含む) の変更
-
attributeOldValue Boolean値、attributes属性がtrueに設定されていることを前提に、変更された属性ノードより前の属性値を記録します(下記MutationRecordオブジェクトのoldValue属性に記録されます)
-
CharacterDataOldValue CharacterDataがBoolean値で属性がtrueに設定されていることを前提に、変更後のcharacterDataノード以前のテキスト内容を記録します(以下のMutationRecordオブジェクトのoldValue属性に記録します)。
-
attributeFilter 配列。監視する必要がある特定の属性 (['class', 'src'] など) を示します。
上記のコードは、変更をトリガーするためにテキスト ノードのコンテンツを変更するテキスト ノードを作成していることがわかります。データ モデルを更新した後、dom ノードが再レンダリングされるため、そのような変更リスナーを追加しました。 , テキスト ノードの変更を使用してリスナーをトリガーします。すべての DOM がレンダリングされた後、関数を実行して遅延効果を実現します。
3.setImmediate実装
このメソッドは、IE および Edge ブラウザーでのみネイティブに実装されています。
なぜ setTimeout を直接ではなく setImmediate を使用するのですか? HTML5 では、setTimeout 実行の最小遅延が 4 ミリ秒であり、ネストされたタイムアウトのパフォーマンスが 10 ミリ秒であると規定されているためです。コールバックを実行するには、最小遅延制限のない setImmediate の方が setTimeout よりも明らかに優れています。
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//回退到setImmediate。
//从技术上讲,它利用(宏)任务队列,
//但它仍然是比setTimeout更好的选择。
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
4.setTimeOutディレイラー
else {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
setTimeout の遅延原理を使用して、setTimeout(func, 0) は func 関数を次の関数呼び出しスタックの先頭まで遅延させます。つまり、現在の関数が実行された後に関数を実行し、遅延関数を完了します。
遅延呼び出しの優先順位は次のとおりです:
Promise > MutationObserver > setImmediate > setTimeout
マイクロ タスクがデフォルトで最初に使用されるのは、その高優先度機能を使用して、キュー内のすべてのマイクロ タスクが 1 サイクルで実行されるようにするためです。
クロージャー関数
// src/core/util/next-tick.js
export function nextTick(cb? Function, ctx: Object) {
let _resolve
// cb 回调函数会统一处理压入callbacks数组
callbacks.push(() => {
if(cb) {
try {
cb.call(ctx)
} catch(e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// pending 为false 说明本轮事件循环中没有执行过timerFunc()
if(!pending) {
pending = true
timerFunc()
}
// 当不传入 cb 参数时,提供一个promise化的调用
// 如nextTick().then(() => {})
// 当_resolve执行时,就会跳转到then逻辑中
if(!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
next-tick.js は nextTick パラメータを外部に公開するため、Vue.nextTick が呼び出されるたびに実行されます。
- まず第一に、nextTick は受信した cb コールバック関数を try-catch でラップし、それを匿名関数に入れてコールバック配列にプッシュします。これは、単一の cb の実行エラーによって JS スレッド全体がハングするのを防ぐために行われます。ラップすると、実行エラーが発生した場合にこれらのコールバック関数が相互に影響を与えるのを防ぎます。たとえば、前の関数がエラーをスローしても、次の関数は引き続き実行できます。
- 次に、保留ステータス (フラグ ビット) を確認します。これは、最初は false で、timerFunc メソッドに入る前に true に設定されるため、次回 nextTick が呼び出されるとき、timerFunc メソッドには入りません。次のマクロ/マイクロティックで非同期になる コールバックキューに収集されたタスクを実行するため、flushCallbacks メソッドは実行開始時に pending を false に設定するため、次回 nextTick が呼び出されたときに timerFunc の新しいラウンドを開始できます。したがって、vue でイベント ループが形成されます。
- 最後に、cb が渡されているかどうかを確認します。nextTick は Promise ベースの呼び出しもサポートしています: nextTick().then(() => {})。そのため、cb が渡されていない場合は、Promise インスタンスが直接返され、resolve は_resolve に渡されるため、後者が実行されると、呼び出し時に then に渡されたメソッドにジャンプします。
$nextTick
最後に、Vue プロトタイプに nexttick 関数をハングすればOKです。
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
ソースコード
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = [] // 声明公共数组,存储nextTick回调函数
let pending = false
function flushCallbacks () { // 执行timerFunc函数时执行这个回调函数,处理在执行nextTick时新增的方法
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 这里,我们使用微任务异步延迟包装器。
// 在2.5中,我们使用了(宏)任务(结合了微任务)。
// 但是,当状态在重新绘制之前更改状态时(例如,#6813,由外而内的过渡),它存在一些细微的问题。
// 另外,在事件处理程序中使用(宏)任务会导致一些无法规避的怪异行为(例如,#7109,#7153,#7546,#7834,#8109)。
// 因此,我们现在再次在各处使用微任务。
// 这种权衡的主要缺点是,在某些情况下,微任务的优先级过高,
// 并且在假定的顺序事件之间(例如#4521,#6690,它们具有解决方法)甚至在同一事件冒泡之间也会触发(#6566) 。
let timerFunc // 定义全局的timerFunc
// nextTick行为利用了微任务队列,可以通过本机Promise.then或MutationObserver对其进行访问。
// MutationObserver具有更广泛的支持,但是当在触摸事件处理程序中触发时,
// 它在iOS> = 9.3.3的UIWebView中严重错误。 触发几次后,它将完全停止工作...因此,如果本地Promise可用,
// 我们将使用它:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// 但是它可能会陷入怪异的状态,在这种状态下,回调被推送到微任务队列中,
// 但是队列没有被刷新,直到浏览器需要执行其他工作 ,
// 例如 处理一个计时器。 因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 在本地Promise不可用的地方使用MutationObserver,
// 例如 PhantomJS,iOS7,Android 4.4(#6466 MutationObserver在IE11中不可靠)
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 ,执行这个函数会
// 触发执行 flushCallbacks 这个函数,他会遍历执行全部的callbacks
// 为什么会有那么多的callback呢,因为nextTick每次被执行都会在callbacks中
// 推送一个事件,形成一个事件组就是 callbacks
// 这里的pending 是一个全局的变量,默认值false,在flushCallBacks里会把
// pending = false;此处是一个锁保证nextTick仅有一次执行。
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()
}
// 如果没有回调函数,vue会让nextTick返回一个promise对象返回结果
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}