vue nextTick

找了一個實習坟岔,去公司做數(shù)據(jù)的可視化豫缨,就是用iview-admin独令,Echarts做一下展示。中間遇到了一個問題數(shù)據(jù)第一次死活渲染不出來好芭,后來把那段代碼放到this.nextTick(()=>{})里面燃箭,就渲染出來了,很神奇舍败,再早之前也是一個數(shù)據(jù)顯示項目招狸,this.$refs死活選不到動態(tài)生成的dom,后來放到this.nextTick(()=>{})里面也行了邻薯。所以必須得查找一下nextTick的相關(guān)資料裙戏,在此記錄,供自己以后查閱厕诡。

官網(wǎng)解釋

先來看看官網(wǎng)如何解釋的

可能你還沒有注意到累榜,Vue 異步執(zhí)行 DOM 更新。只要觀察到數(shù)據(jù)變化灵嫌,Vue 將開啟一個隊列壹罚,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)改變。如果同一個 watcher 被多次觸發(fā)寿羞,只會被推入到隊列中一次猖凛。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作上非常重要。然后绪穆,在下一個的事件循環(huán)“tick”中形病,Vue 刷新隊列并執(zhí)行實際 (已去重的) 工作。Vue 在內(nèi)部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel霞幅,如果執(zhí)行環(huán)境不支持,會采用 setTimeout(fn, 0) 代替量瓜。

例如司恳,當你設(shè)置 vm.someData = 'new value' ,該組件不會立即重新渲染绍傲。當刷新隊列時扔傅,組件會在事件循環(huán)隊列清空時的下一個“tick”更新。多數(shù)情況我們不需要關(guān)心這個過程烫饼,但是如果你想在 DOM 狀態(tài)更新后做點什么猎塞,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發(fā)人員沿著“數(shù)據(jù)驅(qū)動”的方式思考杠纵,避免直接接觸 DOM荠耽,但是有時我們確實要這么做。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM 比藻,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback) 铝量。這樣回調(diào)函數(shù)在 DOM 更新完成后就會調(diào)用倘屹。

如果同一個 watcher 被多次觸發(fā),只會被推入到隊列中一次這句話什么意思呢慢叨?跑個例子就知道了

    <div id="example">
        {{ msg }}
    </div>
<script type="text/javascript">
var vm = new Vue({
    el: '#example',
    data: {
        msg: 1
    },
    created(){
        this.msg = 1
        this.msg = 2
        this.msg = 3
    },
    watch: {
        msg(){
            console.log(this.msg)
        }
    }
})
</script>

瀏覽器控制臺會輸出什么呢纽匙?答案是3,而不是1拍谐、2烛缔、3。這就是官網(wǎng)說的多次觸發(fā)轩拨,只會被推入隊列一次践瓷。
再看個例子

    <div id="example">
        {{ msg }}
    </div>
var vm = new Vue({
    el: '#example',
    data: {
        msg: '123'
    }
})
vm.msg = 'new message'
console.log(1)
console.log(vm.$el.innerText)
console.log(2)
Vue.nextTick(()=>{
    console.log(vm.$el.innerText)
})
console.log(3)
</script>

在谷歌瀏覽器控制臺中的輸出是

1
123
2
3
new message

是不是和想象中不太一樣,為什么最后打出的是'new message'而不是代碼的最后3气嫁,為什么第一次打印vm.$el.innerText是123当窗,而不是賦值后的'new message'這個就要了解一下JavaScript的EventLoop,這個稍后會說寸宵,再來看一個例子崖面,這個例子也是之前我為什么取不到div實例的簡化版

  <div id="example">
        <div v-for="i in number" :ref="'div'+i" v-if="number > 0">{{i}}</div>
        <button @click="addNumber">點擊</button>
    </div>
<script type="text/javascript">
var vm = new Vue({
    el: '#example',
    data: {
        number: 0
    },
    methods:{
        addNumber(){
            this.number = 3
            console.log(1)
            console.log(this.$refs['div1'])
            console.log(2)
            this.$nextTick(()=>{
                console.log(this.$refs['div1'])
            })
            console.log(3)
        }
    }
})
</script>

打印結(jié)果為

1
undefined
2
3
[div]

可以看到第一次并沒有取到id為div1的div元素,在nextTick里面就取到了梯影。這個就對應(yīng)官網(wǎng)里面的為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM 巫员,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)

Javascript EventLoop

上面那么多奇怪的行為其實和JavaScript的EventLoop有很大的關(guān)系,下面我就嘗試著解釋一下這個EventLoop甲棍。
有一個比較有名的視頻是解釋這個的简识,Philip Roberts的演講《Help, I'm stuck in an event-loop》,雖然是純英文感猛,但是配合PPT上面的動畫還是能看懂的七扰。我把阮一峰老師的解釋直接復(fù)制粘貼過來吧,因為寫的確實很好陪白。

