序文
あなたがuseEffectを感じる場合は、ソースコードの解釈は、最後にフックを何をすべきかを把握する助長している非常に「魔法」、この資料では、いくつかのを助けるかもしれません。
このブログのスペースが限られている、一見useEffect、シンプルかつ簡潔、見てあなたを取る深いフックに反応
あなたがソースコードを見つけたいものに関連したフック(あなたが直接ジャンプすることができます)
まず、Githubのが反応からソースコードを取得し、その後、フォルダがindex.jsが私たちの入り口であるパッケージで見つけることができます反応します。
コードは、2つのラインで、簡単です:
const React = require('./src/React');
module.exports = React.default || React;
それでは、行くと見てみましょう「反応/ SRC /リアクト」、もう少しコードが、我々はそれを簡素化:
import ReactVersion from 'shared/ReactVersion';
// ...
import {
useEffect,
} from './ReactHooks';
const React = {
useEffect
};
//...
export default React;
参照のフックがある理由まあ、少なくとも今は知っています:
「反応」からインポート{useEffect}
次に、私たちは「反応/ SRC / ReactHooks」を参照し続けます。
ReactHooksファイル(あなたが直接ジャンプすることができます)
よく見てuseEffect、コードを簡素化するため、同じ必要性を言って前に。
そして、何人かの人々は、活字体文法、構文も削除活字に慣れていないことを考えると、それはケースの後にコードを単純化します。
今、私たちはコードを簡素化するために見て:
import invariant from 'shared/invariant';
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// React版本不对或者Hook使用有误什么的就报错
// ...
return dispatcher;
}
export function useEffect(create,inputs) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, inputs);
}
ここでは、実際に私たちのuseEffect ReactCurrentDispatcher.current.useEffect、見ることができます。
ReactCurrentDispatcherファイル(あなたが直接ジャンプすることができます)
ReactCurrentDispatcherファイルを見て、何の簡素化はありません。
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
彼はので、ここで、現在のタイプがnullまたはディスパッチャであることが判明し、我々はそれが、この事のソースは「/ SRC / ReactFiberHooks-競合回避を反応させる」ことを推測するのは簡単ですしています。
ReactFiberHooksファイル
コードの行数千人、大型ヘッド。しかし、Moのパニック、我々はそれの原則を確認するために、反応するように書いていません。
我々はすでに、実際にReactCurrentDispatcher.current.useState前useStateを知っています。
明らかにReactCurrentDispatcher.currentどんなにそれが個別にリストされているものを、私たちはラインで彼に割り当てられているかを知る必要があります。
コードを合理化した後、__DEV__の開発コードで区別を削除し、私たちは、ファイル全体が数ReactCurrentDispatcher.currentに割り当てられていることがわかります。
決意の唯一の例外は、独立しているrenderWithHooksのコードブロックの機能:
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderExpirationTime
){
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
return children;
}
私たちは、このコードがやっているかわからないが、コンポーネントをレンダリングするとき、彼は確かに使用します。
そして、ここではReactCurrentDispatcher.current値のみHooksDispatcherOnMountとHooksDispatcherOnUpdateできることは明らかです。
もちろん、更新のためのロードと1時間の両方。
その後、我々はいくつかの関連するコードを発見しました。
const HooksDispatcherOnMount = {
useEffect: mountEffect
};
const HooksDispatcherOnUpdate = {
useEffect: updateEffect
};
アセンブリがロードされている言い換えれば、useEffectは、mountEffectを呼び出してコンポーネントを更新する際updateEffectを呼び出します。
私たちは、これらの二つの機能を見ていきましょう:
function mountEffect(create, deps) {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
function updateEffect(create, deps) {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
ここでUpdateEffect定数とPassiveEffectは、バイナリ、ビット操作の操作モードです。
まず、あなたは、特定の意味を知らない一定であることを知っています。
私たちは、特定のmountEffectImplを見て次へ:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
const hook = mountWorkInProgressHook();
// useEffect不传依赖,那么就为null
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.effectTag |= fiberEffectTag;
// 链表尾部hook对象的memoizedState为pushEffect的返回值
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
私たちは、コードの最初の行がmountWorkInProgressHookで見てみましょう、mountWorkInProgressHookがフックオブジェクトを構築して呼び出して参照してください。
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
workInProgressHookリストは、ヌルオブジェクトが新しいフックにそれを割り当てられ、そうでない場合はnull、その後、リストの末尾に追加されますされている場合明らかに、ここで、リスト構造workInProgressHookです。
ここでは、説明する必要があります。
memoizedState繊維のリンクリストとして保存フック。
currentHook現在の繊維は、リンクリスト。
workInProgressHookは、リストの作業中の繊維に追加されようとしています。
その後、我々はpushEffectを見て:
function pushEffect(tag, create, destroy, deps) {
// 新建一个effect,很明显又是个链表结构
const effect = {
tag,
create,
destroy,
deps,
// Circular
next: null,
};
// 从currentlyRenderingFiber.updateQueue获取组件更新队列
let componentUpdateQueue= currentlyRenderingFiber.updateQueue;
// 判断组件更新队列是否为空,每次在调用renderWithHooks都会将这个componentUpdateQueue置为null
// 这样的话每次update这个组件时,就会创建一个新的effect链表
if (componentUpdateQueue === null) {
// 为空就创建一个组件更新队列
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 并赋值给currentlyRenderingFiber.updateQueue
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 组件更新队列最新的effect为我们新建的effect
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 如果组件更新队列已经存在,获取它最新的Effect
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
// 如果最新的Effect为null,那么组件更新队列最新的Effect为我们新建的effect
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 否则将我们的effect加入到链表结构中最末尾,然后他的next为链表结构的第一个effect
// 这里的effect链表是个闭环
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
私たちはupdateEffectImplを見てみましょうするとき、更新と呼ばれます:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 这里 updateWorkInProgressHook
// workInProgressHook = workInProgressHook.next;
// currentHook = currentHook.next;
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对比两个依赖数组的各个值之间是否有变动,如果没变动,那么就设置标志位为NoHookEffect
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.effectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
最も重要なことは、我々が一緒に文字列に二つの機能を持っている、のように我々は、彼らが最終的に達成何を見てupdateEffectImplとmountEffectImplを見ることができます。
データ構造図フック
ここで私は理解するのに役立つ図を描きます:
この構成では、一度にコンポーネントの構成図です。
図黄色の光ファイバノード、ノードフック緑、青のエフェクトノード。
ファイバノードは、実際には、私たちの仮想DOMノードは、ファイバノードツリーを生成します反応し、各構成要素は、対応する光ファイバのファイバツリーノードを持っています。
レンダリングされた当社の継続的なを表しCurrentlyRenderingFiberノード、それはworkInProgress、すでにレンダリングを表し、現在のノードから来ています。
コンポーネントがロードされ、実行は各useEffect、それはリストのフックを作成し、リストのノードフックフック尾にmemoizedState視野点のworkInProgressますでしょう、。
各ノードを構成しながら、フックは、ノードは、ノードの効果に対応するノードに、同様memoizedStateフック視野点に影響を構築します。
効果ノードと各ターンでは、尾updateQueue作用効果ノードリストに、リンクされたリストを形成するために、次にworkInProgress視野点に加わりました。
コンポーネントの更新は、配列は、それがeffectTag NoHookEffectノードエフェクトを設定した場合と同様currentHookを順次、新たな依存性効果アレイの相違点を比較依存するであろう。
しかし、関係なく、価値のかどうか、配列に依存して変化し、memoizedStateフックノードのフィールド値として、新しいノードの影響を構築します。
そして、レンダリングする準備ができて時間を、それが直接テール・ノードエフェクトエフェクトリストに、あるファイバノードのlastEffect updateQueueに直接移動します。
因为effect链表是闭环的,这里通过lastEffect的next找到第一个Effect。
然后循环遍历effect链表,当effectTag为NoHookEffect则不做操作,否则会去先执行effect的destroy操作,然后再执行create操作。
对,你没看错,总结起来就是每次更新后,只要依赖项改变,那么就会执行useEffect的卸载函数,再执行第一个参数create函数。
这一部分代码比较远:
function commitHookEffectList(
unmountTag,
mountTag,
finishedWork,
) {
const updateQueue = finishedWork.updateQueue;
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
这里的位运算大家可能有点看不懂,因为NoHookEffect的值是0,所以只要effect.tag被设置为NoHookEffect,那么
effect.tag & unmountTag
就必然为NoHookEffect。
我们还记得,我们之前的玩法,依赖数组各个值不变时,就设置Effect节点的effectTag为NoHookEffect。
此时是绝对不会执行先destroy Effect节点,再执行Effect函数create的操作。
而如果effect.tag的值不为NoHookEffect,那也得需要effect.tag与unmountTag至少有一个位相同才能执行destroy。
让我们看看之前无论是mountEffectImpl还是updateEffectImpl都默认传的是:UnmountPassive | MountPassive,也就是说effect.tag为UnmountPassive | MountPassive。
而很明显这个设计的目的在于,当mountTag为MountPassive时执行create函数,而unmountTag为UnmountPassive时创建执行destroy函数。
而只有下面这个地方会做这个Passive操作:
export function commitPassiveHookEffects(finishedWork: Fiber): void {
if ((finishedWork.effectTag & Passive) !== NoEffect) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Chunk: {
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
break;
}
default:
break;
}
}
}
这里的意思很明显,先遍历一遍effect链表,每个依赖项变了的hook都destroy一下,然后再遍历一遍effect链表,每个依赖项变了的,都执行create函数一下。
也就是说每次都会按照useEffect的调用顺序,先执行所有useEffect的卸载函数,再执行所有useEffect的create函数。
而commitPassiveHookEffects又是只有flushPassiveEffects这个函数最终能调用到。
而每次 React 在检测到数据变化时,flushPassiveEffects就会执行。
不论是props还是state的变化都会如此。
所以如果您真的有需要去模拟一个像之前的componentDidMount和componentWillUnmount的生命周期,那么最好用上一个单独的Effect:
useEffect(()=>{
// 加载时的逻辑
return ()=>{
// 卸载时的逻辑
}
},[])
这里用[]作为依赖数组,是因为这样依赖就不会变动,也就是只在加载时执行一次加载逻辑,卸载时执行一次卸载逻辑。
不加依赖数组时,那么每次渲染都会执行一次加载和卸载。
总结
希望我的这篇文章让您有一些收获。
这里主要想说一下自己在源码阅读中的感想。
读这部分的源码,其实更像是在盲人摸象。
为了追求效率,需要避开一些比较复杂的东西,比如我们提到的Fiber节点树,又比如其实useEffect替换effect和具体执行effect并不是同步的。
否则解读很多东西要花很多时间,怕是真的要写一个系列了。
源码阅读略显艰涩,还好关键的地方都是有注释的,在阅读的过程中也能收获不少东西。
我并不是react的开发者,如果在解读的过程中有什么疏漏和误解,还望诸位批评指正。