JS Event Loop(VUE nextTick)

前言

js是一個單線程的語言(非阻塞),最初的目的是為了和瀏覽器交互,也就是事件的輸入輸出流,計算機根據(jù)人類的指令做出不同的反應(yīng)結(jié)果,但是在JS執(zhí)行的過程環(huán)境中 我們有 幾個特殊的 “單詞” setTimeoutsetInterval、 Promise、另外在 Node中還有 process.nextTick。那么他們的執(zhí)行順序到底是怎么樣的呢,瀏覽器不應(yīng)該是按照他們書寫的順序從上往下執(zhí)行嗎?

那你又有疑問了,既然是單線程的,在某個特定的時刻只有特定的代碼能夠被執(zhí)行,并阻塞其它的代碼。

那不行啊,我們總不能一直等著啊,前端需要調(diào)用后端接口取數(shù)據(jù),這個過程是需要響應(yīng)時間的,那執(zhí)行這個代碼的時候瀏覽器也等著?答案是否定的。

其實還有其他很多類線程(應(yīng)該叫做任務(wù)隊列),比如進行ajax請求、監(jiān)控用戶事件、定時器、讀寫文件的線程(例如在NodeJS中)等等。

這些我們稱之為異步事件,當(dāng)異步事件發(fā)生時,將他們放入執(zhí)行隊列,等待當(dāng)前代碼執(zhí)行完成。就不會長時間阻塞主線程。

等主線程的代碼執(zhí)行完畢,然后再讀取任務(wù)隊列,返回主線程繼續(xù)處理。如此循環(huán)這就是事件循環(huán)機制。

JS 在執(zhí)行的過程中會產(chǎn)生執(zhí)行環(huán)境,這些執(zhí)行環(huán)境會被順序的加入到執(zhí)行棧中。如果遇到異步的代碼,會被掛起并加入到 Task(有多種 task) 隊列中。一旦執(zhí)行棧為空,Event Loop 就會從 Task 隊列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行,所以本質(zhì)上來說 JS 中的異步還是同步行為

舉個栗子

console.log('0');
setTimeout(() => {
  console.log('1');
}, 0);
console.log('2');
//輸出 0 , 2 ,1

看起來是setTimeout 設(shè)置了時間為0 但是 setTimeout 是一個“異步”的操作,其實真是的情況是 setTimeout 的0 參數(shù)是無效的, JS會給他默認(rèn)一個值為4毫秒。所以結(jié)果是 0 2 1 。


image.png

我們剛剛說到了“異步” 那么JS是怎么異步的呢 ,其實在JS執(zhí)行的時候 不同的任務(wù)會分配到不同的隊列中,每個任務(wù)在制定的時候 已經(jīng)規(guī)定了他的基礎(chǔ)要素 也就是他屬于哪個隊列的 ,任務(wù)源可以分為2類 微任務(wù) microtask宏任務(wù) macrotask,微任務(wù)又稱之為JOBS,宏任務(wù)稱為TASK。
我們在來看看下面這個例子

setTimeout(function() {
    console.log(1)
}, 0);
new Promise((resolve)=>{
    console.log(2);
    for(var i = 0; i < 10000; i++) {
        i == 9999 && resolve();
    }
    console.log(3);
}).then(function() {
    console.log(4);
});
console.log(5)
// 2 3 5 4  1

為什么是這個結(jié)果呢 。這就是 Jobs 和 Task的區(qū)別 我們下面仔細(xì)梳理下

微任務(wù)(Jobs)包括

process.nextTick

Promise

Object.observe(已廢棄)

MutationObserver (html5 新特性)

宏任務(wù)(Task)包括

setTimeout/setInterval

setImmediate

I/O操作

UI rendering

瀏覽器中新標(biāo)準(zhǔn)中的事件循環(huán)機制與 node.js 類似,其中會介紹到幾個nodejs有但是瀏覽器中沒有的 API,大家只需要了解就好。

比如process.nextTicksetImmediate我們稱他們?yōu)槭录矗?事件源作為任務(wù)分發(fā)器,他們的回調(diào)函數(shù)才是被分發(fā)到任務(wù)隊列,而本身會立即執(zhí)行。

例如,setTimeout第一個參數(shù)被分發(fā)到任務(wù)隊列,Promise 的 then 方法的回調(diào)函數(shù)被分發(fā)到任務(wù)隊列(catch方法同理)。
不同源的事件被分發(fā)到不同的任務(wù)隊列,其中 setTimeoutsetInterval 屬于同源

