Reactの実行メカニズムの分析
8542 ワード
引用useEffectとuseLayoutEffectはReactが公式に発表した2つのhooksで、いずれも副作用を実行するためのフック関数で、名前は似ていて、機能は似ていて、唯一の違いは実行のタイミングに違いがあります.今日の文章は主にこの2つのフック関数の実行タイミングから着手して、Reactの実行原理とブラウザのレンダリングの流れを分析します.
公式解釈useLayoutEffectその関数署名はuseEffectと同じですが、すべてのDOM変更後にeffectが同期して呼び出されます.これを使用して、DOMレイアウトを読み込み、再レンダリングのトリガを同期できます.ブラウザがペイントを実行する前に、useLayoutEffect内部の更新計画が同期してリフレッシュされ、できるだけ標準のuseEffectを使用して視覚更新がブロックされないようにします.
簡単に言えば、useEffectは非同期であり、useLayoutEffectは同期であり、異(同)ステップはブラウザがスクリーンTaskをリフレッシュすることに対して実行される.
実際には、Reactが16.13.1バージョンであり、まずサンプルコードである簡単なdemo例によって具体的な実行手順を説明する.
import React, { useState, useEffect, useLayoutEffect } from ‘react’;
const EffectDemo = () => { const [count, setCount] = useState(0); useEffect(function useEffectDemo() { console.log(‘useEffect:’, count); }, [count]); useLayoutEffect(function useLayoutEffectDemo() { console.log(‘useLayoutEffect:’, count); }, [count]); return ( onClick={() => { setCount(count + 1); }} >click me ); };
export default EffectDemo;
機能は簡単で、インタフェースの展示をしないで、ここでは主にブラウザコンソールPerformanceの監視図を見てみましょう:画像の説明は2つのhooksの実行図を通じて見ることができて、useLayoutEffectはページがスクリーン(ユーザーが見える)までレンダリングする前に発生して、useEffectはその後に発生して、中間はDCL、FCP、FMP、LCPの段階を経験して、DCL(DomContentLoaded)を除いて、これらの指標はRAILモデルがページの性能を測定する基準であり、総じて言えば、スクリーンにレンダリングされる段階は分水嶺である.レンダリングには何が含まれているのか、それとも図を見てみましょう.画像はこの段階でスタイルの計算(Recalculate Style)とレイアウト(Layout)が完了したことを説明し、続いてTaskであり、Update Layer Tree、Paint、Composite Layersを完了し、この一連のタスクを経て、ページは最終的にユーザーに表示され、ブラウザのレンダリングプロセスを1枚の図で表すことができます.画像の説明の後に関連する学習資料があります.ここでは詳しく説明しません.
シミュレーション実行例Reactの実行を深く理解する前に、まずローカルに簡単な例を書いて、文章の最初の例をほぼシミュレーションします.
次に、Performanceモニタレンダリングを有効にします.画像の説明
まとめてみると:1.まずrenderを実行し、完了したらすぐにuseLayoutEffectDemo関数を実行します(DOMは挿入されていますが、インタフェースはレンダリングされていません).2.非同期コールバック関数useEffectDemoを登録し、0 ms後にEventLoopのマクロタスクキューに追加する.3.ページレンダリング開始:Recolculate Style->Layout->Update Layer Tree->Paint->Composite Layers->GPU描画;4.マクロタスクuseEffectDemoを取り出し、コールバックを実行する.
Reactの実行はこのシミュレーションの例よりずっと複雑であるが,抽象化されたプロセスノードは大きく異なり,理解した後,Reactの実行メカニズムを深く掘り下げることができるようになった.
Reactの動作原理Reactレンダリングページは2つの段階に分けられます:1.スケジューリングフェーズ(reconciliation):更新するノード要素を見つけます.レンダーフェーズ(commit):更新する要素をDOMに挿入します.次に、Reactの実行プロセスに従って、異なるフェーズの実行状況を具体的に見ます.
レンダリングフローチャート(初回レンダリング)ピクチャの説明
簡単にまとめると:1.react-domはFiberノードの作成を担当し、最終的にFiberノードツリーを形成し、各Fiberには実行する必要がある副作用と画面にレンダリングするDOMオブジェクトが含まれている.2.scheduler暴露を呼び出す方法スケジューリングが必要なイベントを登録する.3.DOM挿入を実行する;4.useLyaoutEffectまたはClassComponentのライフサイクル関数を実行します.5.ブラウザは制御権を受け取り、レンダリングを実行する.6.schedulerはスケジューリングタスクを実行し、useEffectDemoを実行する.
以上が全体の流れです.次に、useEffectとuseLayoutEffectがどのように解析され、実行されているかを見てみましょう.画像の説明です.
use(Layout)Effect解析と実行1.解析画像の説明上の図から分かるように、uesEffectとuseLayoutEffectは最終的にmountEffectImpl関数を呼び出し、FiberのupdateQueueを初期化/更新し、mountEffectImpl関数がどのようなものかを見ることができます.
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber$1.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps); } みんな知っていますが、何をしているのか分かりません.いいでしょう.やはり図で説明しましょう.
画像はこの関数の機能を説明します:1.hookオブジェクトを作成し、workInProgressHookチェーンテーブルに挿入します.2.FiberのupdateQueueは、前のステップで作成したhookに関連付けられており、各FiberオブジェクトでEffectが実行されることがわかります.
ではworkInProgressHookは何をしているのでしょうか.ソースコードの説明を見てみましょう.
var workInProgressHook = null;//Whether an update was scheduled at any point during the render phase. This//does not get reset if we do another render pass; only when we’re completely//finished evaluating this component. This is an optimization so we know//whether we need to clear render phase updates after a throw. 2.updateQueueデータ構造updateQueueといえば、最終的に私たちが書いたuseEffectDemoとuseLayoutEffectDemoはここに置かれています.では、どのようにして構造が格納されているのでしょうか.印刷してみてください.画像の説明は実は終わりにつながっているリング構造です.なぜこのように設計されているのでしょうか.commitHookEffectListMount実行関数の遍歴を見てみましょう.
function commitHookEffectListMount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect;
}ここではeffectのtagによってどのeffectを実行するかが決定されます.ここで私たちのuseEffectDemoとuseLayoutEfectDemoのtagはそれぞれ5と3なので、useEffectの副作用関数を実行する必要がある場合、commitHookEffectListMountのtagは5に違いありません.useLayoutEffectの副作用関数を実行する場合、commitHookEffectListMountのtagは3に違いありません.総じてすべてのuseEffectとuseLayoutEffectの副作用関数はここで実行され,tagによって実行タイミングを制御する.
3.実行実際にはcommitHookEffectListMountの実行について述べたが、ここでは具体的な実行手順:画像の説明を参照する.
useEffectのエントリを実行するには、次の手順に従います.
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork); return; } … }
useLayoutEffectのエントリを実行します.
function commitPassiveHookEffects(finishedWork) { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { … commitHookEffectListMount(Passive$1 | HasEffect, finishedWork); break; } } } } 2つの実行エントリから入力される最初のパラメータtagが異なることがわかり,最終的に実行される副作用関数が区別される.
MessageChannel非同期スケジューリングでは、useEffectとuseLayoutEffectの実行について大まかに理解する必要があります.では、scheduler非同期スケジューリングに関する小さな問題もあります.本稿の最初のシミュレーションの例ではsettimeoutで完了しています.ReactではMessageChannelで実現されています.使用方法に慣れていない場合は、調べてみてください.ここでは、非同期で実行されるプロセスについて説明します.
ブラウザレンダリングの流れブラウザのレンダリングについてここでは、私自身もこれらの説明が上手ではないので、繰り返す必要はありません.基本知識ブラウザのレンダリングは非常に複雑なプロセスです.よく知らない場合は、Googleが提供している紹介記事を参照してください.リンクは以下の通りです.https://developers.google.cn/web/fundamentals/performance/rendering
ブラウザの基本的なレンダリングを深く理解すると、ブラウザの動作をより深く垣間見ることができます.まず、前の図:画像の説明の上の図はhttps://aerotwist.com/blog/the-anatomy-of-a-frameブラウザのレンダリングについて説明する記事もお勧めします.https://juejin.im/entry/6844903476506394638
他のライフサイクル関数はHooksを学習するとき、classコンポーネントのライフサイクルと比較するのは避けられません.ここではuseEffectだけに注目します.useEffectは、ブラウザのレンダリングをブロックするため、componentDidMount、componentDidUpdate、componentWillUnmountの3つのフック関数の集合に相当します.componentDidMount、componentDidUpdateの実行はどこにあるのか、上記のcommitLifeCycles関数を見てみるとわかります(componentWillUnmount皆さんは興味があるので自分で探してみましょう).
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork);
公式解釈useLayoutEffectその関数署名はuseEffectと同じですが、すべてのDOM変更後にeffectが同期して呼び出されます.これを使用して、DOMレイアウトを読み込み、再レンダリングのトリガを同期できます.ブラウザがペイントを実行する前に、useLayoutEffect内部の更新計画が同期してリフレッシュされ、できるだけ標準のuseEffectを使用して視覚更新がブロックされないようにします.
簡単に言えば、useEffectは非同期であり、useLayoutEffectは同期であり、異(同)ステップはブラウザがスクリーンTaskをリフレッシュすることに対して実行される.
実際には、Reactが16.13.1バージョンであり、まずサンプルコードである簡単なdemo例によって具体的な実行手順を説明する.
import React, { useState, useEffect, useLayoutEffect } from ‘react’;
const EffectDemo = () => { const [count, setCount] = useState(0); useEffect(function useEffectDemo() { console.log(‘useEffect:’, count); }, [count]); useLayoutEffect(function useLayoutEffectDemo() { console.log(‘useLayoutEffect:’, count); }, [count]); return ( onClick={() => { setCount(count + 1); }} >click me ); };
export default EffectDemo;
機能は簡単で、インタフェースの展示をしないで、ここでは主にブラウザコンソールPerformanceの監視図を見てみましょう:画像の説明は2つのhooksの実行図を通じて見ることができて、useLayoutEffectはページがスクリーン(ユーザーが見える)までレンダリングする前に発生して、useEffectはその後に発生して、中間はDCL、FCP、FMP、LCPの段階を経験して、DCL(DomContentLoaded)を除いて、これらの指標はRAILモデルがページの性能を測定する基準であり、総じて言えば、スクリーンにレンダリングされる段階は分水嶺である.レンダリングには何が含まれているのか、それとも図を見てみましょう.画像はこの段階でスタイルの計算(Recalculate Style)とレイアウト(Layout)が完了したことを説明し、続いてTaskであり、Update Layer Tree、Paint、Composite Layersを完了し、この一連のタスクを経て、ページは最終的にユーザーに表示され、ブラウザのレンダリングプロセスを1枚の図で表すことができます.画像の説明の後に関連する学習資料があります.ここでは詳しく説明しません.
シミュレーション実行例Reactの実行を深く理解する前に、まずローカルに簡単な例を書いて、文章の最初の例をほぼシミュレーションします.
次に、Performanceモニタレンダリングを有効にします.画像の説明
まとめてみると:1.まずrenderを実行し、完了したらすぐにuseLayoutEffectDemo関数を実行します(DOMは挿入されていますが、インタフェースはレンダリングされていません).2.非同期コールバック関数useEffectDemoを登録し、0 ms後にEventLoopのマクロタスクキューに追加する.3.ページレンダリング開始:Recolculate Style->Layout->Update Layer Tree->Paint->Composite Layers->GPU描画;4.マクロタスクuseEffectDemoを取り出し、コールバックを実行する.
Reactの実行はこのシミュレーションの例よりずっと複雑であるが,抽象化されたプロセスノードは大きく異なり,理解した後,Reactの実行メカニズムを深く掘り下げることができるようになった.
Reactの動作原理Reactレンダリングページは2つの段階に分けられます:1.スケジューリングフェーズ(reconciliation):更新するノード要素を見つけます.レンダーフェーズ(commit):更新する要素をDOMに挿入します.次に、Reactの実行プロセスに従って、異なるフェーズの実行状況を具体的に見ます.
レンダリングフローチャート(初回レンダリング)ピクチャの説明
簡単にまとめると:1.react-domはFiberノードの作成を担当し、最終的にFiberノードツリーを形成し、各Fiberには実行する必要がある副作用と画面にレンダリングするDOMオブジェクトが含まれている.2.scheduler暴露を呼び出す方法スケジューリングが必要なイベントを登録する.3.DOM挿入を実行する;4.useLyaoutEffectまたはClassComponentのライフサイクル関数を実行します.5.ブラウザは制御権を受け取り、レンダリングを実行する.6.schedulerはスケジューリングタスクを実行し、useEffectDemoを実行する.
以上が全体の流れです.次に、useEffectとuseLayoutEffectがどのように解析され、実行されているかを見てみましょう.画像の説明です.
use(Layout)Effect解析と実行1.解析画像の説明上の図から分かるように、uesEffectとuseLayoutEffectは最終的にmountEffectImpl関数を呼び出し、FiberのupdateQueueを初期化/更新し、mountEffectImpl関数がどのようなものかを見ることができます.
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber$1.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps); } みんな知っていますが、何をしているのか分かりません.いいでしょう.やはり図で説明しましょう.
画像はこの関数の機能を説明します:1.hookオブジェクトを作成し、workInProgressHookチェーンテーブルに挿入します.2.FiberのupdateQueueは、前のステップで作成したhookに関連付けられており、各FiberオブジェクトでEffectが実行されることがわかります.
ではworkInProgressHookは何をしているのでしょうか.ソースコードの説明を見てみましょう.
var workInProgressHook = null;//Whether an update was scheduled at any point during the render phase. This//does not get reset if we do another render pass; only when we’re completely//finished evaluating this component. This is an optimization so we know//whether we need to clear render phase updates after a throw. 2.updateQueueデータ構造updateQueueといえば、最終的に私たちが書いたuseEffectDemoとuseLayoutEffectDemoはここに置かれています.では、どのようにして構造が格納されているのでしょうか.印刷してみてください.画像の説明は実は終わりにつながっているリング構造です.なぜこのように設計されているのでしょうか.commitHookEffectListMount実行関数の遍歴を見てみましょう.
function commitHookEffectListMount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Mount
var create = effect.create;
effect.destroy = create();
{
var destroy = effect.destroy;
if (destroy !== undefined && typeof destroy !== 'function') {
var addendum = void 0;
if (destroy === null) {
addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
} else if (typeof destroy.then === 'function') {
addendum = '
It looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:
' + 'useEffect(() => {
' + ' async function fetchData() {
' + ' // You can await here
' + ' const response = await MyAPI.getData(someId);
' + ' // ...
' + ' }
' + ' fetchData();
' + "}, [someId]); // Or [] if effect doesn't need props or state
" + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching';
} else {
addendum = ' You returned: ' + destroy;
}
error('An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s%s', addendum, getStackByFiberInDevAndProd(finishedWork));
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}ここではeffectのtagによってどのeffectを実行するかが決定されます.ここで私たちのuseEffectDemoとuseLayoutEfectDemoのtagはそれぞれ5と3なので、useEffectの副作用関数を実行する必要がある場合、commitHookEffectListMountのtagは5に違いありません.useLayoutEffectの副作用関数を実行する場合、commitHookEffectListMountのtagは3に違いありません.総じてすべてのuseEffectとuseLayoutEffectの副作用関数はここで実行され,tagによって実行タイミングを制御する.
3.実行実際にはcommitHookEffectListMountの実行について述べたが、ここでは具体的な実行手順:画像の説明を参照する.
useEffectのエントリを実行するには、次の手順に従います.
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork); return; } … }
useLayoutEffectのエントリを実行します.
function commitPassiveHookEffects(finishedWork) { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { … commitHookEffectListMount(Passive$1 | HasEffect, finishedWork); break; } } } } 2つの実行エントリから入力される最初のパラメータtagが異なることがわかり,最終的に実行される副作用関数が区別される.
MessageChannel非同期スケジューリングでは、useEffectとuseLayoutEffectの実行について大まかに理解する必要があります.では、scheduler非同期スケジューリングに関する小さな問題もあります.本稿の最初のシミュレーションの例ではsettimeoutで完了しています.ReactではMessageChannelで実現されています.使用方法に慣れていない場合は、調べてみてください.ここでは、非同期で実行されるプロセスについて説明します.
ブラウザレンダリングの流れブラウザのレンダリングについてここでは、私自身もこれらの説明が上手ではないので、繰り返す必要はありません.基本知識ブラウザのレンダリングは非常に複雑なプロセスです.よく知らない場合は、Googleが提供している紹介記事を参照してください.リンクは以下の通りです.https://developers.google.cn/web/fundamentals/performance/rendering
ブラウザの基本的なレンダリングを深く理解すると、ブラウザの動作をより深く垣間見ることができます.まず、前の図:画像の説明の上の図はhttps://aerotwist.com/blog/the-anatomy-of-a-frameブラウザのレンダリングについて説明する記事もお勧めします.https://juejin.im/entry/6844903476506394638
他のライフサイクル関数はHooksを学習するとき、classコンポーネントのライフサイクルと比較するのは避けられません.ここではuseEffectだけに注目します.useEffectは、ブラウザのレンダリングをブロックするため、componentDidMount、componentDidUpdate、componentWillUnmountの3つのフック関数の集合に相当します.componentDidMount、componentDidUpdateの実行はどこにあるのか、上記のcommitLifeCycles関数を見てみるとわかります(componentWillUnmount皆さんは興味があるので自分で探してみましょう).
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork);
return;
}
case ClassComponent:
{
var instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) { //
......
instance.componentDidMount();
stopPhaseTimer();
} else { //
......
instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
stopPhaseTimer();
}
}