為什么JavaScript是單線程颈走?

JavaScript語言的一大特點就是單線程,也就是說咱士,同一個時間只能做一件事立由。那么,為什么JavaScript不能有多個線程呢序厉?這樣能提高效率啊锐膜。

JavaScript的單線程,與它的用途有關(guān)弛房。作為瀏覽器腳本語言道盏,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程捞奕,否則會帶來很復(fù)雜的同步問題牺堰。比如,假定JavaScript同時有兩個線程颅围,一個線程在某個DOM節(jié)點上添加內(nèi)容伟葫,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準院促?

所以筏养,為了避免復(fù)雜性,從一誕生常拓,JavaScript就是單線程渐溶,這已經(jīng)成了這門語言的核心特征,將來也不會改變弄抬。

為了利用多核CPU的計算能力茎辐,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程掂恕,但是子線程完全受主線程控制拖陆,且不得操作DOM。所以懊亡,這個新標準并沒有改變JavaScript單線程的本質(zhì)依啰。

任務(wù)隊列

單線程就意味著,所有任務(wù)需要排隊店枣,前一個任務(wù)結(jié)束速警,才會執(zhí)行后一個任務(wù)。如果前一個任務(wù)耗時很長鸯两,后一個任務(wù)就不得不一直等著闷旧。

如果排隊是因為計算量大,CPU忙不過來钧唐,倒也算了忙灼,但是很多時候CPU是閑著的,因為IO設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù))逾柿,不得不等著結(jié)果出來,再往下執(zhí)行宅此。

JavaScript語言的設(shè)計者意識到机错,這時主線程完全可以不管IO設(shè)備,掛起處于等待中的任務(wù)父腕,先運行排在后面的任務(wù)弱匪。等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去萧诫。

于是斥难,所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous)帘饶,另一種是異步任務(wù)(asynchronous)哑诊。同步任務(wù)指的是,在主線程上排隊執(zhí)行的任務(wù)及刻,只有前一個任務(wù)執(zhí)行完畢镀裤,才能執(zhí)行后一個任務(wù);異步任務(wù)指的是缴饭,不進入主線程暑劝、而進入"任務(wù)隊列"(task queue)的任務(wù),只有"任務(wù)隊列"通知主線程颗搂,某個異步任務(wù)可以執(zhí)行了担猛,該任務(wù)才會進入主線程執(zhí)行。
具體來說丢氢,異步執(zhí)行的運行機制如下傅联。(同步執(zhí)行也是如此,因為它可以被視為沒有異步任務(wù)的異步執(zhí)行卖丸。)

(1)所有同步任務(wù)都在主線程上執(zhí)行纺且,形成一個執(zhí)行棧(execution context stack)。
(2)主線程之外稍浆,還存在一個"任務(wù)隊列"(task queue)载碌。只要異步任務(wù)有了運行結(jié)果,就在"任務(wù)隊列"之中放置一個事件衅枫。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢嫁艇,系統(tǒng)就會讀取"任務(wù)隊列",看看里面有哪些事件弦撩。那些對應(yīng)的異步任務(wù)步咪,于是結(jié)束等待狀態(tài),進入執(zhí)行棧益楼,開始執(zhí)行猾漫。
(4)主線程不斷重復(fù)上面的第三步。

主線程從"任務(wù)隊列"中讀取事件感凤,這個過程是循環(huán)不斷的悯周,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))。

EcentLoop

主線程在運行的時候會產(chǎn)生堆棧陪竿,堆就是存儲變量禽翼,棧記錄執(zhí)行的順序,如果碰到回調(diào)函數(shù)、DOM操作比如點擊闰挡、鼠標移上去等锐墙、setTimeout操作會放到任務(wù)隊列,只有棧中的代碼執(zhí)行完畢才會從任務(wù)隊列取出代碼长酗,進行執(zhí)行溪北。所以這也是為什么上面代碼例子中雖然console.log(3)在代碼最后,但是比nextTick里面的代碼先輸出花枫。你可以這么理解刻盐,一堆代碼,該放到stack里面的方法劳翰,放到stack里面敦锌,然后這堆代碼里面的異步操作放到任務(wù)隊列里面,然后執(zhí)行棧里面的代碼佳簸,棧里面的代碼執(zhí)行完畢乙墙,執(zhí)行任務(wù)隊列里面的代碼,所以代碼的執(zhí)行順序和寫的順序并不是一直的

任務(wù)隊列

上面的任務(wù)隊列分為兩種生均,執(zhí)行順序也是有一點差別的听想,Macrotasks 和 Microtasks

  • Macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
  • Microtasks: process.nextTick, Promises, Object.observe(廢棄), MutationObserver

