国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

做面試的不倒翁:一道事件循環題引發的血案

ispring / 2452人閱讀

摘要:通過查看的文檔可以發現整個分為個階段定時器相關任務,中我們關注的是它會執行和中到期的回調執行某些系統操作的回調內部使用執行,一定條件下會在這個階段阻塞住執行的回調如果或者關閉了,就會在這個階段觸發事件,執行事件的回調的代碼在文件中。

這次我們就不要那么多前戲,直奔主題,我們的龍門陣正式開始。

開局一道題,內容全靠吹。(此處應有滑稽)

// 文件名: index.js
// 我們盡量模擬所有的異步場景,包括 timers、Promise、nextTick等等
setTimeout(() => {
  console.log("timeout 1");
}, 1);

process.nextTick(() => {
  console.log("nextTick 1");
});

fs.readFile("./index.js", (err, data) => {
  if(err) return;
  console.log("I/O callback");
  process.nextTick(() => {
      console.log("nextTick 2");
  });
});

setImmediate(() => {
  console.log("immediate 1");
  process.nextTick(() => {
      console.log("nextTick 3");
  });
});

setTimeout(() => {
  console.log("timeout 2");
  process.nextTick(() => {
    console.log("nextTick 4");
  });
}, 100);

new Promise((resolve, reject) => {
  console.log("promise run");
  process.nextTick(() => {
      console.log("nextTick 5");
  });
  resolve("promise then");
  setImmediate(() => {
      console.log("immediate 2");
  });
}).then(res => {
  console.log(res);
});

note: 上面的代碼執行環境是 node v10.7.0,瀏覽器的事件循環和 node 還是有一點區別的,有興趣的可以自己找資料看一看。

好了,上面的代碼涉及到定時器、nextTick、Promise、setImmediate 和 I/O 操作。頭皮有點小發麻哈,大家想好答案了么?檢查一下吧!

promise run
nextTick 1
nextTick 5
promise then
timeout 1
immediate 1
immediate 2
nextTick 3
I/O callback
nextTick 2
timeout 2
nextTick 4

怎么樣?跟自己想的一樣么?不一樣的話,就聽我慢慢道來。

event loop

在 Node.js 中,event loop 是基于 libuv 的。通過查看 libuv 的文檔可以發現整個 event loop 分為 6 個階段:

timers: 定時器相關任務,node 中我們關注的是它會執行 setTimeout() 和 setInterval() 中到期的回調

pending callbacks: 執行某些系統操作的回調

idle, prepare: 內部使用

poll: 執行 I/O callback,一定條件下會在這個階段阻塞住

check: 執行 setImmediate 的回調

close callbacks: 如果 socket 或者 handle 關閉了,就會在這個階段觸發 close 事件,執行 close 事件的回調

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

event loop 的代碼在文件 deps/uv/src/unix/core.c 中。

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  // 確定 event loop 是否繼續
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop); // 更新時間
    uv__run_timers(loop); // timers 階段
    ran_pending = uv__run_pending(loop); // pending callbacks 階段
    uv__run_idle(loop); // idle 階段
    uv__run_prepare(loop); // prepare 階段

    timeout = 0;
    // 設置 poll 階段的超時時間,有以下情況超時時間設為 0,此時 poll 不會阻塞
    // 1. stop_flag 不為 0
    // 2. 沒有活躍的 handles 和 request
    // 3. idle、pending callback、close 階段 handle 隊列不為空
    // 否則的話會將超時時間設置成距離當前時間最近的 timer 的時間
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    // poll 階段
    uv__io_poll(loop, timeout);
    // check 階段
    uv__run_check(loop);
    // close 階段
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}
注冊加觸發

這一小節我們主要看看 Node 如何將我們寫的定時器等等注冊到 event loop 中去并執行的。

以 setTimeout 為例,首先我們進到了 timers.js 這個文件中,找到了 setTimeout 函數,我們主要關注這么兩句:

function setTimeout(callback, after, arg1, arg2, arg3) {
  // ...
  const timeout = new Timeout(callback, after, args, false);
  active(timeout);

  return timeout;
}

我們看到它 new 了一個 Timeout 類,我們順著這條線索找到了 Timeout 的構造函數:

function Timeout(callback, after, args, isRepeat) {
  // ...
  this._onTimeout = callback;
  // ...
}

我們主要關注這一句,Node 將回調掛載到了 _onTimeout 這個屬性上。那么這個回調是在什么時候執行的呢?我們全局搜一下 _onTimeout(),我們可以發現是一個叫做 ontimeout 的方法執行了回調,好了,我們開始順藤摸瓜,可以找到這么一條調用路徑 processTimers -> listOnTimeout -> tryOnTimeout -> ontimeout -> _onTimeout

