チェーン呼び出しとイベントループ--JavaScript面接問題の思考

9770 ワード

前言:昨日グループの中で(jin)論(chui)技(niu)術(pi)を求めて、ある鉄さんは彼の面のある会社の面接問題を出して、ついでに保存しました.今朝、少し時間をかけてこの問題を作りましたが、面白いことに気づき、今日まじめに仕事をする前に、私の解題案と思考過程をみんなと分かち合うことにしました.
テーマは以下の通りです(自分で考えてみると、私より良い方法が考えられるかもしれません):
小さな目で見ると、これらのニーズはチェーン呼び出しを実現することですが、チェーン呼び出しで最も一般的なのはjQueryであり、私たちがよく知っているPromiseです.
jQueryにおけるチェーン呼び出しの原理は、関数の末尾return this(すなわち、このオブジェクト自体を返す)において、オブジェクトが自身の関数を呼び出し続け、チェーン呼び出しをサポートすることができるようにすることである.
このコースを知ったら、次にこのコースに従って最初の小さなニーズに合った関数をすばやく書くことができます.
const LazyMan = function (name) {
  console.log(`Hi i am ${name}`);
}
LazyMan('Tony')
// Hi i am Tony

わずか3行のコードしかありませんが、エラーは報告されず、実行が速く、最初の小さなニーズを完璧に実現しました.
ある通行人:“待って、、1つの簡単な関数ではありませんて、方法はどこに使いますか?”
ああ、あなたに発見されました.若者はいいですね.はい、今チェーン調で2番目の小さな需要を実現しました.
  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
ナニー、eatはなぜundefinedで呼び出されたのか、私はsleepの中でthisに戻ったのではないでしょうか!?Chromeがまたこっそり更新して新しいバグを追加したのか、、、
しかし、Googleエンジニアはそんなに頼りにならないわけではないでしょう.まさか私が書き間違えたの?
コードをスキャンすると、return thissetTimeoutの処理関数で返され、sleepではなく、少し変更されます.
// ...
sleep(timeout) {
        setTimeout(function () {
          console.log(`   ${timeout} ....`);
        }, timeout)
        return this;
      };
// ...

もうちょっと走って、赤いストライプはありません.ねえ.
しかし、よく見ると、需要の順序と一致していません.私たちの現在の出力はこうです.
Hi i am Tony
I am eating lunch
   10 

Emmmmmさん、今JavaScriptのハードな腕を出さなければなりませんね.
JavaScriptには同期タスクと非同期タスクがあり、同期タスクは私たちが作成した順序で実行スタックに押し込み、一歩一歩実行します.setTimeoutは非同期のタスクであり、ブラウザではタイミングトリガスレッドが担当し、このスレッドはタイミングを計時し、計時が完了するとこのイベントのhandlerをタスクキューに押し込み、タスクキュー内のタスクは、実行スタックが空の場合、キュー内のタスクを実行スタックに捨てて実行する必要がある(ここからもhandlerが時間通りに実行できないことが分かる).(手当たり次第にスケッチを描いたので、ちょっと醜いですが、私が表現したい意味には影響しないはずです)
よくわからなかったら、この文章を参考にして、JavaScriptの実行メカニズムを徹底的に理解して、とても分かりやすく書くことができます.
この知識を知った後、卵を並べて、それは私たちに必要なコードを書くことができません.の
空気が静かになって数十分後、私はまだ何の見当もつかず、コップを手に取って、起きてコップの水圧を倒す準備をしていたが、突然稲妻が私に当たったように、vueの中でnextTickを実現する方法で実現したコードが頭に浮かんだ.コードははっきりしていないが(私は全然覚えていない)、私はこれが何か問題を解決するのに役立つはずだ.so、私はコップを置いて、熟練してあるhubを開けて、中でnextTickの実現コード(ここでnext-tick.js)を見つけました.最初の行から最後の行まですばやくスキャンすると、実行する必要がある関数をcallbacks配列で格納し、micro taskおよびmacro 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

実行が終わって、出力は需要とそっくりで、へへへ.
次に、3番目の小さなニーズに従って実行します.結果は次のとおりです.
//...

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();
  };

走って、3番目の小さな需要の出力とそっくりで、ねえへへ、おかずは一皿です.
最後に、この小さな要件において、チェーン呼び出しにはsleepFirstが追加され、sleepがチェーン呼び出しの最前端に持ち上げられて実行される、すなわちsleepFirstの優先度が最も高いという効果がある.
優先度に基づいて操作できるデータ構造は、私の知っている範囲では優先キューのみであり、優先キューは配列で実現することができ、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が1回通過するたびに、indexが+1され、新しいcallbackのグループがあることを示し、eatが実行されると、現在のindexに対応する配列が存在するか否かを判断し、存在しない場合は対応する空の配列を作成し、対応する呼び出す必要がある関数をこの配列に追加し、最後にこの配列をcallbackQueueに保存し、追加が完了すると、callbackQueueから順番に一歩一歩取り出して実行される.
私の考えは正しいはずですが、中に含まれている赤いメモをぼんやりと感じて、まずブラウザをなくして走ってみました.
結果は次のとおりです.
Hi i am Tony
I am eating lunch
I am eating dinner
   5 ....
   10 ....

やはり、必要な順序で実行されていません.ここではsleepFirstの優先度を処理できるという根本的な問題はありませんから.の
待って...私はさっき何を言ったのか、「優先度」、私たちは上にひっくり返して、私は前にこの言葉を言ったことがあるようです.
そう、vuenextTickで使用されています.これを参考にして、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の中でも同様に、欧耶、完璧です.
最後にいくつかの粗い図を描いて、この実行の過程を簡単に説明します.
チェーン呼び出しなので、チェーン上のはスタックに入って実行されます.ええ、実行スタックにはsleepとsleepFirstが描かれていません.の
Hi i am Tony

ここでsettimeoutのhandlerはマクロタスクであり、marco taskキューに参加する.Promise.resolve().thenのコールバックはマイクロタスクになり、micro taskキューに追加されます.
その後、実行スタックが空になり、micro taskの空になっていないタスクが実行スタックに加わって実行され、
セットタイムアウトが1つあるので、handlerをmacro taskに追加します.
前のマイクロタスクが実行されるとスタックが出て、このときmacro taskの最初のタスクが実行スタックに入って実行されます.
このときcallbacksがあれば実行されます
関数の内部にsettimeoutがあるので、handlerをmacro taskに追加します.
その後、実行スタックを空にして、次のマクロタスクを続行します.
   5 ....
I am eating lunch
I am eating dinner

実行スタックが空で、最後のマクロタスクをスタックに投げ込んで実行します.
   10 ....
I am eating junk food

最後にまとめると、この問題の難点はevent loopで解決したいかどうかで、この方向に考えれば簡単です.
また、普段あまりペンを動かさない(例えば私)、最初から文章を書くと喉が詰まってしまい、多くの内容が漏れてしまいます.だから、普段は時間があればもっとペンを動かして、文章を書く必要がありますが、東拼西凑一篇ではなく、本当に自分の思考と悟りを持っていなければなりません.
最后に各位の见官の殿さまに1つの小さい需要の练习手を追加します:
 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