VUE3.0 をすぐに始める方法: 学習の開始
Node.js に関して言えば、ほとんどのフロントエンド エンジニアはそれをベースにしたサーバーの開発を考えると思います。フルスタックになるには JavaScript をマスターするだけで済みます。エンジニアですが、実は Node.js の意味 そしてそれだけではありません。
多くの高級言語では、実行権限がオペレーティング システムに到達しますが、ブラウザー側で実行される JavaScript は例外です。ブラウザーによって作成されるサンドボックス環境は、フロントエンド エンジニアをプログラミングの世界の象牙の塔に閉じ込めます。しかし、Node.jsの登場によりこの欠点は補われ、フロントエンドエンジニアもコンピュータの世界の底辺に到達できるようになりました。
したがって、フロントエンド エンジニアにとって Nodejs の重要性は、フルスタックの開発機能を提供するだけでなく、さらに重要なことに、フロントエンド エンジニアにコンピューターの基礎となる世界への扉を開くことです。この記事では、Node.js の実装原則を分析することでこの扉を開きます。
Node.js ソース コード ウェアハウスの /deps ディレクトリには、C 言語で書かれたモジュール (libuv、V8 など) や JavaScript 言語 (acorn など) で書かれたモジュールなど、十数の依存関係があります。 、acorn-plugins)を以下に示します。
それらの中で最も重要なのは、v8 ディレクトリと uv ディレクトリに対応するモジュールです。 V8 自体には非同期で実行する機能はありませんが、ブラウザーの他のスレッドの助けを借りて実装されています。これが、JS がシングルスレッドであると言われる理由です。これは、その解析エンジンが同期解析コードのみをサポートしているためです。ただし、Node.js では、非同期実装は主に libuv に依存します。libuv の実装原理の分析に焦点を当てましょう。
libuv は、C で書かれた非同期 I/O ライブラリで、主にブロッキングを引き起こしやすい I/O 操作の問題を解決します。元々は Node.js で使用するために特別に開発されましたが、その後 Luvit、Julia、pyuv などの他のモジュールで使用されるようになりました。以下の図はlibuvの構造図です。
libuv には 2 つの非同期実装メソッドがあります。これらは、上の図の左右の黄色のボックスで選択された 2 つの部分です。
左側の部分はネットワーク I/O モジュールで、プラットフォームごとに異なる実装メカニズムがあります。Linux システムは epoll を使用して実装し、OSX およびその他の BSD システムは KQueue を使用し、SunOS システムはイベント ポートを使用し、Windows システムは IOCP を使用します。これにはオペレーティング システムの基盤となる API が関係するため、理解するのがさらに複雑になるため、ここでは紹介しません。
右側の部分には、ファイル I/O モジュール、DNS モジュール、およびスレッド プールを介して非同期操作を実装するユーザー コードが含まれています。ファイル I/O はネットワーク I/O とは異なります。libuv はシステムの基礎となる API に依存しませんが、ブロックされたファイル I/O 操作をグローバル スレッド プールで実行します。
次の図は libuv 公式 Web サイトに掲載されているイベントポーリングのワークフロー図です。コードと合わせて分析してみましょう。
libuv イベント ループのコア コードは uv_run() 関数に実装されています。以下は、Unix システムでのコア コードの一部です。 C言語で書かれていますが、JavaScriptと同じような高級言語なので、理解することはそれほど難しくありません。最大の違いはアスタリスクと矢印です。アスタリスクは単に無視して構いません。たとえば、関数パラメータの uv_loop_t* ループは、uv_loop_t 型の変数ループとして理解できます。矢印「→」はピリオド「.」として理解でき、例えばloop→stop_flagはloop.stop_flagとして理解できる。
int uv_run(uv_loop_t* ループ、uv_run_mode モード) { ... r = uv__loop_alive(ループ); if (!r) uv__update_time(loop); while (r != 0 && ループ - >stop_flag == 0) { uv__update_time(ループ); uv__run_timers(ループ); ran_pending = uv__run_pending(ループ); uv__run_idle(loop); uv__run_prepare(ループ);...uv__io_poll(ループ, タイムアウト); uv__run_check(ループ); uv__run_closed_handles(loop);... }...
uv__loop_alive
この関数は、
イベント ポーリングを続行するかどうかを決定するために使用されます。ループ オブジェクト内にアクティブなタスクがない場合は、0 を返し、ループを終了します。
C言語では、この「タスク」には「ハンドル」という専門名が付けられており、タスクを指す変数として理解できます。ハンドルは、それぞれ短命のサイクルハンドルと長期的なサイクルハンドルを表すリクエストとハンドルの2つのカテゴリに分けることができます。特定のコードは次のとおりです。StaticInt
UV__LOOP_ALIVE(const uv_loop_t * loop){ return uv__has_active_handles(loop) || uv__has_active_reqs(loop) ループ - >closed_handles != NULL; }
uv__update_time
为了减少与时间相关的系统调用次数,同构这个函数来缓存当前系统时间,精度很高,可以达到纳秒级别,但单位还是毫秒。
具体的なソース コードは次のとおりです。
UV_UNUSED(static void uv__update_time(uv_loop_t *loop)) { ループ - > time = uv__hrtime(uv_clock_fast) / 1000000;
uv__run_timers は
、
setTimeout() および setInterval() の時間しきい値に達したコールバック関数を実行します。この実行プロセスは、for ループのトラバーサルによって実装されます。以下のコードからわかるように、タイマー コールバックは最小ヒープ構造のデータに格納され、最小ヒープが空になるか、時間のしきい値に達しないときに終了します。 。
タイマー コールバック関数を実行する前にタイマーを削除します。繰り返しが設定されている場合は、再度最小ヒープに追加する必要があります。その後、タイマー コールバックが実行されます。
具体的なコードは次のとおりです。
void uv__run_timers(uv_loop_t *loop) { 構造体ヒープノード * ヒープノード; uv_timer_t * ハンドル; のために (;;) { heap_node = heap_min(timer_heap(loop)); if (heap_node == NULL) ブレーク; ハンドル = コンテナーオブ(ヒープノード, uv_timer_t, ヒープノード); if (ハンドル - >タイムアウト > ループ - >時間) ブレーク; uv_timer_stop(ハンドル); uv_timer_again(ハンドル); ハンドル ->timer_cb(ハンドル); }
uv__run_pending は
、
pending_queue に格納されているすべての I/O コールバック関数を走査し、pending_queue が空の場合は 0 を返し、それ以外の場合は、pending_queue 内のコールバック関数の実行後に 1 を返します。
コードは次のとおりです。
static int uv__run_pending(uv_loop_t *loop) { キュー * q; キュー pq; uv__io_t * w; if (QUEUE_EMPTY( & ループ - >pending_queue)) は 0 を返します。 QUEUE_MOVE( & ループ - >pending_queue, &pq); while (!QUEUE_EMPTY( & pq)) { q = QUEUE_HEAD( & pq); QUEUE_REMOVE(q); queue_init(q); w = QUEUE_DATA(q, uv__io_t, pending_queue); w - >cb(ループ, w, POLLOUT); } 1を返します。3 つの関数
uvrun_idle / uvrun_prepare / uv__run_check
はすべて、マクロ関数 UV_LOOP_WATCHER_DEFINE によって定義されます。マクロ関数は、
コード テンプレート、または関数を定義するために使用される関数として理解できます。マクロ関数は 3 回呼び出され、名前パラメーターの値 prepare、check、idle がそれぞれ渡されます。同時に、uvrun_idle、uvrun_prepare、uv__run_check の 3 つの関数が定義されます。
したがって、それらの実行ロジックはすべて一貫しています。これらはすべて、キューループのオブジェクトを取り出します。
#define UV_LOOP_WATCHER_DEFINE(名前、タイプ) void uv__run_##name(uv_loop_t* ループ) { uv _ ## name ## _ t* h; QUEUE キュー; キュー* q; QUEUE_MOVE(&loop->name##_handles, &queue); while (!QUEUE_EMPTY(&queue)) { q = QUEUE_HEAD(&キュー); h = QUEUE_DATA(q, uv_##name##_t, キュー); QUEUE_REMOVE(q); QUEUE_INSERT_TAIL(&loop->name##_handles, q); h->名前##_cb(h); } } UV_LOOP_WATCHER_DEFINE(準備、PREPARE) UV_LOOP_WATCHER_DEFINE(チェック、チェック) UV_LOOP_WATCHER_DEFINE(idle, IDLE)
uv__io_poll
uv__io_poll は主に I/O 操作をポーリングするために使用されます。特定の実装は、オペレーティングシステムによって異なります。
uv__io_poll 関数には多くのソース コードがあり、その中心となるのは次の 2 つのループ コードです。
void uv__io_poll(uv_loop_t *loop, int timeout) { while (!QUEUE_EMPTY( & ループ - >watcher_queue)) { q = QUEUE_HEAD( & ループ - >watcher_queue); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, watcher_queue); e.events = w ->イベント; e.data.fd = w ->fd; if (w - >events == 0) op = EPOLL_CTL_ADD; それ以外の場合、op = EPOLL_CTL_MOD; if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) { if (errno != EEXIST) abort(); if(epoll_ctl(loop-> backend_fd、epoll_ctl_mod、w-> fd、&e))abort(); } w - >イベント = w - >イベント; } のために (;;) { for (i = 0; i < nfds; i++) { pe = イベント + i; fd = pe ->data.fd; w = ループ - >watchers[fd]; pe - >イベント &= w - >イベント POLLHUP | if(pe-> events == pollerr || pe-> events == pollhup)pe-> events | = w-> pevents&(pollin | pollout | uv__pollrdhup | uv__pollpri); if (pe - >イベント != 0) { if (w == &loop - >signal_io_watcher) have_signals = 1; else w-> cb(loop、w、pe-> events); nevents++; } } if(has_signals!= 0)loop-> signal_io_watcher.cb(loop、&loop-> signal_io_watcher、pollin); }... }
whileループで、オブザーバーキューウォッチャー_queueをトラバースし、イベントを取り出してファイル記述子をイベントオブジェクトEに割り当て、epoll_ctl関数を呼び出してepollイベントを登録または変更します。
forループでは、epollで待機しているファイル記述子が削除され、NFDに割り当てられ、NFDSが通信されてコールバック関数を実行します。
uv__run_closing_handlesは、
閉じられるのを待ってキューを横断し、ストリーム、TCP、UDPなどのハンドルを閉じてから、ハンドルに対応するclose_cbを呼び出します。コードは次のとおりです。
静的void uv__run_closing_handles(uv_loop_t * loop){ uv_handle_t * p; uv_handle_t * q; p =ループ - > closit_handles; ループ ->closed_handles = NULL; while(p){ q = p ->next_close; uv__finish_close(p); p = q; } }
tTickとPromiseはどちらも非同期APIですが、イベントポーリングの一部ではありません。したがって、これら2つの非同期APIを使用する場合、着信コールバック関数で長いタスクまたは再帰が実行される場合、イベントポーリングがブロックされ、それによって「飢え」I/O操作がブロックされます。
次のコードは、prcoess.nexttickを再帰的に呼び出すことでfs.readfileのコールバック関数を実行できない例です。
fs.readFile('config.json', (err, data) = >{... })const traverse =()=> { process.nexttick(traverse) }
この問題を解決するには、Setimmediateがイベントループでコールバック関数キューを実行するため、代わりにSetimmediateを使用できます。 process.nextTick タスク キューは Promise タスク キューよりも高い優先順位を持っています。具体的な理由については、次のコードを参照してください。
function processTicksAndRejections() { tockをさせてください。 する { while(tock = queue.shift()){ const asyncId = tock[async_id_symbol]; emitbefore(asyncid、tock [trigger_async_id_symbol]、tock); 試す { const コールバック = tock.callback; if (tock.args === 未定義) { 折り返し電話(); } それ以外 { const args = tock.args; switch(args。length){ ケース1: callback(args [0]); 壊す; ケース 2: callback(args [0]、args [1]); 壊す; ケース 3: callback(args [0]、args [1]、args [2]); 壊す; ケース4: callback(args [0]、args [1]、args [2]、args [3]); 壊す; デフォルト: コールバック(... args); } } } ついに { if(destroyhooksexist())emitdestroy(非薄暗い); } emitafter(非微細); } runmicrotasks(); } while(!queue。isempty()|| processpromiserejections()); setHasTickScheduled(false); setHasRejectionToWarn(false); }
ProcessTickSandRejections()関数からわかるように、キューキューのコールバック関数は最初にWhile Loopを使用し、キューキューのコールバック関数はProcess.nextTickを介して追加されます。 whileループが終了すると、runmicrotasks()関数が呼び出され、Promiseコールバック関数が実行されます。
libuvに依存するnode.jsは、1つの部分に分割されます/o、dns、およびユーザーコード。
非同期操作を処理するLibuvのコアメカニズムは、イベントポーリングがいくつかのステップに分かれています。
最後に、非同期APIプロセスと約束は、イベントポーリングに属していないことがイベントの投票をブロックすることです。