最后的最后,我們在文件的頭部發現了這么幾行代碼:

const {
  getLibuvNow,
  setupTimers,
  scheduleTimer,
  toggleTimerRef,
  immediateInfo,
  toggleImmediateRef
} = internalBinding("timers");
setupTimers(processImmediate, processTimers);

我們一看,setupTimers 是從 internalBinding("timers") 獲取的,我們去看一下 internalBinding 就知道這就是 js 代碼和內建模塊關聯的地方了。于是,我們順著這條線索往下找,我們去 src 目錄下去找叫 timers 的文件,果不其然,我們找到一個叫 timers.cc 的文件,同時,找到了一個叫 SetupTimers 的函數。

void SetupTimers(const FunctionCallbackInfo& args) {
  CHECK(args[0]->IsFunction());
  CHECK(args[1]->IsFunction());
  auto env = Environment::GetCurrent(args);

  env->set_immediate_callback_function(args[0].As());
  env->set_timers_callback_function(args[1].As());
}

上面的 args[1] 就是我們傳遞的 processTimers,在這個函數中我們其實就完成了 processTimers 的注冊,它成功的注冊到了 node 中。

那是如何觸發的回調呢?這里我們首先先看到 event loop 代碼中的 timers 階段執行的函數,然后跟進去:

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
    heap_node = heap_min(timer_heap(loop));
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)
      break;

    uv_timer_stop(handle);
    uv_timer_again(handle);
    handle->timer_cb(handle);
  }
}

這段代碼我們將我們的目光放在 handle->timer_cb(handle) 這一行,這個函數是在哪兒定義的呢?我們全局搜一下 timer_cb 發現 uv_timer_start 中有這么一行代碼:

handle->timer_cb = cb;

所以我們知道是調用了 uv_timer_start 將回調函數掛載到了 handle 上。那么 cb 又是什么呢?其實你沿著代碼上去找就能發現其實 cb 就是 timers_callback_function,眼熟對么?這就是我們上面注冊進來觸發回調的函數 processTimers

恍然大悟,原來是這么觸發的回調,現在還有個問題,誰去調用的 uv_timer_start 呢?這個問題就簡單了,我們通過源碼可以知道是 ScheduleTimer 這個函數調用了,是不是感覺很熟悉,對,這個函數就是我們通過 internalBinding 引進來的 scheduleTimer 函數。

在這個地方就有點不一樣了。現在最新的 tag 版本和 github 上 node 最新的代碼是有區別的,在一次 pr 中,將 timer_wrap.cc 重構成了 timers.cc,并且移除了 TimerWrap 類,再說下面的區別之前,先補充一下 timer 對應的數據結構:

// 這是在有 TimeWrap 的版本
// 對應的時間后面是一個 timer 鏈表
refedLists = {
  1000: TimeWrap._list(TimersList(item<->item<->item<->item)),
  2000: TimeWrap._list(TimersList(item<->item<->item<->item)),
};
// 這是 scheduleTimer 的版本
refedLists = {
  1000: TimersList(item<->item<->item<->item),
  2000: TimersList(item<->item<->item<->item),
};

TimeWrap 的版本里,js 是通過調用實例化后的 start() 函數去調用了 uv_timer_start

scheduleTimer 版本是注冊定時器的時候通過比較哪個定時器是最近要執行的,從而將對應時間的 timerList 注冊到 uv_timer 中去。

那么,為什么要這么改呢?是為了讓定時器和 Immediate 擁有更相似的行為,也就是將單個 uv_timer_t handle 存在 Environment 上(Immediate 是有一個 ImmediateQueue,這也是個鏈表)。

這里就只說了一個 timer,其他的大家就自己去看看吧,順著這個思路大家肯定會有所收獲的。

事件循環流程

在加載 node 的時候,將 setTimeout、setInterval 的回調注冊到 timerList,將 Promise.resolve 等 microTask 的回調注冊到 microTasks,將 setImmediate 注冊到 immediateQueue 中,將 process.nextTick 注冊到 nextTickQueue 中。

當我們開始 event loop 的時候,首先進入 timers 階段(我們只看跟我們上面說的相關的階段),然后就判斷 timerList 的時間是否到期了,如果到期了就執行,沒有就下一個階段(其實還有 nextTick,等下再說)。

接下來我們說 poll 階段,在這個階段,我們先計算需要在這個階段阻塞輪詢的時間(簡單點就是下個 timer 的時間),然后等待監聽的事件。

下個階段是 check 階段,對應的是 immediate,當有 immediateQueue 的時候就會跳過 poll 直接到 check 階段執行 setImmediate 的回調。

