鏈式調用與事件循環--一道JavaScript面試題的思考

前言:昨天在羣裏討(jin)論(chui)技(niu)術(pi),有位老鐵發了一道他面的某公司面試題,順手保存了。今早花了一點時間把這題做了出來,發現挺有意思的,決定在今天認真工(hua)作(shui)前,與大家分享我的解題方案和思考過程。

題目如下(可以自己先思考一會,沒準可以想出比我更好的方法):

clipboard.png



小眼一撇,這幾個需求都是要實現鏈式調用,而鏈式調用最常見的是 jQuery,還有就是我們非常熟悉的 Promise。

jQuery中鏈式調用的原理是在函數的末尾return this(即返回這個對象自身),使得對象可以繼續調用自身的函數從而達到支持鏈式調用。

知道了這個套路之後,接下來我們可以按照這個套路飛快的先寫出符合第一個小需求的函數。

const LazyMan = function (name) {
  console.log(`Hi i am ${name}`);
}
LazyMan('Tony')
// Hi i am Tony

雖然只有短短三行代碼,但沒有報一點錯,而且運行起來飛快的,完美的實現了第一個小需求。

f3d57c6ca0419d41e3cbbf21013c7f74

路人甲:“等等,,不就是一個簡單的函數,套路在哪”

嘖嘖,被你發現了,小夥子不錯嘛,好,現在就用鏈式調把第二個小需求實現了:

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);
    class F {
      sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒`);
          return this;
        }, timeout)
      };

      eat(food) {
        console.log(`I am eating ${food}`);
        return this;
      }
    }
    return new F();
  }

LazyMan('Tony').sleep(10).eat('lunch')

丟瀏覽器裏面跑一下,一段紅條條蹦了出來

Uncaught TypeError: Cannot read property 'eat' of undefined

15e8c84a9fea062128e753880fd5b7d4

納尼,eat爲什麼會在undefined上調用,我不是在sleep中返回了this麼!?是不是 Chrome 又偷偷更新,加了一個新 bug,,,

72270bb84093d8a24f43db9d53271db3

不過 google 工程師應該沒有這麼不靠譜吧。難道是我寫錯了?

deb0ffca089deb1792b55c5ddee9cadd

掃一遍代碼,發現return this是在setTimeout中的處理函數返回的,而不是sleep返回的,小改一下。

// ...
sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
        }, timeout)
        return this;
      };
// ...

再跑一下,沒有紅條條了,嘿。

e2c3a63d35170ef73750e4bd8eab32c3

但仔細一看,跟需求中的順序不一致,我們現在的輸出是這樣的:

Hi i am Tony
I am eating lunch
等待了10秒

e9620866e9a465cf73c79675ac398057

emmmmm,看來,現在得拿出一點 JavaScript 硬本事了。

JavaScript 中有同步任務和異步任務,同步任務就是按照我們編寫順序推入執行棧,一步一步執行;而setTimeout屬於異步任務,在瀏覽器中是由定時觸發器線程負責,這個線程會進行計時,當計時完成後將這個事件的handler推入到任務隊列中,任務隊列中的任務需要等待執行棧中爲空時把隊列中的任務丟入執行棧中進行執行(從這裏也可以知道handler並不能準時執行)。
(隨手畫了一張草圖,有點醜,不過應該不影響我想要表達的意思)

xnip2019-01-30_15-29-45

如果不太瞭解,可以參考這篇文章 這一次,徹底弄懂 JavaScript 執行機制 ,寫的非常易懂了

知道了這個知識後,然並卵,它不能幫我們寫出所需要的代碼。。。

688b227c69e52211c43d0bd123d9b4a2

在空氣安靜了數十分鐘後,我還是毫無頭緒,只好拿起杯子,準備起身去倒杯水壓壓驚,突然猶有一道閃電擊到了我一般,腦海中浮現了 vue 中實現nextTick這一方法實現的代碼,代碼雖模糊不清(我根本記不清楚了),但我造這應該可以幫助我解決點什麼問題。so,我放下杯子,熟練的打開某 hub,在裏面找到了nextTick的實現代碼(在這裏next-tick.js)。
快速從第一行到最後一行掃了一遍,可以獲取到的東東是:它用一個callbacks數組存儲需要執行的函數,然後利用micro taskmacro task的優先級特性,從而可以在 DOM 渲染之前執行callbacks中的回調。emmmmm,跟我現在的需求好像扯不上什麼關係,並不能給什麼幫助。不過我也可以把需要執行的函數加入一個數組中,在最後執行它。說幹就幹,可以快速寫出如下代碼:

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);
    function _eat(food){
      console.log(`I am eating ${food}`);
    }
    const callbacks = [];
    class F {
      sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
          callbacks.forEach(cb=>cb())
        }, timeout);
        return this;

      };

      eat(food) {
        callbacks.push(_eat.bind(null,food));
        return this;
      }
    }
    return new F();
  };

  LazyMan('Tony').sleep(10).eat('lunch')
  // Hi i am Tony
  // 等待了10秒....
  // I am eating lunch

執行完,輸出跟需求一模一樣,嘿嘿嘿。

6ba543ba4849600fd8a029c7bd2eebc7

接着按照第三個小需求執行一下,結果如下:

//...

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner')

// Hi i am Tony
// 等待了10秒
// I am eating lunch
// I am eating dinner

//...

沒有報錯,很好,但順序又錯了。。。這可不好辦。
眼看着空氣又要安靜下來了,我不能乾耗着,決定使用一些常用套路了,比如加個flag,區分是否是需要在 sleep 之後執行的方法,改寫後如下:

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);

    function _eat(food) {
      console.log(`I am eating ${food}`);
    }
    const callbacks = [];
    let isNeedSleep = false;
    class F {
      sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒`);
          callbacks.forEach(cb => cb())
        }, timeout);
        isNeedSleep = true;
        return this;
      };
      eat(food) {
        if (isNeedSleep) {
          callbacks.push(_eat.bind(null, food));
        } else {
          _eat.call(null, food);
        }
        return this;
      }
    }
    return new F();
  };

