[TOC]
# 為什么用Vue.nextTick()
首先來了解一下JS的運行機制。
<br>
## JS運行機制(Event Loop)
JS執行是單線程的,它是基于事件循環的。
1. 所有同步任務都在主線程上執行,形成一個執行棧。
2. 主線程之外,會存在一個任務隊列,只要異步任務有了結果,就在任務隊列中放置一個事件。
3. 當執行棧中的所有同步任務執行完后,就會讀取任務隊列。那些對應的異步任務,會結束等待狀態,進入執行棧。
4. 主線程不斷重復第三步。
這里主線程的執行過程就是一個`tick`,而所有的異步結果都是通過任務隊列來調度。`Event Loop`分為宏任務和微任務,無論是執行宏任務還是微任務,完成后都會進入到一下`tick`,**并在兩個`tick`之間進行UI渲染**。
由于Vue DOM更新是異步執行的,即修改數據時,視圖不會立即更新,而是會監聽數據變化,并緩存在同一事件循環中,等同一數據循環中的所有數據變化完成之后,再統一進行視圖更新。為了確保得到更新后的DOM,所以設置了`Vue.nextTick()`方法。
<br>
<br>
# 什么是Vue.nextTick()
是Vue的核心方法之一,官方文檔解釋如下:
> 在下次DOM更新循環結束之后執行延遲回調。在修改數據之后立即使用這個方法,獲取更新后的DOM。
<br>
## MutationObserver
先簡單介紹下`MutationObserver`:MO是HTML5中的API,是一個用于監視DOM變動的接口,它可以監聽一個DOM對象上發生的子節點刪除、屬性修改、文本內容修改等。
調用過程是要先給它綁定回調,得到MO實例,這個回調會在MO實例監聽到變動時觸發。這里MO的回調是放在`microtask`中執行的。
~~~
// 創建MO實例
const observer = new MutationObserver(callback)
const textNode = '想要監聽的Don節點'
observer.observe(textNode, {
characterData: true // 說明監聽文本內容的修改
})
~~~
<br>
## 源碼淺析
`nextTick`的實現單獨有一個JS文件來維護它,在`src/core/util/next-tick.js`中。
`nextTick`源碼主要分為兩塊:能力檢測和根據能力檢測以不同方式執行回調隊列。
<br>
## 能力檢測
由于宏任務耗費的時間是大于微任務的,所以在瀏覽器支持的情況下,優先使用微任務。如果瀏覽器不支持微任務,再使用宏任務。
~~~
// 空函數,可用作函數占位符
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
// 用來存儲所有需要執行的回調函數
const callbacks = []
// 用來標志是否正在執行回調函數
let pending = false
// 對callbacks進行遍歷,然后執行相應的回調函數
function flushCallbacks () {
pending = false
// 這里拷貝的原因是:
// 有的cb 執行過程中又會往callbacks中加入內容
// 比如 $nextTick的回調函數里還有$nextTick
// 后者的應該放到下一輪的nextTick 中執行
// 所以拷貝一份當前的,遍歷執行完當前的即可,避免無休止的執行下去
const copies = callbcks.slice(0)
callbacks.length = 0
for(let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 異步執行函數 用于異步延遲調用 flushCallbacks 函數
// 在2.5中,我們使用(宏)任務(與微任務結合使用)。
// 然而,當狀態在重新繪制之前發生變化時,就會出現一些微妙的問題
// (例如#6813,out-in轉換)。
// 同樣,在事件處理程序中使用(宏)任務會導致一些奇怪的行為
// 因此,我們現在再次在任何地方使用微任務。
// 優先使用 Promise
if(typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// IOS 的UIWebView, Promise.then 回調被推入 microTask 隊列,但是隊列可能不會如期執行
// 因此,添加一個空計時器強制執行 microTask
if(isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if(!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString === '[object MutationObserverConstructor]')) {
// 當 原生Promise 不可用時,使用 原生MutationObserver
// e.g. PhantomJS, iOS7, Android 4.4
let counter = 1
// 創建MO實例,監聽到DOM變動后會執行回調flushCallbacks
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true // 設置true 表示觀察目標的改變
})
// 每次執行timerFunc 都會讓文本節點的內容在 0/1之間切換
// 切換之后將新值復制到 MO 觀測的文本節點上
// 節點內容變化會觸發回調
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter) // 觸發回調
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
~~~
延遲調用優先級如下:
Promise > MutationObserver > setImmediate > setTimeout
~~~
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`時會執行:
* 把傳入的回調函數`cb`壓入`callbacks`數組
* 執行`timerFunc`函數,延遲調用`flushCallbacks`函數
* 遍歷執行`callbacks`數組中的所有函數
這里的`callbacks`沒有直接在`nextTick`中執行回調函數的原因是保證在同一個`tick`內多次執行`nextTick`,不會開啟多個異步任務,而是把這些異步任務都壓成一個同步任務,在下一個`tick`執行完畢。
<br>
## 附加
`noop`的定義如下
~~~
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
export function noop (a?: any, b?: any, c?: any) {}
~~~
<br>
<br>
# 怎么用
**語法**:`Vue.nextTick([callback, context])`
**參數**:
* `{Function} [callback]`:回調函數,不傳時提供promise調用
* `{Object} [context]`:回調函數執行的上下文環境,不傳默認是自動綁定到調用它的實例上。
~~~
//改變數據
vm.message = 'changed'
//想要立即使用更新后的DOM。這樣不行,因為設置message后DOM還沒有更新
console.log(vm.$el.textContent) // 并不會得到'changed'
//這樣可以,nextTick里面的代碼會在DOM更新后執行
Vue.nextTick(function(){
// DOM 更新了
//可以得到'changed'
console.log(vm.$el.textContent)
})
// 作為一個 Promise 使用 即不傳回調
Vue.nextTick()
.then(function () {
// DOM 更新了
})
~~~
Vue實例方法`vm.$nextTick`做了進一步封裝,把context參數設置成當前Vue實例。
<br>
<br>
# 小結
使用`Vue.nextTick()`是為了可以獲取更新后的DOM 。
觸發時機:在同一事件循環中的數據變化后,DOM完成更新,立即執行`Vue.nextTick()`的回調。
> 同一事件循環中的代碼執行完畢 -> DOM 更新 -> nextTick callback觸發
](images/screenshot_1627751757798.png)
應用場景:
* 在Vue生命周期的`created()`鉤子函數進行的DOM操作一定要放在`Vue.nextTick()`的回調函數中。
**原因**:是`created()`鉤子函數執行時DOM其實并未進行渲染。
* 在數據變化后要執行的某個操作,而這個操作需要使用隨數據改變而改變的DOM結構的時候,這個操作應該放在`Vue.nextTick()`的回調函數中。
**原因**:Vue異步執行DOM更新,只要觀察到數據變化,Vue將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據改變,如果同一個watcher被多次觸發,只會被推入到隊列中一次。
<br>
## 版本分析
2.6 版本優先使用 microtask 作為異步延遲包裝器,且寫法相對簡單。而2.5 版本中,nextTick 的實現是 microTimerFunc、macroTimerFunc 組合實現的,延遲調用優先級是:Promise > setImmediate > MessageChannel > setTimeout,具體見源碼。
2.5 版本在重繪之前狀態改變時會有小問題(如[#6813](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue%2Fissues%2F6813))。此外,在事件處理程序中使用 macrotask 會導致一些無法規避的奇怪行為(如[#7109](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue%2Fissues%2F7109),[#7153](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue%2Fissues%2F7153)等)。
microtask 在某些情況下也是會有問題的,因為 microtask 優先級比較高,事件會在順序事件(如[#4521](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue%2Fissues%2F4521),[#6690](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue%2Fissues%2F6690)有變通方法)之間甚至在同一事件的冒泡過程中觸發([#6566](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvuejs%2Fvue%2Fissues%2F6566))。
<br>
<br>
# 參考資料
[淺析Vue.nextTick()原理](https://segmentfault.com/a/1190000020499713)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