Macrotasks 和 Microtasks有什么區(qū)別呢?我們以setTimeout和Promises來舉例马胧。

   console.log('1');
    setTimeout(function() {
      console.log('2');
    }, 0);
    Promise.resolve().then(function() {
      console.log('3');
    }).then(function() {
      console.log('4');
    });
    console.log('5');
    //輸出結(jié)果:
    //1
    //5
    //3
    //4
    //2

原因是Promise中的then方法的函數(shù)會被推入 microtasks 隊列汉买,而setTimeout的任務(wù)會被推入 macrotasks 隊列。在每一次事件循環(huán)中佩脊,macrotask 只會提取一個執(zhí)行蛙粘,而 microtask 會一直提取,直到 microtasks 隊列清空威彰。結(jié)論如下:

  1. microtask會優(yōu)先macrotask執(zhí)行
  2. microtasks會被循環(huán)提取到執(zhí)行引擎主線程的執(zhí)行棧出牧,直到microtasks任務(wù)隊列清空,才會執(zhí)行macrotask

【注:一般情況下歇盼,macrotask queues 我們會直接稱為 task queues舔痕,只有 microtask queues 才會特別指明”海】

解釋vue nextTick

查看一下vue nextTick的代碼實現(xiàn)

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

從上面這一段代碼知道伯复,vue nextTick默認使用microTask,然后生成兩個函數(shù)邢笙,首先是macroTimerFunc啸如,順序是setImmediate->MessageChannnel->setTimeout,microTimerFunc生成順序是 Promise

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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }

nextTick函數(shù)其實做了兩件事情鸣剪,一是生成兩個timerFunc组底,把回調(diào)作為microTask或macroTask參與到事件循環(huán)中來。二是把回調(diào)函數(shù)放入一個callbacks隊列筐骇,等待適當?shù)臅r機執(zhí)行债鸡。(這個時機和timerFunc不同的實現(xiàn)有關(guān)),說白了就是改變代碼的執(zhí)行順序铛纬,在dom節(jié)點更新完畢再去執(zhí)行厌均,因為有些操作需要dom節(jié)點更新完畢才行,所以不能立刻執(zhí)行告唆,需要放到nextTick里面去執(zhí)行棺弊,下面圖片可以參考一下

原始代碼
A();
B();
C();
執(zhí)行順序


正常順序

原始代碼(這個nextTick可不是vue里面的nextTick,而是原生的函數(shù)擒悬,不過可以借鑒一下模她,有助于理解vue里面的nextTick執(zhí)行順序)
A();
process.nextTick(B);
C();


nextTick

原始代碼
A();
setImmediate(B);//或者setTimeout(B,0);
C();


setImmediate|setTimeout

應(yīng)用場景

在操作DOM節(jié)點無效的時候,就要考慮操作的實際DOM節(jié)點是否存在懂牧,或者相應(yīng)的DOM是否被更新完畢侈净。

比如說,在created鉤子中涉及DOM節(jié)點的操作肯定是無效的僧凤,因為此時還沒有完成相關(guān)DOM的掛載畜侦。解決的方法就是在nextTick函數(shù)中去處理DOM,這樣才能保證DOM被成功掛載而有效操作躯保。

還有就是在數(shù)據(jù)變化之后要執(zhí)行某個操作旋膳,而這個操作需要使用隨數(shù)據(jù)改變而改變的DOM時,這個操作應(yīng)該放進Vue.nextTick途事。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末验懊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子盯孙,更是在濱河造成了極大的恐慌鲁森,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件振惰,死亡現(xiàn)場離奇詭異歌溉,居然都是意外死亡,警方通過查閱死者的電腦和手機骑晶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門痛垛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桶蛔,你說我怎么就攤上這事匙头。” “怎么了仔雷?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵蹂析,是天一觀的道長舔示。 經(jīng)常有香客問我,道長电抚,這世上最難降的妖魔是什么惕稻? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮蝙叛,結(jié)果婚禮上俺祠,老公的妹妹穿的比我還像新娘。我一直安慰自己借帘,他們只是感情好蜘渣,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肺然,像睡著了一般蔫缸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上际起,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天捂龄,我揣著相機與錄音,去河邊找鬼加叁。 笑死倦沧,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的它匕。 我是一名探鬼主播展融,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼豫柬!你這毒婦竟也來了告希?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤烧给,失蹤者是張志新(化名)和其女友劉穎燕偶,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體础嫡,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡指么,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榴鼎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伯诬。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖巫财,靈堂內(nèi)的尸體忽然破棺而出盗似,到底是詐尸還是另有隱情,我是刑警寧澤平项,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布赫舒,位于F島的核電站悍及,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏接癌。R本人自食惡果不足惜并鸵,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扔涧。 院中可真熱鬧,春花似錦届谈、人聲如沸枯夜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湖雹。三九已至,卻和暖如春曙搬,著一層夾襖步出監(jiān)牢的瞬間摔吏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工纵装, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留征讲,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓橡娄,卻偏偏與公主長得像诗箍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子挽唉,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353