跑一下,跟第三個小需求輸出一模一樣,嘿嘿嘿,小菜一碟。

0a8487ab892816b7af21ae91d4bc1256

到最後這個小需求中,鏈式調用中多了一個sleepFirst,其效果是會將sleep提至鏈式調用的最前端來執行,也就是說sleepFirst的優先級最高。

fa075ddbe8b2aa4a67afce496acfa019

容我思考一下: 能夠根據優先級來操作的數據結構,在我所知的範圍內只有優先隊列,而優先隊列可以用數組來實現,so,是不是說可以用數組來實現優先級callbacks的調用,即用嵌套數組。答曰:你想的沒有錯啦。

擼起袖子繼續幹,於是數分鐘後有了下面這個函數

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);

    function _eat(food) {
      console.log(`I am eating ${food}`);
    }
    const callbackQueue = [];
    let index = 0;
    class F {
      sleep(timeout) {
        const _callbacks = callbackQueue.shift();
        _callbacks && _callbacks.forEach(cb => cb());
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
          const _callbacks = callbackQueue.shift();
          _callbacks && _callbacks.forEach(cb => cb())
        }, timeout);
        index ++;
        return this;
      };
      eat(food) {
        if(!callbackQueue[index]) callbackQueue[index] = [];
        callbackQueue[index].push(_eat.bind(null, food));
        return this;
      };
      sleepFirst(timeout){
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
          const _callbacks = callbackQueue.shift();
           _callbacks && _callbacks.forEach(cb => cb())
        }, timeout);
        index ++;
        return this;
      }
    }

    return new F();
  };

我的想法是 每經過一次sleep後,index會+1,表示有新的一組callback,當執行eat時,判斷是否存在當前index對應的數組,不存在則創建一個對應的空數組,然後把對應需要調用的函數添加入這個數組中,最後把這個數組存到callbackQueue中,當添加完成後,會按照順序一步一步從callbackQueue中取出並執行。