整體代碼開始第一次循環(huán)。全局上下文進入函數(shù)調(diào)用棧。直到調(diào)用棧清空(只剩全局),然后執(zhí)行所有的job。

當(dāng)所有可執(zhí)行的 job 執(zhí)行完畢之后。循環(huán)再次從task開始,找到其中一個任務(wù)隊列執(zhí)行完畢,然后再執(zhí)行所有的 job,這樣一直循環(huán)下去。

無論是 task 還是 job,都是通過函數(shù)調(diào)用棧來完成。

這個時候我們是不是有一個大發(fā)現(xiàn),除了首次整體代碼的執(zhí)行,其他的都有規(guī)律,先執(zhí)行task任務(wù)隊列,再執(zhí)行所有的 job 并清空 job 隊列。

再執(zhí)行 task—job—task—job……,往復(fù)循環(huán)直到?jīng)]有可執(zhí)行代碼。

那我們可不可以這么理解,第一次 script 代碼的執(zhí)行也算是一個task任務(wù)呢,如果這么理解那整個事件循環(huán)就很容易理解了。

UI rendering是在Task執(zhí)行之后就運行的 那么我們只要把DOM操作放入Job中就可以提高渲染的性能了

下面我們說說 vue的 nextTick
還是舉個栗子

        <div id="app">
            <ul ref="list">
                <li  v-for="li in list">
                    {{li.name}}
                </li>
            </ul>
        </div>
new Vue({
    el: '#app',
    data: {
        list: []
    },
    mounted() {
        this.init()
    },
    methods: {
        init() {
            this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
            this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
                    
        },
    }
})

我們會發(fā)現(xiàn) 這樣會報錯


error.png

如下修改:

new Vue({
    el: '#app',
    data: {
        list: []
    },
    mounted() {
        this.init()
    },
    methods: {
        init() {
                this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
                this.$nextTick(()=>{
                    this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
                })
        },
    }
})
image.png

我在獲取到數(shù)據(jù)后賦值給 data 對象的 list 屬性,然后我想引用ul元素找到第一個li把它的顏色變?yōu)榧t色,但是事實上,這個要報錯的。

我們知道,在執(zhí)行這句話時,ul 下面并沒有 li,也就是說剛剛進行的賦值操作,當(dāng)前并沒有引起視圖層的更新。

因為 Vue 的數(shù)據(jù)驅(qū)動視圖更新,是異步的,即修改數(shù)據(jù)的當(dāng)下,視圖不會立刻更新,而是等同一事件循環(huán)中的所有數(shù)據(jù)變化完成之后,再統(tǒng)一進行視圖更新。

因此,在這樣的情況下,vue 給我們提供了 nextTick 方法,如果我們想對未來更新后的視圖進行操作,我們只需要把要執(zhí)行的函數(shù)傳遞給 this.nextTick 方法,vue 在更新完視圖后就會執(zhí)行我們的函數(shù)幫我們做事情。

nextTick 可以讓我們在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào),用于獲得更新后的 DOM。

var callbacks = [];
var pending = false;

function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var 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).
var microTimerFunc;
var macroTimerFunc;
var 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 = function () {
    setImmediate(flushCallbacks);
  };
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = function () {
    port.postMessage(1);
  };
} else {
  /* istanbul ignore next */
  macroTimerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  microTimerFunc = function () {
    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;
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
function withMacroTask (fn) {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true;
    var res = fn.apply(null, arguments);
    useMacroTask = false;
    return res
  })
}
function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    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(function (resolve) {
      _resolve = resolve;
    })
  }
}

綜合上面的代碼我們可以知道
在 Vue 2.4 之前都是使用的 microtasks,但是 microtasks 的優(yōu)先級過高,在某些情況下可能會出現(xiàn)比事件冒泡更快的情況,但如果都使用 macrotasks 又可能會出現(xiàn)渲染的性能問題。所以在新版本中,會默認(rèn)使用 microtasks,但在特殊情況下會使用 macrotasks,比如 v-on。

對于實現(xiàn) macrotasks ,會先判斷是否能使用 setImmediate ,不能的話降級為 MessageChannel ,以上都不行的話就使用 setTimeout
setImmediate傳送門
MessageChannel傳送門
event-loops傳送門

總結(jié)一下今天的知識

(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ù)上面的第三步

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容