那有同學就要問了,nextTick 和 microTasks 去哪兒了啊?別慌,聽我慢慢道來。

process.nextTick 和 microTasks

現在我們有了剛剛找 timer 的經驗,我們繼續去看看 nextTick 是怎么執行的。

經過排查我們能找到一個叫 _tickCallback 的函數,它不斷的從 nextTickQueue 中獲取 nextTick 的回調執行。

function _tickCallback() {
    let tock;
    do {
      while (tock = queue.shift()) {
        // ...
        const callback = tock.callback;
        if (tock.args === undefined)
          callback();
        else
          Reflect.apply(callback, undefined, tock.args);

        emitAfter(asyncId);
      }
      tickInfo[kHasScheduled] = 0;
      runMicrotasks();
    } while (!queue.isEmpty() || emitPromiseRejectionWarnings());
    tickInfo[kHasPromiseRejections] = 0;
  }

我們看到了什么?在將 nextTick 的回調執行完之后,它執行了 runMicrotasks。一切都真相大白了,microTasks 的執行時機是當執行完所有的 nextTick 的回調之后。那 nextTick 又是在什么時候執行的呢?

這就需要我們去找 C++ 的代碼了,在 bootstrapper.cc 里找到了 BOOTSTRAP_METHOD(_setupNextTick, SetupNextTick),所以我們就要去找 SetupNextTick 函數。

void SetupNextTick(const FunctionCallbackInfo& args) {
  Environment* env = Environment::GetCurrent(args);
  // ...
  env->set_tick_callback_function(args[0].As());
  // ...
}

我們關注這一句,是不是很熟啊,跟上面 timer 一樣是吧,我們將 __tickCallback 注冊到了 node,在 C++ 中通過 tick_callback_function 來調用這個函數。

我們通過查看源碼可以發現是 InternalCallbackScope 這個類調用 Close 函數的時候就會觸發 nextTixk 執行。

void InternalCallbackScope::Close() {
  if (closed_) return;
  closed_ = true;
  HandleScope handle_scope(env_->isolate());
  // ...
  if (!tick_info->has_scheduled()) {
    env_->isolate()->RunMicrotasks();
  }
  // ...
  if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) {
    return;
  }
  // ...
  if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
    failed_ = true;
  }
}

可能有同學有疑問了,為啥在執行 nextTick 上面還有 RunMicrotasks 呢?其實這是對 event loop 的優化,假如沒有 process.nextTick 就直接從 node 里面調用 RunMicrotasks 加快速度。

現在在 node.cc 里我們找到了調用 Close 的地方:

MaybeLocal InternalMakeCallback(Environment* env,
                                       Local recv,
                                       const Local callback,
                                       int argc,
                                       Local argv[],
                                       async_context asyncContext) {
  CHECK(!recv.IsEmpty());
  InternalCallbackScope scope(env, recv, asyncContext);

  scope.Close();

  return ret;
}

InternalMakeCallback() 則是在 async_wrap.ccAsyncWrap::MakeCallback() 中被調用。

找了半天,只找到了 setImmediate 注冊時,注冊函數執行回調運行了這個函數,沒有找到 timer 的。之前因為使用的 TimeWrap,TimeWrap 繼承了 AsyncWrap,在執行回調的時候調用了 MakeCallback(),問題是現在移除了 TimeWrap,那是怎么調用的呢?我們會到 js 代碼,發現了這樣的代碼:

const { _tickCallback: runNextTicks } = process;
function processTimers(now) {
  runNextTicks();
}

一切都明了了,在移除了 TimeWrap 之后,將 _tickCallback 放到了這里執行,所以我們剛剛在 C++ 里找不到。

其實,每一個階段執行完之后,都會去執行 _tickCallback ,只是方式可能有點不同。

答案解析

好了,剛剛了解了關于 event loop 的一些情況,我們再來看看文章開頭的那段代碼,我們一起來分析。

第一步

首先運行 Promise 里的代碼,輸出了 promise run,然后 promise.resolve 將 then 放入 microTasks。

第二步

這里要提到的一點是 nextTick 在注冊之后,bootstrap 構建結束后運行SetupNextTick函數,這時候就會清空 nextTickQueue 和 MicroTasks,所以輸出 nextTick 1、nextTick 5、promise then

第三步

在 bootstrap 之后便進入了 event loop,第一個階段 timers,這時 timeout 1 定時器時間到期,執行回調輸出 timeout 1,timerList 沒有其他定時器了,去清空 nextTickQueue 和 MicroTasks,沒有任務,這時繼續下階段,這時候有 immediate,所以跳過 poll,進入 check,執行 immediate 回調,輸出 immediate 1 和 immediate 2,并將 nextTick 3 推入 nextTickQueue,階段完成 immediateQueue 沒有需要處理的東西了,就去清空 nextTickQueue 和 MicroTasks 輸出 nextTick 3