雖然我思路這思路應該是對的,但我還是隱隱約約感覺到了裏面蘊含的紅條條,先丟瀏覽器中跑一下試試。

結果如下:

Hi i am Tony
I am eating lunch
I am eating dinner
等待了5秒....
等待了10秒....

果然,沒有按照所需的順序執行,因爲這裏還是沒有能夠處理sleepFirst優先級的這個根本問題。。。

等等。。。我剛剛說了啥,'優先級',咱們往上翻,我前面好像提到過這個詞!

e38e86c1bed2f9243738c1b412a27bd1

沒錯,vue中的nextTick中就用到了,我們可以參考它,利用Event Loopmicro taskmacro task執行的優先級來解決這個問題。

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);

    function _eat(food) {
      console.log(`I am eating ${food}`);
    }

    const callbackQueue = [];
    let index = 0;

    class F {
      sleep(timeout) {
        setTimeout(() => {
          const _callbacks = callbackQueue.shift();
          _callbacks && _callbacks.forEach(cb => cb());
          setTimeout(function () {
            console.log(`等待了${timeout}秒....`);
            const _callbacks = callbackQueue.shift();
            _callbacks && _callbacks.forEach(cb => cb())
          }, timeout);
        })
        index++;
        return this;
      };

      eat(food) {
        if (!callbackQueue[index]) callbackQueue[index] = [];
        callbackQueue[index].push(_eat.bind(null, food));
        return this;
      };

      sleepFirst(timeout) {
        Promise.resolve().then(() => {
          const _callbacks = callbackQueue.shift();
          setTimeout(function () {
            console.log(`等待了${timeout}秒....`);
            _callbacks && _callbacks.forEach(cb => cb())
          }, timeout);
        })
        index++;
        return this;
      }
    }

    return new F();
  };

丟瀏覽器執行一下,完全冇問題,丟 node 中也一樣,歐耶,完美。

f70ed1ce5995e0061c3edcc645516d1e

最後畫幾張粗糙的圖,簡單描述一下這個執行的過程:

xnip2019-01-30_19-52-31

因爲是鏈式調用,所以在鏈上的都會入棧然後執行,額,執行棧少畫了 sleep 和 sleepFirst。。。

Hi i am Tony

其中 setTimeout 的 handler 爲宏任務,加入marco task隊列中;Promise.resolve().then的回調爲微任務,加入micro task隊列中

xnip2019-01-30_19-52-59

然後執行棧被清空,micro task中未清空的任務加入執行棧中被執行,

xnip2019-01-30_19-54-10

因爲其中有一個 setTimeout,所以把其 handler 加入macro task

xnip2019-01-30_19-54-34

前面的微任務執行完就出棧了,這時候macro task中第一個任務入執行棧中進行執行

這個時候如果有 callbacks 就會執行

xnip2019-01-30_19-55-33

因爲函數內部又有一個 setTimeout,於是把它的 handler 加入macro task

xnip2019-01-30_19-56-09

然後清空執行棧,繼續執行下一個宏任務

等待了5秒....
I am eating lunch
I am eating dinner

xnip2019-01-30_19-56-25

執行棧爲空,把最後一個宏任務丟進棧中執行

等待了10秒....
I am eating junk food


最後總結一下,這道題的難點是能否想到用event loop來解決,如果能往這方向去想了,做起來就很簡單了。

還有平時不怎麼動筆的(比如我),一開始寫起文章來就會如鯁在喉,許多內容都寫漏了。所以平時有時間就要多動動筆,寫寫文章,但也不是說東拼西湊一篇,而是真的要有自己的思考和感悟。



最最最後給各位看官老爺多添加一個小需求練練手:

 LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food').eat('healthy food')
 // Hi i am Tony
 // 等待了5秒
 // I am eating lunch
 // I am eating dinner
 // 等待了10秒
 // I am eating junk food
 // I am eating healthy food
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章