第四步

在這一輪,文件讀取完成,并且 timers 沒到期,進入 poll 階段,超時時間設置為 timeout 2 的時間,執行回調輸出 I/O callback,并且向 nextTickQueue 推入 nextTick 2。阻塞過程中沒有其他的 I/O 事件,去清空 nextTickQueue 和 MicroTasks,輸出 nextTick 2

第五步

這時候又到了 timers 階段,執行 timeout 2 的回調,輸出 timeout 2,將 nextTick 4 推入 nextTickQueue,這時 timeList 已經沒有定時器了,清空 nextTickQueue 和 MicroTasks 輸出 nextTick 4

總結

不知道大家懂了沒有,整個過程其實還比較粗糙,在學習過程中也看了不少的源碼分析,但是 node 發展很快,很多分析已經過時了,源碼改變了不少,但是對于理清思路還是很有作用的。

各位看官如果覺得還行、OK、有點用,歡迎來我 GitHub 給個小星星,我會很舒服的,哈哈。

文 / 小烜同學
天衣無縫的秘密是:做技術,你快樂嗎?

編 / 熒聲

本文已由作者授權發布,版權屬于創宇前端。歡迎注明出處轉載本文。本文鏈接:https://knownsec-fed.com/2018...

想要訂閱更多來自知道創宇開發一線的分享,請搜索關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回復。

歡迎點贊、收藏、留言評論、轉發分享和打賞支持我們。打賞將被完全轉交給文章作者。

感謝您的閱讀。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/98271.html

相關文章

  • 一道JS面試引發血案

    摘要:項目組長給我看了一道面試別人的面試題。打鐵趁熱,再來一道題來加深下理解。作者以樂之名本文原創,有不當的地方歡迎指出。 showImg(https://segmentfault.com/img/bVbur0z?w=600&h=400); 剛入職新公司,屬于公司萌新一枚,一天下午對著屏幕看代碼架構時。BI項目組長給我看了一道面試別人的JS面試題。 雖然答對了,但把理由說錯了,照樣不及格。 ...

    fantix 評論0 收藏0
  • 一道JS面試引發"血案",透過現象尋本質,再從本質看現象

    摘要:一看這二逼就是周杰倫的死忠粉看看控制臺輸出,確實沒錯就是對象。從根本上來說,作用域是基于函數的,而執行環境是基于對象的例如全局執行環境即全局對象。全局對象全局屬性和函數可用于所有內建的對象。全局對象只是一個對象,而不是類。 覺得本人寫的不算很爛的話,可以登錄關注一下我的GitHub博客,博客會堅持寫下去。 今天同學去面試,做了兩道面試題,全部做錯了,發過來給我看,我一眼就看出來了,因為...

    QiShare 評論0 收藏0
  • Deep in JS - 收藏集 - 掘金

    摘要:今天同學去面試,做了兩道面試題全部做錯了,發過來給道典型的面試題前端掘金在界中,開發人員的需求量一直居高不下。 排序算法 -- JavaScript 標準參考教程(alpha) - 前端 - 掘金來自《JavaScript 標準參考教程(alpha)》,by 阮一峰 目錄 冒泡排序 簡介 算法實現 選擇排序 簡介 算法實現 ... 圖例詳解那道 setTimeout 與循環閉包的經典面...

    enali 評論0 收藏0
  • 一道面試引發思考 --- Event Loop

    摘要:想必面試題刷的多的同學對下面這道題目不陌生,能夠立即回答出輸出個,可是你真的懂為什么嗎為什么是輸出為什么是輸出個這兩個問題在我腦邊縈繞。同步任務都好理解,一個執行完執行下一個。本文只是我對這道面試題的一點思考,有誤的地方望批評指正。 想必面試題刷的多的同學對下面這道題目不陌生,能夠立即回答出輸出10個10,可是你真的懂為什么嗎?為什么是輸出10?為什么是輸出10個10?這兩個問題在我腦...

    betacat 評論0 收藏0
  • 一道面試引發思考

    摘要:下面我們來使用面向對象類圖這里就不再畫了首先面試題中所提到的我們都可以看成類,比如停車場是一個類吧,它里面的車位是一個類吧,攝像頭,屏幕。。。 以下是某場的一道面試題(大概): 1、一個停車場,車輛入場時,攝像頭記錄下車輛信息2、屏幕上顯示所接收的車輛的信息情況(車牌號)以及各層車位的車位余量3、停車場一共四層車位,其中的三層都為普通車位,還有一層為特殊車位(體現在停車計費價格上面的不...

    Apollo 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<