Reactの内部実装を見る(1. mountState/updateStateにたどり着くまで)
useStateの中身らしい、mountState/updateState にたどり着くまでをまとめる。
そこから先の一番気になるところは別スクラップでやっていく予定。
まずこれ見る
メモ
-
if (__DEV__)
で開発環境かどうか見てるっぽい-
__DEV__
の定義をぜひ見てみたいところ(本記事では触れられていない)
-
- 初回レンダー時≒mount時 のみしか触れられていないのでupdateの方も見たい
-
.current
が状態保持に結構使われてる- useRefのref.currentと同じだよな、多分
- どうやってDOMのrefを取得してるんだろう(=useRefの内部実装が気になる)
- useRefのref.currentと同じだよな、多分
- useStateの引数にinitializer functionが渡された場合はそれを実行するだけでいいのか。確かに
if (typeof initialState === 'function') {
initialState = initialState();
}
- これはシンプルに知らなかったんだけど、
=
を同じ行で2つ使えるらしい。
// 以下1行は
hook.memoizedState = hook.baseState = initialState;
// 以下2行と同じ
hook.memoizedState = initialState;
hook.baseState = initialState;
- ここから全部のフックをどんどん辿っていくのが良さそう
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
- このqueueどうなってんのか気になる
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
-
至る所で出てくるDispatcherっていう型は何なんだ
-
dispatchの定義がすごくわかりづらいので言語化してみる
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
dispatchSetStateにcurrentlyRenderingFiber引数とqueue引数をbindつまり固定したものをqueue.dispatchに代入して、そのqueue.dispatchを引数に取る。で返り値はany。っていう関数がdispatchである。
んでそのdispatch関数自体の型はDispatch<BasicStateAction<S>,>である。
bindが複雑だが、例えばqueue.dispatch(arg1, arg2)と呼び出した場合、内部的にはdispatchSetState(null, currentlyRenderingFiber, queue, arg1, arg2)と呼び出されるようになってるみたいな感じっぽい。事前に固定した引数を受け取る関数を新しい関数として作れるみたいなのがbind。
なのでqueue.dispatch()と呼び出したらdispatchSetState(null, currentlyRenderingFiber, queue)を呼び出したのと同じことになって再利用性が上がって楽になる。
bindの理解がそれだと雑すぎることにこれ読んで気付いた
bindは引数を省略しておけるとかそういう話ではなくthisの参照先を変えられるという認識の方が良さそう(省略もできてるのか?)
- null / undefined にするとthisの参照先は
window
になる - bind()は関数の実行ではなく新しい関数を生成して返す
- 即実行できちゃうのがcall()で、ちょっとbind()と似てる
- 定義時は第一引数のthisだけ書いといて、実行するついでに第二引数以降を呼び出すのもok
bind()の部分は以下のように、第2引数以降を実行するときに書くこともできます。
var flyCar = airplane.fly.bind(car);
flyCar('2015年10月21日', '慌ただしく');
ふむふむ
bind() 関数は新しい「バインド済み関数 (bound function)」を生成します。
引数省略の機能もちゃんとありそう
バインド済み関数は、渡された引数、すなわち this の値と最初のいくつかの引数を内部の状態として格納します。これらの値は、呼び出し時に渡されるのではなく、あらかじめ格納されています。
お、やっぱり省略の考え方は合ってた。なるほど
一般に、
const boundFn = fn.bind(thisArg, arg1, arg2)
は、const boundFn = (...restArgs) => fn.call(thisArg, arg1, arg2, ...restArgs)
と呼ばれるのと同じだと考えてよいでしょう
記事で、
ほとんど何も書かれていませんでした。スカされ続けています。
どうやらReactCurrentDispatcher.currentに実質的な値を格納している箇所が他でありそうです。
と書かれていたように、「定義は見れたけどそこに実質的な値を代入してるコードがどこかわからん」になったら記事を見て情報収集すれば良さそうなので、一旦自分でどんどんソースコード追ってみるか
(いやわからなくてもその変数名検索すれば普通に探せるか?...まぁいいや)
とりあえずリポジトリcloneしてVSCodeで見よう
git clone https://github.com/facebook/react.git
まずReactHook.jsで出てきたresolveDispatcher関数内の__DEV__
の中身を見てみよう
ん?ReactHook.jsというファイルが無いぞ
→ReactHooks.jsを発見した。変わったのかな?それともミスか
react\packages\react\src\ReactHooks.js
(ちなみにこれ以降はC:\Users\
などは省略してreact\
からパスを書き始めることにする)
さて、resolveDispatcher関数を見てみるとこうなっている
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
ん、1行変わってるな
- const dispatcher = ReactCurrentDispatcher.current; // これが
+ const dispatcher = ReactSharedInternals.H; // これになってる
ReactCurrentDispatcherはすでに記事内で説明されてるので、ReactSharedInternalsを見てみるか
同ファイルに
import ReactSharedInternals from 'shared/ReactSharedInternals';
とあるのでVSCodeのGo to Definitionで定義元に飛んでみようと思ったら飛べないようになってるな、めんどくさい
おそらくビルドツール(Webpack、Rollup、Babelなど)でパスエイリアスが設定されているためだと思うが、そもそも複数のビルドツール使ってるぽいし、かつ至るところ(階層)に構成ファイルがあってどれが共通パスを設定しているのか分からなかったので、普通にshared/ReactSharedInternalsで検索して探す。
見つけた。
今のファイルは
react\packages\react\src\ReactHooks.js
で、相対パスとしてshared/ReactSharedInternals
がimportされてる訳だが、お目当てのファイルは
react\packages\shared\ReactSharedInternals.js
にあった。
なのでreact\packages
が共通パスで設定されているのだろう(多分)
んで、中身はこんな感じだった
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;
前見たXの投稿のやつに似てるな
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
っていう面白い名前のやつ
わぉ、それらは同じものだった。つい2週間前くらいに名前が変わったらしい。
React 19 RC(Release Candidate)という、「19にアップグレードするためにいろいろちょっとずつ変えていきまっせ」的なやつがあるっぽく、その途中経過としてのReact 18.3の公開によるものらしい。
なので「React19になったので変わった」のではなく「React19に向かっていろいろ変更している途中であるバージョン18、の18.3になったので変わった」みたいな感じだと思われる。
気になる点
-
React.○○
ってソースコード読めないのかな?-
React
自体の中身ってどうなってる?
-
- 内部ってそもそも何?
- 内部にアクセスするというのはどういうこと?
- なぜ内部にアクセスしてはいけない?
- 内部アクセスするとユーザーはアップグレードできないってどゆこと?
- アップグレードとは?
- 逆に、内部アクセスしなければアップグレードできるってこと?
import * as React from 'react'
の'react'
の定義に飛んでみる
すると
C:\Users\ユーザー名\node_modules\react\index.js
に飛んだ。ということはReactリポジトリとは関係なく、ローカルに入っているものだな。
以前npm install -g ○○
でreactなどをグローバルインストールしたときにできたものだろう。
中身は以下。
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
本番環境の方はこれ
おそらくただ開発環境の方をビルド(バンドル?)してるだけなので飛ばす
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';var l=Symbol.for("react.element"),n=Symbol.for("react.portal"),p=Symbol.for("react.fragment"),q=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),t=Symbol.for("react.provider"),u=Symbol.for("react.context"),v=Symbol.for("react.forward_ref"),w=Symbol.for("react.suspense"),x=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),z=Symbol.iterator;function A(a){if(null===a||"object"!==typeof a)return null;a=z&&a[z]||a["@@iterator"];return"function"===typeof a?a:null}
var B={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,D={};function E(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B}E.prototype.isReactComponent={};
E.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState")};E.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};function F(){}F.prototype=E.prototype;function G(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B}var H=G.prototype=new F;
H.constructor=G;C(H,E.prototype);H.isPureReactComponent=!0;var I=Array.isArray,J=Object.prototype.hasOwnProperty,K={current:null},L={key:!0,ref:!0,__self:!0,__source:!0};
function M(a,b,e){var d,c={},k=null,h=null;if(null!=b)for(d in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(k=""+b.key),b)J.call(b,d)&&!L.hasOwnProperty(d)&&(c[d]=b[d]);var g=arguments.length-2;if(1===g)c.children=e;else if(1<g){for(var f=Array(g),m=0;m<g;m++)f[m]=arguments[m+2];c.children=f}if(a&&a.defaultProps)for(d in g=a.defaultProps,g)void 0===c[d]&&(c[d]=g[d]);return{$$typeof:l,type:a,key:k,ref:h,props:c,_owner:K.current}}
function N(a,b){return{$$typeof:l,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function O(a){return"object"===typeof a&&null!==a&&a.$$typeof===l}function escape(a){var b={"=":"=0",":":"=2"};return"$"+a.replace(/[=:]/g,function(a){return b[a]})}var P=/\/+/g;function Q(a,b){return"object"===typeof a&&null!==a&&null!=a.key?escape(""+a.key):b.toString(36)}
function R(a,b,e,d,c){var k=typeof a;if("undefined"===k||"boolean"===k)a=null;var h=!1;if(null===a)h=!0;else switch(k){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case l:case n:h=!0}}if(h)return h=a,c=c(h),a=""===d?"."+Q(h,0):d,I(c)?(e="",null!=a&&(e=a.replace(P,"$&/")+"/"),R(c,b,e,"",function(a){return a})):null!=c&&(O(c)&&(c=N(c,e+(!c.key||h&&h.key===c.key?"":(""+c.key).replace(P,"$&/")+"/")+a)),b.push(c)),1;h=0;d=""===d?".":d+":";if(I(a))for(var g=0;g<a.length;g++){k=
a[g];var f=d+Q(k,g);h+=R(k,b,e,f,c)}else if(f=A(a),"function"===typeof f)for(a=f.call(a),g=0;!(k=a.next()).done;)k=k.value,f=d+Q(k,g++),h+=R(k,b,e,f,c);else if("object"===k)throw b=String(a),Error("Objects are not valid as a React child (found: "+("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}
function S(a,b,e){if(null==a)return a;var d=[],c=0;R(a,d,"","",function(a){return b.call(e,a,c++)});return d}function T(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b});-1===a._status&&(a._status=0,a._result=b)}if(1===a._status)return a._result.default;throw a._result;}
var U={current:null},V={transition:null},W={ReactCurrentDispatcher:U,ReactCurrentBatchConfig:V,ReactCurrentOwner:K};exports.Children={map:S,forEach:function(a,b,e){S(a,function(){b.apply(this,arguments)},e)},count:function(a){var b=0;S(a,function(){b++});return b},toArray:function(a){return S(a,function(a){return a})||[]},only:function(a){if(!O(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};exports.Component=E;exports.Fragment=p;
exports.Profiler=r;exports.PureComponent=G;exports.StrictMode=q;exports.Suspense=w;exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=W;
exports.cloneElement=function(a,b,e){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var d=C({},a.props),c=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=K.current);void 0!==b.key&&(c=""+b.key);if(a.type&&a.type.defaultProps)var g=a.type.defaultProps;for(f in b)J.call(b,f)&&!L.hasOwnProperty(f)&&(d[f]=void 0===b[f]&&void 0!==g?g[f]:b[f])}var f=arguments.length-2;if(1===f)d.children=e;else if(1<f){g=Array(f);
for(var m=0;m<f;m++)g[m]=arguments[m+2];d.children=g}return{$$typeof:l,type:a.type,key:c,ref:k,props:d,_owner:h}};exports.createContext=function(a){a={$$typeof:u,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:t,_context:a};return a.Consumer=a};exports.createElement=M;exports.createFactory=function(a){var b=M.bind(null,a);b.type=a;return b};exports.createRef=function(){return{current:null}};
exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.isValidElement=O;exports.lazy=function(a){return{$$typeof:y,_payload:{_status:-1,_result:a},_init:T}};exports.memo=function(a,b){return{$$typeof:x,type:a,compare:void 0===b?null:b}};exports.startTransition=function(a){var b=V.transition;V.transition={};try{a()}finally{V.transition=b}};exports.unstable_act=function(){throw Error("act(...) is not supported in production builds of React.");};
exports.useCallback=function(a,b){return U.current.useCallback(a,b)};exports.useContext=function(a){return U.current.useContext(a)};exports.useDebugValue=function(){};exports.useDeferredValue=function(a){return U.current.useDeferredValue(a)};exports.useEffect=function(a,b){return U.current.useEffect(a,b)};exports.useId=function(){return U.current.useId()};exports.useImperativeHandle=function(a,b,e){return U.current.useImperativeHandle(a,b,e)};
exports.useInsertionEffect=function(a,b){return U.current.useInsertionEffect(a,b)};exports.useLayoutEffect=function(a,b){return U.current.useLayoutEffect(a,b)};exports.useMemo=function(a,b){return U.current.useMemo(a,b)};exports.useReducer=function(a,b,e){return U.current.useReducer(a,b,e)};exports.useRef=function(a){return U.current.useRef(a)};exports.useState=function(a){return U.current.useState(a)};exports.useSyncExternalStore=function(a,b,e){return U.current.useSyncExternalStore(a,b,e)};
exports.useTransition=function(){return U.current.useTransition()};exports.version="18.2.0";
開発環境の方は2739行あったので一部だけ抜粋。
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
function useEffect(create, deps) {
var dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
// ...
exports.Children = Children;
exports.Component = Component;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.Profiler = REACT_PROFILER_TYPE;
exports.PureComponent = PureComponent;
exports.StrictMode = REACT_STRICT_MODE_TYPE;
exports.Suspense = REACT_SUSPENSE_TYPE;
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals;
exports.cloneElement = cloneElement$1;
exports.createContext = createContext;
exports.createElement = createElement$1;
exports.createFactory = createFactory;
exports.createRef = createRef;
exports.forwardRef = forwardRef;
exports.isValidElement = isValidElement;
exports.lazy = lazy;
exports.memo = memo;
exports.startTransition = startTransition;
exports.unstable_act = act;
exports.useCallback = useCallback;
exports.useContext = useContext;
exports.useDebugValue = useDebugValue;
exports.useDeferredValue = useDeferredValue;
exports.useEffect = useEffect;
exports.useId = useId;
exports.useImperativeHandle = useImperativeHandle;
exports.useInsertionEffect = useInsertionEffect;
exports.useLayoutEffect = useLayoutEffect;
exports.useMemo = useMemo;
exports.useReducer = useReducer;
exports.useRef = useRef;
exports.useState = useState;
exports.useSyncExternalStore = useSyncExternalStore;
exports.useTransition = useTransition;
exports.version = ReactVersion;
なるほど、おそらくこのexports.○○がReact.○○となるんだろう。
なのでReactのメソッドは35行あるということになるはず
やっぱりそうだ。普通にViteでReactプロジェクト作ってあるファイルで
import React from "react";
console.log(React);
をすると以下が出た。
最後の__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
を除けばぴったり35個ある。
なぜ自分のプロジェクトではまだ__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
なんだろう?
"dependencies": {
"react": "^18.3.1",
},
名前が正式に変更される前にこのプロジェクトを自分が作成したからかも?調べてみよう
まず、変更があったコミットは多分これ。10月17日。React Nativeって書いてるけどまぁ多分同じでしょう。
(react-nativeのリポジトリは別にあるしなぁ)
で、自分がこのプロジェクトを作成したのが10月12日。やっぱりコミットされる前に自分がプロジェクト作成した方が早かったのが原因か
じゃあ今新しくViteでReactプロジェクトを作成したらもう名前は変わってると思われるので試してみよう。
npm create vite@latest
いや、変わってないな。
うーん、考えられる理由はこんな感じ?
- React Nativeでしか変更してない
- React 19 RCは「こういうことを正式版にリリースする予定だよ」というただの予告なのであって実際に変更されていない
今日React19がstableになったのでconsole.log(React)
試してみたら、ちゃんと変わってた
やはりReact 19 RCはただの予告であって実際の変更ではないという感じだったか
まぁ、とりあえず現状は把握できた。
んで__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
の中身を見れることがわかったので見よう。
多分これは__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
と同じなのではないかと思われるので見ておきたい
自分のViteプロジェクトで以下実行
import React from "react";
console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED);
(ちなみにReact.
まで書いても補完で__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
は出てこないようになってた)
結果はこれ
オブジェクトになっていて、その中に更に5つのオブジェクトがあることがわかる。
- ReactCurrentActQueue
- ReactCurrentBatchConfig
- ReactCurrentDispatcher
- ReactCurrentOwner
- ReactDebugCurrentFrame
それぞれ軽く見てみるか
1. ReactCurrentActQueue
keyが3つある。
- current
- didScheduleLegacyUpdate
- isBatchingLegacy
2. ReactCurrentBatchConfig
keyが1つだけある。
- transition
3. ReactCurrentDispatcher
keyが1つだけある。
- current
そのcurrentの中に、更に18個keyがある。
- readContext
- unstable_isNewReconciler
- useCallback
- useContext
- useDebugValue
- useDeferredValue
- useEffect
- useId
- useImperativeHandle
- useInsertionEffect
- useLayoutEffect
- useMemo
- useMutableSource
- useReducer
- useRef
- useState
- useSyncExternalStore
- useTransition
フックは中にnameというkeyがあって、その値が"throwInvalidHookError"
になってる。
4. ReactCurrentOwner
keyが1つだけある。
- current
5. ReactDebugCurrentFrame
keyが3つある。
- getCurrentStack
- getStackAddendum(関数)
- setExtraStackFrame(関数)
こんな感じ↓で、リンクになってる青い文字を押すとChrome DevToolsが定義元に勝手に飛ばしてくれるので見に行こう
- getStackAddendum関数の中身(100行目)
関数内で新しいstack変数を作成・初期化し、それにcurrentExtraStackFrameとimpl()を追加してからreturnする(条件分岐によって追加しない可能性もある)
ReactDebugCurrentFrame.getStackAddendum = function() {
var stack = "";
if (currentExtraStackFrame) {
stack += currentExtraStackFrame;
}
var impl = ReactDebugCurrentFrame.getCurrentStack;
if (impl) {
stack += impl() || "";
}
return stack;
};
- setExtraStackFrame関数の中身(94行目)
stackを受け取ってそれをcurrentExtraStackFrameに代入するだけ
ReactDebugCurrentFrame.setExtraStackFrame = function(stack) {
{
currentExtraStackFrame = stack;
}
};
どちらも同じファイル内で宣言されており、3のsetExtraStackFrame関数の方が早く宣言されていることからも、
3のsetExtraStackFrame関数でstackをcurrentExtraStackFrameに代入
→そのcurrentExtraStackFrameを2のgetStackAddendum関数内で使っている
という流れはあるっぽい。
ん-、とはいえ全く実態が掴めてないけど、まぁ覗けたことだしとりあえず__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
の深掘りはもういいかな。
resolveDispatcher関数の読解に戻る
あーでもReactSharedInternalsがわからないとReactSharedInternals.Hが分からないなぁ
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
とりあえず定義は見たいんだよなぁ。定義の箇所で実質的な値が格納されていないにしても、シンプルにどんな定義されてるのかは気になる。
このプロジェクトを開発環境で立ち上げてconsole.logするっていうのはできるか?試してみる
yarn install
yarn build
yarn test
npm run build
でまずエラーになるぞ。
Javaが入ってないから、ぽい
Process spawn error. Is java in the path?
spawn java ENOENT
Java Development Kit (JDK)をインストールせねばならんか・・・?
いや、他のbuildオプションも用意されてるからどれか使えばいけるんじゃないか?
"scripts": {
"build": "node ./scripts/rollup/build-all-release-channels.js",
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE",
"build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
"build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
}
全部試してみたけど同じエラー出た。Javaが無いぞって怒られる
「react ソースコード 実行」でググったら良さげなの見つけたのでこれ通りやってみる
yarn run flow dom-node
を実行するとエラーになるんだが(runを省略してみても同じ)
errno: -4071,
code: 'EINVAL',
syscall: 'spawn'
ちなみにyarn flowに割り当てられているコマンドはこれ
"scripts": {
"flow": "node ./scripts/tasks/flow.js",
}
つまり実質的には
node ./scripts/tasks/flow.js dom-node
を実行していることになる。
エラーの原因特定するために以下4つ理解・確認するか
- code: 'EINVAL'
- syscall: 'spawn'
- errno: -4071
- flow.jsの中身
- オプション(引数)としての
dom-node
の意味
1. code: 'EINVAL'
C, C++あたりの定数っぽい。
引数が無効です。 関数の引数のいずれかに無効な値が指定されています。 たとえば、( fseek の呼び出しによる) ファイル ポインターの配置時に原点に指定された値は、ファイルの先頭の前にあります。
fseekはC言語の関数らしい。とはいえ後半意味わからない。が、とにかく
関数の引数のいずれかに無効な値が指定されています
が重要っぽい。
code: 'EINVAL',
というのはエラーコードのcodeだな。
というかEINVALって何の略だ?
あー多分、全部Eで始まってる かつ タイトルはerrno、つまりErrorのEとInvalid ArgumentのINVALを合わせたのだろう。
2. syscall: 'spawn'
直訳:卵、産卵、産む
なるほど、nodeのメソッド(関数)か
OS のコマンドを node から実行したい場合に使われるもので、新しい子プロセスを生成してコマンドを実行するメソッドの一部だ。英語の意味合い的には、魚が産卵するときの様子を指している。
子プロセスを"産んで"、そやつにコマンド実行をさせる、て感じかな。
なので、spawnメソッドを使って実行したがその結果EINVALエラーが出たよ、という流れだと考えられる。
んで
syscall: 'spawn'
のsyscallはsystem call(システム呼び出し)の略。
じゃあsystem callとは?
システムコールとはOSが提供する機能をアプリケーションが利用する仕組み
なるほど、じゃあ今回で言うと「アプリケーションがOSの機能を利用するためにspawnメソッドを使った」みたいな感じか
3. errno: -4071
なかなか情報が出てこなかったのでNode.jsのソースコード(GitHub)で検索したらこれあった
WIN32とも書いてあるから自分が使ってるPCがWindowsであることは関係してそう
つまりWindowsを使っているのであればEINVAL
と-4071
が1対1対応している、と予想
4. flow.jsの中身
'use strict';
process.on('unhandledRejection', err => {
throw err;
});
const chalk = require('chalk');
const runFlow = require('../flow/runFlow');
const inlinedHostConfigs = require('../shared/inlinedHostConfigs');
// This script is using `flow status` for a quick check with a server.
// Use it for local development.
const primaryRenderer = inlinedHostConfigs.find(
info => info.isFlowTyped && info.shortName === process.argv[2]
);
if (!primaryRenderer) {
console.log(
'The ' +
chalk.red('yarn flow') +
' command now requires you to pick a primary renderer:'
);
console.log();
inlinedHostConfigs.forEach(rendererInfo => {
if (rendererInfo.isFlowTyped) {
console.log(' * ' + chalk.cyan('yarn flow ' + rendererInfo.shortName));
}
});
console.log();
console.log(
'If you are not sure, run ' + chalk.green('yarn flow dom-node') + '.'
);
console.log(
'This will still typecheck non-DOM packages, although less precisely.'
);
console.log();
console.log('Note that checks for all renderers will run on CI.');
console.log(
'You can also do this locally with ' +
chalk.cyan('yarn flow-ci') +
' but it will be slow.'
);
console.log();
process.exit(1);
}
runFlow(primaryRenderer.shortName, ['status']);
5. オプション(引数)としてのdom-nodeの意味
dom-nodeを省略してyarn flowだけ実行すると以下のようになる
んで、flow.jsを見てみると
if (!primaryRenderer) {
console.log(
'The ' +
chalk.red('yarn flow') +
' command now requires you to pick a primary renderer:'
);
// ...
という箇所がある。まったく同じ文章。ここでターミナルに出力される文章を規定しているようだ。
primaryRendererの定義を見てみるとこうなってる。
const primaryRenderer = inlinedHostConfigs.find(
info => info.isFlowTyped && info.shortName === process.argv[2]
);
なので、dom-nodeなどのコマンドライン引数はprimary rendererと呼ばれていることがわかる。
直訳すると「一級の(第一義的な)レンダラー」かぁ・・・わからん
まぁとりあえず、dom-nodeなどのprimary rendererをコマンドライン引数として設定しないと、
process.exit(1);
とあるように最終的にエラーになるようになってる。
で、ちゃんとprimary rendererが設定された上でコマンドが実行された場合、そのif文には入らず以下1行のみが実行されるようになってる。
runFlow(primaryRenderer.shortName, ['status']);
なので今回dom-nodeを設定したのに発生してしまったエラーは、そのrunFlow内で起こっているということになる。
なのでrunFlowのコードを見てみる。
'use strict';
const chalk = require('chalk');
const {spawn} = require('child_process');
const fs = require('fs');
// TODO: This generates all the renderer configs at once. Originally this was
// to allow the possibility of running multiple Flow processes in parallel, but
// that never happened. If we did, we'd probably do this in CI, anyway, and run
// on multiple machines. So instead we could remove this intermediate step and
// generate only the config for the specified renderer.
require('./createFlowConfigs');
async function runFlow(renderer, args) {
return new Promise(resolve => {
let cmd = __dirname + '/../../node_modules/.bin/flow';
if (process.platform === 'win32') {
cmd = cmd.replace(/\//g, '\\') + '.cmd';
}
// Copy renderer flowconfig file to the root of the project so that it
// works with editor integrations. This means that the Flow config used by
// the editor will correspond to the last renderer you checked.
const srcPath =
process.cwd() + '/scripts/flow/' + renderer + '/.flowconfig';
const srcStat = fs.statSync(__dirname + '/config/flowconfig');
const destPath = './.flowconfig';
if (fs.existsSync(destPath)) {
const oldConfig = String(fs.readFileSync(destPath));
const newConfig = String(fs.readFileSync(srcPath));
if (oldConfig !== newConfig) {
// Use the mtime to detect if the file was manually edited. If so,
// log an error.
const destStat = fs.statSync(destPath);
if (destStat.mtimeMs - srcStat.mtimeMs > 1) {
console.error(
chalk.red(
'Detected manual changes to .flowconfig, which is a generated ' +
'file. These changes have been discarded.\n\n' +
'To change the Flow config, edit the template in ' +
'scripts/flow/config/flowconfig. Then run this command again.\n',
),
);
}
fs.unlinkSync(destPath);
fs.copyFileSync(srcPath, destPath);
// Set the mtime of the copied file to be same as the original file,
// so that the above check works.
fs.utimesSync(destPath, srcStat.atime, srcStat.mtime);
}
} else {
fs.copyFileSync(srcPath, destPath);
fs.utimesSync(destPath, srcStat.atime, srcStat.mtime);
}
console.log(
'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...',
);
spawn(cmd, args, {
// Allow colors to pass through:
stdio: 'inherit',
}).on('close', function (code) {
if (code !== 0) {
console.error(
'Flow failed for the ' + chalk.red(renderer) + ' renderer',
);
console.log();
process.exit(code);
} else {
console.log(
'Flow passed for the ' + chalk.green(renderer) + ' renderer',
);
resolve();
}
});
});
}
module.exports = runFlow;
そして改めてエラーも見る
まずここまでは無事通過し、正常に出ていることがわかる。
console.log(
'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...',
);
つまりそれ以降の
spawn(cmd, args, {
// Allow colors to pass through:
stdio: 'inherit',
}).on('close', function (code) {
if (code !== 0) {
console.error(
'Flow failed for the ' + chalk.red(renderer) + ' renderer',
);
console.log();
process.exit(code);
} else {
console.log(
'Flow passed for the ' + chalk.green(renderer) + ' renderer',
);
resolve();
}
});
のどこかでエラーになってる。
まず前半はどうやって通過できてるのかを1行ずつ見てみよう
自分でも実際に試して事実を述べていく
まず以下でcmdはc:/~~/react\scripts\flow/../../node_modules/.bin/flow
になる
let cmd = __dirname + '/../../node_modules/.bin/flow';
ちなみにそのflowというファイルを見てみるとこうなってる
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../flow-bin/cli.js" "$@"
ret=$?
else
node "$basedir/../flow-bin/cli.js" "$@"
ret=$?
fi
exit $ret
次
if (process.platform === 'win32') {
cmd = cmd.replace(/\//g, '\\') + '.cmd';
}
process.platform
はwin32なのでこのif文の中に入る。
そして正規表現によって、/
を\
に変えてる。で最後に.cmdという拡張子をつけてる。
つまりcmdが、
react\scripts\flow/../../node_modules/.bin/flow
から
react\scripts\flow\..\..\node_modules\.bin\flow.cmd
になる。
次
const srcPath = process.cwd() + '/scripts/flow/' + renderer + '/.flowconfig';
process.cwd()
の説明
つまりprocess.cwd()
の返り値は
\react
になる。このReactリポジトリのルートディレクトリって感じか。
で、rendererはrunFlow()の第一引数で既に受け取っている。
改めてflow.jsを見返すとこうなってる
runFlow(primaryRenderer.shortName, ['status']);
つまりprimaryRenderer.shortNameがrendererに入っている。
primaryRenderer.shortNameは自分が設定したコマンドライン引数、つまりdom-node
。
以上を踏まえると、
const srcPath = process.cwd() + '/scripts/flow/' + renderer + '/.flowconfig';
の結果としてのsrcPathは
react/scripts/flow/dom-node/.flowconfig
になる。
次
const srcStat = fs.statSync(__dirname + '/config/flowconfig');
fs.statSync()
の説明
つまりパスを引数に入れ、そのファイル/ディレクトリが存在するかどうかをチェックできる。
存在しなかったらエラー、存在したらそのファイル/ディレクトリの詳細情報を返す。
なので、__dirname + '/config/flowconfig'
が存在するかどうかをチェックしているということ。
まず__dirname
は
react\scripts\flow
である。つまりrunFlow.jsの親ディレクトリである。
以上から、__dirname + '/config/flowconfig'
は
react\scripts\flow/config/flowconfig
であることがわかる。
このパスが存在するかどうかをfs.statSync()
によってチェックしている。
そしてそのパスはちゃんと存在するので、そのファイルの詳細情報が返ってくる。これがsrcStatに格納される。
ちなみにそのflowconfigファイルの中身はこれ
[ignore]
.*/scripts/bench/.*
.*/build/.*
.*/fixtures/.*
# Any hidden directory
.*/\..*
# These shims are copied into external projects:
.*/rollup/shims/facebook-www/.*
.*/rollup/shims/react-native/.*
.*/node_modules/fbjs/lib/keyMirrorRecursive.js.flow
.*/node_modules/resolve/test/.*
.*/__mocks__/.*
.*/__tests__/.*
# contains modern flow syntax that requires a Flow upgrade
.*/node_modules/prettier-plugin-hermes-parser/.*
# TODO: noop should get its own inlinedHostConfig entry
.*/packages/react-noop-renderer/.*
%REACT_RENDERER_FLOW_IGNORES%
[libs]
./node_modules/fbjs/flow/lib/dev.js
./scripts/flow/environment.js
./scripts/flow/react-devtools.js
./scripts/flow/react-native-host-hooks.js
./scripts/flow/xplat.js
[lints]
untyped-type-import=error
internal-type=off
deprecated-type=off
[options]
munge_underscores=false
react.custom_jsx_typing=true
# Substituted by createFlowConfig.js:
%REACT_RENDERER_FLOW_OPTIONS%
[version]
%FLOW_VERSION%
次
const destPath = './.flowconfig';
これはそのままなので飛ばす
次
if (fs.existsSync(destPath)) {
ん?さっきの記事でこんなのを見たが、もしこれが正しいのであれば、旧式のメソッドと混合して使われてるのは妙だな
fs.existsSync(path) はv1.0.0でDeprecatedになっていたので、fs.statSync(path)を使う。
fs.existsSync
の説明
ん-なるほど、旧式(非推奨)なのかどうかは知らんがちゃんと別の役割を持ってるな。真偽値で返すようになってるのがexistsSyncか
というか旧式じゃないかも?
C:\Users\ユーザ名\node_modules\@types\node\fs.d.ts
を見てみたら、
export function existsSync(path: PathLike): boolean;
と書いてあり、deprecatedの記述は無い。
が、exists()
メソッドというやつの定義を見てみると
export function exists(path: PathLike, callback: (exists: boolean) => void): void;
/** @deprecated */
export namespace exists {
/**
* @param path A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
* URL support is _experimental_.
*/
function __promisify__(path: PathLike): Promise<boolean>;
}
と書いてあり、deprecatedつまり旧式(非推奨)っぽい。
つまり、別にfs.existsSyncはdeprecatedではないのではないだろうか。
fs.existsと間違えたのかな?それともdeprecatedではなくなったとか?
とりあえずこのコード
if (fs.existsSync(destPath)) {
に戻ると、destPathつまり./.flowconfig
が存在するかチェックしてる。
これの返り値はtrueになっていたが、どこにそのファイルがあるのか確認するためにVSCode上で.flowconfig
と検索したらなぜか1つも出てこない。
なので以下コードを書いたら
const path = require('path');
const absolutePath = path.resolve(destPath);
console.log(absolutePath);
ここにあることがわかった。
react\.flowconfig
確かにあったが、.gitignore
の影響でファイル名が薄くなっていたので、.gitignoreに入れられたファイルはVSCodeの検索に出てこなくなるのかもしれない。
とりあえず、最終的にはtrueになるのでこのif文の中に入る。
次
const oldConfig = String(fs.readFileSync(destPath));
const newConfig = String(fs.readFileSync(srcPath));
fs.readFileSync(path)
はpathのファイルを読み取り、そのバッファを返す。
String()
で囲めばバッファを文字列(=ファイルそのものの中身)に変換できる。
なのでoldConfigにはreact\.flowconfig
の中身が入っている。
newConfigにはreact/scripts/flow/dom-node/.flowconfig
の中身が入っている。
次
if (oldConfig !== newConfig) {
上の2つが同じかどうか比較するだけ。
こう書いてチェックしてみてもtrueになっている。
console.log(oldConfig === newConfig);
なので、このif文の中には入らない。
次
console.log(
'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...',
);
やっとここに戻ってきた。これは前述の通り。
で、残りはこれだけ。このどこかでエラーになってる。
spawn(cmd, args, {
// Allow colors to pass through:
stdio: 'inherit',
}).on('close', function (code) {
if (code !== 0) {
console.error(
'Flow failed for the ' + chalk.red(renderer) + ' renderer',
);
console.log();
process.exit(code);
} else {
console.log(
'Flow passed for the ' + chalk.green(renderer) + ' renderer',
);
resolve();
}
});
改めてエラーを見る
気になるのはここ
つまりrunFlow.jsからもう一度flow.jsに戻っていることがわかる。
そしてそのflow.js内の11行目でthrow err;
に遭遇してしまっているようだ。
該当箇所はここ
process.on('unhandledRejection', err => {
throw err;
});
なので残りのrunFlow.jsの処理内に「flow.jsにまた戻っていろいろやるで」という処理があるはず。
ということでまた1行ずつ見ていく。
まずこれ
spawn(cmd, args, {
// Allow colors to pass through:
stdio: 'inherit',
})
spawnはさっき学んだ。新しい子プロセスを生成してコマンドを実行することのできるnodeのメソッド。
VSCodeで補完がでてきた。こんな感じで使うのね。
第一引数のcmd
はこれ
react\scripts\flow\..\..\node_modules\.bin\flow.cmd
第二引数のargs
はflow.jsでrunFlowを呼び出した際に渡してある
runFlow(primaryRenderer.shortName, ['status']);
なのでargsは['status']
。
しかし第三引数の
{
// Allow colors to pass through:
stdio: 'inherit',
}
がよくわからない。
spawnの引数は何が入るのか、Node.js公式で確認
んでさらに第三引数のoptionsがstdioの場合について
オプション
options.stdio
は、親プロセスと子プロセスの間に確立されるパイプを構成するために使用される。
便宜上、
options.stdio
には以下の文字列のいずれかを指定する:
'inherit'
:['inherit', 'inherit', 'inherit']
または[0, 1, 2]
と同じ
それ以外の場合、
options.stdio
の値は、各インデックスが子の fd に対応する配列である。fd 0、1、2 は、それぞれ stdin、stdout、stderr に対応する。追加の fd を指定して、親と子の間に追加のパイプを作成することもできる。値は次のいずれかである。
'inherit'
: 親プロセスとの間で、対応するstdioストリームを渡す。最初の3つの位置では、それぞれprocess.stdin
、process.stdout
、process.stderr
と等価である。それ以外の位置では、'ignore'
と等価である。
うーん、全体的に意味わからんぞ。
まずfdって何だ?Claudeに聞いてみる
なるほど。
fdとは"File Descriptor"(ファイルディスクリプタ)の略で、UNIXライクなシステムにおける重要な概念です。
ファイルディスクリプタは:
基本的な説明:
- オープンされているファイルを識別するための整数値
- プロセスがファイルやI/Oリソースにアクセスする際の「句柄」や「参照」のような役割
- 0から始まる連番で管理される
標準的なファイルディスクリプタ:
0: 標準入力 (stdin) 1: 標準出力 (stdout) 2: 標準エラー出力 (stderr)
- 具体例:
// Node.jsでの例 const { spawn } = require('child_process'); // stdio配列の各要素が、順番にfd 0, 1, 2に対応 spawn('some-command', [], { stdio: [ 0, // stdin (fd: 0) 1, // stdout (fd: 1) 2 // stderr (fd: 2) ] }); // これは以下と同じ spawn('some-command', [], { stdio: 'inherit' // ['inherit', 'inherit', 'inherit']と同等 });
- 実際のファイルシステムでの表現:
- Unixシステムでは
/dev/fd/
ディレクトリ下に数字のファイルとして存在- 例:
/dev/fd/0
は標準入力を表す
ほうほう、やはり0~2のfdは既に決まってるのね
つまり「0」から「2」までのファイルディスクリプタは
0:標準入力
1:標準出力
2:標準エラー出力と決まっています。
パソコンさんが動き出した時点で割り当てられるのです。よって、自分で開いたファイルには「3」以降の数字が割り当てられます。
また、標準入力や標準出力、標準エラー出力に何かしたいときは「0」から「2」のいずれかを指定することになります。
Claudeに公式ドキュメントを読ませたうえで聞いてみる
stdioオプションについて、わかりやすく説明させていただきます。
このオプションは、親プロセス(実行元のNode.jsプログラム)と子プロセス(spawn()で起動されたプロセス)の間での入出力(I/O)の接続方法を制御するものです。
stdio: 'inherit'
と指定した場合の動作を具体的に説明すると:
- 子プロセスの標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)が、親プロセスの対応するストリームに直接つながります
- つまり:
- 子プロセスの出力が直接ターミナルに表示される
- 子プロセスのカラー出力などの制御文字がそのまま維持される
- ターミナルからの入力が直接子プロセスに渡される
簡単な例を示します:
const { spawn } = require('child_process'); // stdio: 'inherit' の場合 const process1 = spawn('ls', ['-la'], { stdio: 'inherit' }); // → ls の出力が直接ターミナルに表示される // stdio: 'pipe' (デフォルト) の場合 const process2 = spawn('ls', ['-la']); process2.stdout.on('data', (data) => { console.log(data.toString()); }); // → 出力をイベントとして受け取り、手動で処理する必要がある
主な設定値の使い分けは:
'inherit'
: 子プロセスの出力をそのままターミナルに表示したい場合(ビルドツールやCLIツールの実行など)'pipe'
: 子プロセスの出力を親プロセスで加工・制御したい場合'ignore'
: 子プロセスの出力を完全に無視したい場合'ipc'
: 親子プロセス間でメッセージを送受信したい場合特に
'inherit'
は、子プロセスの出力をそのまま表示したい場合によく使用され、ビルドツールやCLIツールを実行する際によく使われます。
なるほどなぁ
こういう情報が欲しかった
- 子プロセスの出力が直接ターミナルに表示される
- 子プロセスのカラー出力などの制御文字がそのまま維持される
- ターミナルからの入力が直接子プロセスに渡される
確かに今試したらその通りだった。
const process1 = spawn('ls', ['-la'], { stdio: 'inherit' }); // 出力される
const process2 = spawn('ls', ['-la']); // 出力されない
つまり「親プロセスの標準入出力環境(stdio)を子プロセスが継承(inherit)するよ、だから子プロセスから直接ターミナルに出力したりできるようになるよ」ということか。
だからこの// Allow colors to pass through:
というコメントの意味も、「子プロセス内でこれから書くchalk.red()
とかで設定する色もちゃんとターミナルまで通る(pass through)ようにするよ、'inherit'
にすることによって」という感じだろう
spawn(cmd, args, {
// Allow colors to pass through:
stdio: 'inherit',
})
この部分は理解
次
}).on('close', function (code) {
'close'
イベントについて。プロセスの処理が全部終わったら実行される
'close'
イベントは、プロセスが終了し、子プロセスの stdioストリームがクローズされた後に発行される。複数のプロセスが同じ stdio ストリームを共有する可能性があるため、'exit'
イベントとは区別される。'close'
イベントは、すでに'exit'
が発行された後に発行される。 子プロセスがスポーンに失敗した場合は'error'
が発行される。
んで、引数のcode
には正常終了したら0
、エラーなら0
以外が入る。
If a callback function is provided, it is called with the arguments (error, stdout, stderr). On success, error will be null. On error, error will be an instance of Error. The error.code property will be the exit code of the process. By convention, any exit code other than 0 indicates an error. error.signal will be the signal that terminated the process.
If the process times out or has a non-zero exit code, this method will throw an Error that will include the full result of the underlying child_process.spawnSync().
(code
引数がエラーになって0以外の値が入る...じゃあどんな種類があるの?と気になったのでGPTにちょろっと聞いてみた)
ということでまとめると、このコードは「全部処理が終了したらこの中のコードを実行しまっせ」という意味だな
次(最後)
closeイベントの中のコードはこれ。これで最後。
if (code !== 0) {
console.error(
'Flow failed for the ' + chalk.red(renderer) + ' renderer',
);
console.log();
process.exit(code);
} else {
console.log(
'Flow passed for the ' + chalk.green(renderer) + ' renderer',
);
resolve();
}
codeが0以外だったらエラーログ出力して、0だったら成功なので成功ログ出力して、という感じ。
だが不可解なのは、このどちらもターミナルに出力されなかったということ。
つまり'close'
イベント内にすら届かず、その前段階のコマンドでエラーが発生して強制終了、みたいなことが起きているっぽい?でもエラーが起きたとしてもclose内は絶対実行されるはずなのでおかしい...。(途中でエラーがthrow
されちゃうと'close'
イベントには届かない仕様になってるとか?)
とりあえずそのコマンドの中身を見てみる。当然これ。
spawn(cmd, args, {
stdio: 'inherit',
})
cmd
とargs
の中身はすでに見た通り。
cmd
はreact\scripts\flow\..\..\node_modules\.bin\flow.cmd
。
(省略系なので厳密には以下)
C:\Users\ユーザ名\Downloads\react\scripts\flow\..\..\node_modules\.bin\flow.cmd
で、args
は['status']
。
なので実質的にはこれは
C:\Users\ユーザ名\Downloads\react\scripts\flow\..\..\node_modules\.bin\flow.cmd status
というコマンドをinheritオプションで実行していることになる。
無駄な部分を省くと、
C:\Users\ユーザ名\Downloads\react\node_modules\.bin\flow.cmd status
と同じ。
なのでそのままそれを実行してみたが、エラーになった
省略しても同じ
(そのエラーのFixをCopilotが提案してくれたやつ。メモ)
あ、v0に聞いてみたら、
GitBashはUnix-likeな環境だからWindowsの標準的なパス形式(バックスラッシュや
:
を使用)を正しく解釈できないよ。C:
は/c/
に変えて、バックスラッシュ(\
)はスラッシュ(/
)に変更すれば良いよ
と教えてくれた。なるほど。まさかのターミナルの環境が原因だったか。
なのでこれに直して再度実行してみる
/c/Users/ユーザ名/Downloads/react/node_modules/.bin/flow.cmd status
よし、成功!
status
はflowに対するコマンドで、現在のプロジェクトの型チェック状態を確認することのできるものらしい。
追記:flowのvscode拡張機能入れた後に同じコマンド打つとエラーで失敗するようになる。
拡張機能がすでに機能してるからコマンドの必要性が無いのか?よくわからん
GitBashじゃなくてPowerShellとかで実行すべきだったってことか...
ちなみにreact\node_modules\.bin\flow.cmd
の中身はこれ
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\flow-bin\cli.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\flow-bin\cli.js" %*
)
powershellで実行しなおせばやっと作業再開できそう、ということでやってみる
yarn flow dom-node
エラーが出る
→これらの記事通りSet-ExecutionPolicy RemoteSigned
を実行すればいけそう。
実行するには管理者権限でpowershellを開かないといけなかったのでこれ実行して開く
(現在のディレクトリを維持したまま新しい管理者権限のPowerShellウィンドウを開くコマンド)
Start-Process powershell -Verb RunAs -ArgumentList "-NoExit", "-Command", "Set-Location -Path '$PWD'"
で再度yarn flow dom-node
を実行してみる、これでいけるんじゃないか?
→まさかの同じエラー
errno: -4071,
code: 'EINVAL',
syscall: 'spawn'
ん-、であればやはりさっきのflow.cmdファイルの中身を理解する必要があるか
だがその前に、あえてyarn flow dom-node
より先の手順から実行してみるか。それで解決する可能性もあるので。(無理だったらflow.cmdを読解する)
つまりVSCodeの拡張機能であるFlow Language Supportをインストールするところから。
(久しぶりにこの記事戻ってきたな)
気になるのは、yarn flow dom-node
のところの説明で
上記手順後に node_modules/.bin/flow が生成されますが、plugin側がこれを使います。
と書いてあるのでやはりyarn flow dom-node
を実行しないとダメか...と一瞬思ったが、探して見ると既にreact\node_modules\.bin\flow
ファイルはあった。
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../flow-bin/cli.js" "$@"
ret=$?
else
node "$basedir/../flow-bin/cli.js" "$@"
ret=$?
fi
exit $ret
さっきGitBashで
/c/Users/ユーザ名/Downloads/react/node_modules/.bin/flow.cmd status
の実行が成功したから、それで作れたのか?と思ったが、そういう訳でもない。ファイルのプロパティ見てみたら最初からこのflowファイルは存在する。
...謎だ・・・。記事の内容が古いのが理由か?
まぁ、とりあえずflowファイルがあるのはありがたいので、次の手順に進めるのではないかという期待は高まる。やってみよう
VSCodeの拡張機能、Flow Language Supportをインストール
C:\Users\ユーザ名\AppData\Roaming\Code\User\settings.json
に以下を追加
"flow.pathToFlow": "/c/Users/ユーザ名/Downloads/react/node_modules/.bin/flow",
"flow.useNPMPackagedFlow": true,
"javascript.validate.enable": false
で、
コードジャンプ等のエディタの機能が効くはずです!
とあるのでお目当てのreact\packages\shared\ReactSharedInternals.js
の__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
で試してみたら、定義元に飛べた!やっとディグを再開できそうだ!
yarn flow dom-node
を実行できなかったのに、なぜ結果的にうまくいったかはわからんが...。
ゴリ押しでそのままコマンドを実行させることはできたのでそれが功を奏したか?
しかし別にそれによってflowファイルが作られた訳でも無さそうだし、意味は無いんじゃないかとは思うが...
そしてそもそもなぜyarn flow dom-node
でエラーが出るのかもわからんが...powershellターミナルに変えてもspawnの内部で実行されるターミナルはpowershellになっていないみたいなことが起きてしまっているのか?
そういえばflow / flow.cmdファイルの中身を理解してないので(軽く生成AIに聞いてはみた)、そこらへんに原因があるか?
でもコマンド直打ちでは成功してる時点でコマンドの内容に原因がある訳ではないはず。
つまり自分(人間)がターミナルから直接実行すると成功するのに、同じコマンドをspawnが実行すると失敗するという事象が起きている・・・。
まぁ、結果としてはなぜか上手くいって、このまま進めそうなのでまず進んでみよう。
(元はと言えばこのReactリポジトリで開発環境を立ち上げてconsole.log(secret_○○)
とかをすることで中身を見てやろうというのが目的だったが、そもそもコードジャンプが機能するようになったので、もうその必要性はなくなった)
import * as React from 'react';
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;
定義元はreact\packages\react\index.js
で、中身はこれ
export {
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
// ...
} from './src/ReactClient';
./src/ReactClient.js
に行くとこうなってる
import ReactSharedInternals from './ReactSharedInternalsClient';
//...
export {
// ...
ReactSharedInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
// ...
};
./ReactSharedInternalsClient.js
に行くとこうなってる
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent';
export type SharedStateClient = {
H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
A: null | AsyncDispatcher, // ReactCurrentCache for Cache
T: null | BatchConfigTransition, // ReactCurrentBatchConfig for Transitions
S: null | ((BatchConfigTransition, mixed) => void), // onStartTransitionFinish
// DEV-only
// ReactCurrentActQueue
actQueue: null | Array<RendererTask>,
// Used to reproduce behavior of `batchedUpdates` in legacy mode.
isBatchingLegacy: boolean,
didScheduleLegacyUpdate: boolean,
// Tracks whether something called `use` during the current batch of work.
// Determines whether we should yield to microtasks to unwrap already resolved
// promises without suspending.
didUsePromise: boolean,
// Track first uncaught error within this act
thrownErrors: Array<mixed>,
// ReactDebugCurrentFrame
getCurrentStack: null | (() => string),
};
export type RendererTask = boolean => RendererTask | null;
const ReactSharedInternals: SharedStateClient = ({
H: null,
A: null,
T: null,
S: null,
}: any);
if (__DEV__) {
ReactSharedInternals.actQueue = null;
ReactSharedInternals.isBatchingLegacy = false;
ReactSharedInternals.didScheduleLegacyUpdate = false;
ReactSharedInternals.didUsePromise = false;
ReactSharedInternals.thrownErrors = [];
// Stack implementation injected by the current renderer.
ReactSharedInternals.getCurrentStack = (null: null | (() => string));
}
export default ReactSharedInternals;
やっと実態が見えてきたぞ。あとはこのH
が何か理解できれば進める。
しかし変なことしてるもんだな~~。
これはつまり
ReactSharedInternals
としてexport
↓
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
としてexport
↓
ReactSharedInternals
として受け取る
...ということをしてる訳だな。
なんでそんな無駄にも思えることをしてるんだろう。まぁ一旦無視で
ここのDispatcherを見るべきか
export type SharedStateClient = {
H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
フック用のReactCurrentDispatcher?とのことだが
Dispatcherを押すとreact\packages\react-reconciler\src\ReactInternalTypes.jsに飛ぶ
該当箇所はこれだった
export type Dispatcher = {
use: <T>(Usable<T>) => T,
readContext<T>(context: ReactContext<T>): T,
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
unstable_useContextWithBailout?: <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
) => T,
useContext<T>(context: ReactContext<T>): T,
useRef<T>(initialValue: T): {current: T},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useEffectEvent?: <Args, F: (...Array<Args>) => mixed>(callback: F) => F,
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void,
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
useDeferredValue<T>(value: T, initialValue?: T): T,
useTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
],
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T,
useId(): string,
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
useMemoCache?: (size: number) => Array<any>,
useHostTransitionStatus?: () => TransitionStatus,
useOptimistic?: <S, A>(
passthrough: S,
reducer: ?(S, A) => S,
) => [S, (A) => void],
useFormState?: <S, P>(
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
) => [Awaited<S>, (P) => void, boolean],
useActionState?: <S, P>(
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
) => [Awaited<S>, (P) => void, boolean],
};
へぇ、DispatcherってのはDispatchなどを利用したuseStateなどの、すべて(?)のフックの型を含んでる、「フックの型を集めた型」みたいな感じか
なのでHは、nullもしくはこのDispatcher型になる、ということがわかる。
つまりuseStateとか特定のフックではなくそれらをすべて包含する型。
ということでreact\packages\react\src\ReactHooks.js
にやっと戻ってくる。
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
dispatcherにはHが入る、つまりDispatcher型のでかいオブジェクトが入る
んで開発環境、かつdispatcher === nullだったらユーザーにエラーで警告する
resolveDispatcher関数は何があろうととりあえずこれをreturnする
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
コメントを直訳するとこうなる
レンダリングフェーズ以外でアクセスするとNULLアクセスエラーになります。これはホットパスにあるため、意図的にエラーをスローしないようにしています。また、これがインライン化されていることを確認するのにも役立ちます。
ホットパスとは何か調べたらこんな感じらしい。へぇ
スピードレイヤー(ホットパス)の定義
バッチ処理にてデータ処理を行い、永続的に大量のデータを保持する層。
確かにsetStateなどは非同期でバッチ処理されるので、そういう情報を保持しているフックの集合体はホットパスと呼べるのは納得できる
んで肝心のこれは何かというと、
return ((dispatcher: any): Dispatcher);
まぁそのままで、dispatcherを受け取ってDispatcherを返すというだけの関数だな
dispatcher自体もDispatcher型なので、受け取ったものを少しいじったり確認したりするだけで、その後そのまま返す、みたいなことなのかな?と予想
これ間違ってた。TypeScriptで言うと確かに関数だが、これはFlowなのでFlowとして見るとDispatcher型に型キャストしているだけなので、Dispatcher型のdispatcherということになる。
つまりオブジェクトであるdispatcherをreturnしている。
同ファイルにあるuseStateを見てみる
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
ここに書かれてた通り
initialState: (() => S) | S,
の() => S
はuseStateのinitializer function(初期化関数)だろうな
返り値である[S, Dispatch<BasicStateAction<S>>]
のDispatch<BasicStateAction<S>>
は記事にある通り((S => S) | S) => void
を意味する、すなわちsetStateの型
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
BasicStateActionは「setStateの引数」の型だな
で、Dispatchは「あるものを受け取って何らかの処理をするが、何も返さない」という型
それらを組み合わせてsetStateの型を作ってる
んでuseState関数が実際に行う処理はこの2行
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
dispatcherにはさっきの((dispatcher: any): Dispatcher)
という関数が入る。
んん...?よく考えるとこれ変だな
return dispatcher.useState(initialState);
dispatcherは関数そのものが今入っているので、useStateというプロパティを持っていないはずでは?
dispatcher.useStateでuseStateが取得できる理由がわからない。
dispatcher()と実行してからやっとDispatcher型のものが返るので、そこからuseStateを取らなければいけないのでは?
つまり
dispatcher.useState
ではなく
dispatcher().useState
では?
衝撃的事実...!
gptに聞いてみたら「いや、そもそもそれ関数ちゃうで」と言われた。
んで、公式見てみたが、確かに
Before Flow version 0.229, the legacy syntax (value: Type) was used.
と書かれている。旧式ではvalue: Type
が型キャスト方法として存在していたらしいのでそれを使っているということなのではないだろうか
つまり、このresolveDispatcherが返している((dispatcher: any): Dispatcher)
は、関数ではなくDispatcher型にキャストされたdispatcherというただのオブジェクトということなのかもしれない。
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
それなら矛盾が解消される。なるほどなぁという感じ。
一旦その理解で読み進めてみる
(Flowはまったく使ったことが無い & TypeScriptはほんのちょっとしか知らない...)
その仮説が正しければこの記事の
そしてdispatcher、もといReactCurrentDispatcher.currentが引数である関数がreturnされています。
は間違っていることになる..。?
要確認
ここに戻る
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
この2行
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
dispatcherはDispatcher型なので、その中にuseStateプロパティがあることに矛盾は無い。
でそのuseStateにinitialStateを引数として渡したうえで実行した結果をreturnしている。
へぇ、「useStateの中で、元々定義してあるuseStateの実行結果を返す」ということをしてるのか
その、Dispatcherに元々定義されているuseStateを見に行くと、こうなってる
export type Dispatcher = {
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
ん、呼び出し元とまったく一緒や
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
(ふと気づいたことメモ)
ということで、useStateの表面的な定義だけはわかったが、実質的にどんな値が入ってくるのかはわかっていない。
それを知るためには、元々のdispatcherが何から代入されているのか知る必要があるかもしれない
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
つまりこのReactSharedInternals.H
がDispatcher型として定義されているというのはわかったが、実際の値はいつどのようにこれに代入されているのか?がまだわかっていない。
ので、それを探るためにReactSharedInternals.H =
とかで検索かけてみる
HooksDispatcherというそれっぽいものが代入されてる箇所を2つ発見。
- 1つ目(5195行あるファイル)
export function performWork(request: Request): void {
...
ReactSharedInternals.H = HooksDispatcher;
- 2つ目(4408行あるファイル)
function performWork(request: Request): void {
...
ReactSharedInternals.H = HooksDispatcher;
他にもHooksDispatcherOnUpdateInDEVとかHooksDispatcherOnMountInDEVとかが代入されてるのもあったが、DEVがついてるやつは何となく後回しにしてみる(理由は無いがなんとなく。本番環境の方が大事なんじゃないかという適当すぎる勘)
どっちもperformWork関数であることがわかる。
これめっちゃ見覚えあるなぁ、この記事になかったっけ
performWorkではなくperformUnitOfWorkだった。むむむ
そういやWorkがつくやつでいうとflushWork関数ってのもあったな、全然覚えてないけど
(それを自分が呟いたやつ)
まぁとにかくperformWork関数については初見だなぁ
とりあえずどっちも全体としてはどうなってるのか見てみる
(ていうかどっちも、ファイル名から意味が推測できないなぁ)
- 1個目(61行)
export function performWork(request: Request): void {
if (request.status === CLOSED || request.status === CLOSING) {
return;
}
const prevContext = getActiveContext();
const prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = HooksDispatcher;
let prevAsyncDispatcher = null;
if (enableCache || __DEV__) {
prevAsyncDispatcher = ReactSharedInternals.A;
ReactSharedInternals.A = DefaultAsyncDispatcher;
}
const prevRequest = currentRequest;
currentRequest = request;
let prevGetCurrentStackImpl = null;
if (__DEV__) {
prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
}
const prevResumableState = currentResumableState;
setCurrentResumableState(request.resumableState);
try {
const pingedTasks = request.pingedTasks;
let i;
for (i = 0; i < pingedTasks.length; i++) {
const task = pingedTasks[i];
retryTask(request, task);
}
pingedTasks.splice(0, i);
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
}
} catch (error) {
const errorInfo: ThrownInfo = {};
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
} finally {
setCurrentResumableState(prevResumableState);
ReactSharedInternals.H = prevDispatcher;
if (enableCache) {
ReactSharedInternals.A = prevAsyncDispatcher;
}
if (__DEV__) {
ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
}
if (prevDispatcher === HooksDispatcher) {
// This means that we were in a reentrant work loop. This could happen
// in a renderer that supports synchronous work like renderToString,
// when it's called from within another renderer.
// Normally we don't bother switching the contexts to their root/default
// values when leaving because we'll likely need the same or similar
// context again. However, when we're inside a synchronous loop like this
// we'll to restore the context to what it was before returning.
switchContext(prevContext);
}
currentRequest = prevRequest;
}
}
- 2個目(34行)
function performWork(request: Request): void {
const prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = HooksDispatcher;
const prevRequest = currentRequest;
currentRequest = request;
prepareToUseHooksForRequest(request);
const hadAbortableTasks = request.abortableTasks.size > 0;
try {
const pingedTasks = request.pingedTasks;
request.pingedTasks = [];
for (let i = 0; i < pingedTasks.length; i++) {
const task = pingedTasks[i];
retryTask(request, task);
}
if (request.destination !== null) {
flushCompletedChunks(request, request.destination);
}
if (hadAbortableTasks && request.abortableTasks.size === 0) {
// We can ping after completing but if this happens there already
// wouldn't be any abortable tasks. So we only call allReady after
// the work which actually completed the last pending task
const onAllReady = request.onAllReady;
onAllReady();
}
} catch (error) {
logRecoverableError(request, error, null);
fatalError(request, error);
} finally {
ReactSharedInternals.H = prevDispatcher;
resetHooksForRequest();
currentRequest = prevRequest;
}
}
ほぇ、なかなかの量だなぁ...大分時間かかりそう
とはいえHooksDispatcherというのが代入されてるのは同じなので、とりあえずそれだけ確認してみたい
ReactSharedInternals.H = HooksDispatcher;
ということでHooksDispatcherを調べていく
まずReactFizzServer.jsのHooksDispatcherの方から
こうなってる
import {
...
HooksDispatcher,
} from './ReactFizzHooks';
ReactFizzHooks.jsに飛ぶとこうなってる
export const HooksDispatcher: Dispatcher = supportsClientAPIs
? {
readContext,
use,
useContext,
useMemo,
useReducer,
useRef,
useState,
useInsertionEffect: noop,
useLayoutEffect: noop,
useCallback,
// useImperativeHandle is not run in the server environment
useImperativeHandle: noop,
// Effects are not run in the server environment.
useEffect: noop,
// Debugging effect
useDebugValue: noop,
useDeferredValue,
useTransition,
useId,
// Subscriptions are not setup in a server environment.
useSyncExternalStore,
}
: {
readContext,
use,
useContext,
useMemo,
useReducer: clientHookNotSupported,
useRef: clientHookNotSupported,
useState: clientHookNotSupported,
useInsertionEffect: clientHookNotSupported,
useLayoutEffect: clientHookNotSupported,
useCallback,
useImperativeHandle: clientHookNotSupported,
useEffect: clientHookNotSupported,
useDebugValue: noop,
useDeferredValue: clientHookNotSupported,
useTransition: clientHookNotSupported,
useId,
useSyncExternalStore: clientHookNotSupported,
};
supportsClientAPIsがtruthy(真値)だった場合はたくさんのフックをそのまま格納したオブジェクトになる。falsy(偽値)だった場合はいくつかのフックをclientHookNotSupported型としておき、あとは同じ。
clientHookNotSupportedは同ファイルに定義されていて、ただエラーを投げる関数。
このコンポーネントは決してハイドレイトされないため、renderToHTMLでステートフックまたはエフェクトフックを使用できません。
function clientHookNotSupported() {
throw new Error(
'Cannot use state or effect Hooks in renderToHTML because ' +
'this component will never be hydrated.',
);
}
なるほど、だめな使い方されてたらフック自体をエラー関数にしちゃうみたいなエラーハンドリング的な処理がどっかにあるんだろうな。
んでそのだめな使い方ってのは、サーバー側でつまりRSC(?)とかでuseStateなどを使っちゃった場合ではないだろうか(知らんけど)
じゃあ肝心のsupportsClientAPIsとは何なのか調べていく
こうimportされてる
import {
...
supportsClientAPIs,
} from './ReactFizzConfig';
ReactFizzConfig.jsファイルはこうなってる
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
/* eslint-disable react-internal/prod-error-codes */
// We expect that our Rollup, Jest, and Flow configurations
// always shim this module with the corresponding host config
// (either provided by a renderer, or a generic shim for npm).
//
// We should never resolve to this file, but it exists to make
// sure that if we *do* accidentally break the configuration,
// the failure isn't silent.
throw new Error('This module must be shimmed by a specific renderer.');
まさかの何も無し。なんじゃこりゃ?
英語を直訳する
Rollup、Jest、Flowのコンフィギュレーションは、対応するホストコンフィギュレーション(レンダラーによって提供されるか、npm用の汎用シム)によって、常にこのモジュールをシムすることを期待している。
shimとは、修正コードのことらしい。動詞として使われてるので、「修正する」ということだろうか
シム (shim) とは、すでに存在するコードの動作を修正するために使用されるコードの一部で、通常は問題を回避する新しいAPIを追加することで動作します。
このファイルへの解決は決して行うべきではないが、万が一誤って設定を壊してしまった場合でも、失敗が黙殺されないようにするために存在する。
throw new Error('このモジュールは、特定のレンダラーによってシムされなければならない。')
ふーむ
「このファイルは修正されるべきだぞ、それをしなかったら(=設定が壊れてる)、エラーが吐かれるようにするぞ、このファイルはそのエラーを書いておくためだけのファイルだぞ」みたいなことかな?
であればこのファイルからimportする場合、何らかの設定によりこのファイルがちゃんと修正され、supportsClientAPIsなどがしっかりと中身に記述されるようになるので、普通は大丈夫、みたいなこと?
であればその「何らかの設定」とは?Rollupってやつの設定ファイル的なやつがあるのか?
あと、もしそうだとしてもsupportsClientAPIsの定義や中身を見るにはどうすればいい?書き換えるためのファイルみたいなのが用意されてるのか?
gptに聞いてみたらなるほどぉになった
まずshimは「そのモジュールを修正する」というより「同じ名前のモジュールが他にもあるからそっちに移し替える」みたいなイメージかも
(モジュールとファイルはとりあえずイコールと捉えて良さそう?)
なるほどなぁ、モジュールを1つだけ用意してそれを修正していく、という訳ではなく、モジュールを複数用意してそれを時と場合によって適切なものに差し替えることによって実質的に修正したことになる、ということか。であれば「修正」というより「差し替え」という表現が確かに適切だな
ので「ReactFizzConfig」がファイル名に含まれていて、supportsClientAPIsをexportしてる箇所を探して見ると、3か所発見。
react\packages\react-dom-bindings\src\server\ReactFizzConfigDOM.js
export const supportsClientAPIs = true;
react-markup\src\ReactFizzConfigMarkup.js
// Disable Client Hooks
export const supportsClientAPIs = false;
react\packages\react-server\src\forks\ReactFizzConfig.custom.js
export const supportsClientAPIs = true;
なのでおそらくこれらのうちどれかを常に解決(選択?)するように設定されているのだろう。
なるほど、理解できた。
んでその設定自体も一応確認しておきたいので探して見るか。何の設定だろう、
ちょっと前提知識拾っておくか
基礎が無すぎて恥ずかしいが、
Rollupとやらはバンドラらしいがそもそもバンドラってなんだ?
なるほど、モジュール(≒ファイル)をバンドルする(束にする=1つのファイルにまとめる)からモジュールバンドラと呼ばれるのか
あとコンパイル、ビルド、トランスパイルってなんだっけ?
へぇ、そういうことなんだ
ts→jsに関して言えばコンパイルとトランスパイルは同じ意味と捉えて良さそう
とはいえ、本来の意味の「コンパイル」とはどうしてもずれてきてしまう部分はあるので、こうしたTypeScriptをJavaScriptにコンパイルするような、同レベル間の言語を翻訳することは「トランスパイル」や「クロスコンパイル」と呼び分けされていたりします。
なので、「コンパイル(≒トランスパイル) / バンドル / その他処理 を総称したものがビルド」と言える
ふむふむ、コンパイルはただ「別の言語に変換する」という広義はあるものの、
コンパイルとは、指定された言語で書かれたコンピュータープログラムを、別の形式や言語の一連の命令に変換する作業のことです。コンパイラーは、その作業を実行するコンピュータープログラムのことです。
「バイナリコード/機械語に変換する」という狭義もありそうだな
通常、コンパイラーは C++、Rust、Java などの高級言語で書かれたコードを実行可能な(実行可能な)コード、いわゆるバイナリーコードや機械語に変換します。
バンドラの歴史はこんな感じ
BrowserifyやWebpackといった第2世代のバンドラーは、2011年から2012年頃に登場しました。
やがてJavaScriptの進化が一層進み、モジュール型プログラミングの人気が高まると、第3世代のバンドラーが登場します。Rollup(2014年)はライブラリとパッケージのバンドリング最適化に焦点を当て、Parcel(2017年)はゼロコンフィグセットアップと高速開発ワークフローを強みとしました。
ということでどれが影響してるのかわからので1個1個それっぽいバンドラやテストツールなどのファイルを見てみる
Roolup
rollup.config.jsが4つある
react\compiler\packages\react-compiler-runtime\rollup.config.js
react\compiler\packages\babel-plugin-react-compiler\rollup.config.js
react\compiler\packages\eslint-plugin-react-compiler\rollup.config.js
react\compiler\packages\react-compiler-healthcheck\rollup.config.js
すべて見たが、ReactFizzConfig関連の設定は見られなかった。rollupじゃないのか。
Jest
jest.config.jsが2つある
react\compiler\packages\make-read-only-util\jest.config.js
react\compiler\packages\babel-plugin-react-compiler\jest.config.js
しかし同じくそれらしい記述は無い
webpack
5つある
react\packages\react-devtools-extensions\webpack.config.js
react\fixtures\art\webpack.config.js
react\fixtures\flight\config\webpack.config.js
react-devtools-inline\webpack.config.js
react\packages\react-devtools-fusebox\webpack.config.frontend.js
しかし同じくfizzと調べても何もマッチしない。
Browserify
configファイルは無かった
babel
5つある
react\packages\react-devtools-shared\babel.config.js
react\babel.config.js
react\fixtures\legacy-jsx-runtimes\babel.config.js
react\compiler\packages\eslint-plugin-react-compiler\babel.config.js
react\fixtures\stacks\babel.config.json
しかしfizzという文字列を含む箇所はやはり無い
ん-、だめだ見つからん
まぁいいや、多分「場合によって、使われるReactFizzConfigファイルは切り替えられる」という仮説は正しいはずなのでそれで進める(知らんけど)
ということで戻るが、supportsClientAPIsの実質的な値の代入箇所は3か所あった
react\packages\react-dom-bindings\src\server\ReactFizzConfigDOM.js
export const supportsClientAPIs = true;
react-markup\src\ReactFizzConfigMarkup.js
// Disable Client Hooks
export const supportsClientAPIs = false;
react\packages\react-server\src\forks\ReactFizzConfig.custom.js
export const supportsClientAPIs = true;
「今はクライアントで使ってええで」を表すファイルならtrueに、そうでないならfalseにしてくれる感じと予想
更にHooksDispatcherに戻る
export const HooksDispatcher: Dispatcher = supportsClientAPIs
? {
readContext,
use,
useContext,
useMemo,
useReducer,
useRef,
useState,
useInsertionEffect: noop,
useLayoutEffect: noop,
useCallback,
// useImperativeHandle is not run in the server environment
useImperativeHandle: noop,
// Effects are not run in the server environment.
useEffect: noop,
// Debugging effect
useDebugValue: noop,
useDeferredValue,
useTransition,
useId,
// Subscriptions are not setup in a server environment.
useSyncExternalStore,
}
: {
readContext,
use,
useContext,
useMemo,
useReducer: clientHookNotSupported,
useRef: clientHookNotSupported,
useState: clientHookNotSupported,
useInsertionEffect: clientHookNotSupported,
useLayoutEffect: clientHookNotSupported,
useCallback,
useImperativeHandle: clientHookNotSupported,
useEffect: clientHookNotSupported,
useDebugValue: noop,
useDeferredValue: clientHookNotSupported,
useTransition: clientHookNotSupported,
useId,
useSyncExternalStore: clientHookNotSupported,
};
Dispatcher型ではあるが、それに加えて「使えないフックは使えないようにしておく」のがHooksDispatcherという感じかな
noopという型は空の関数なので
function noop(): void {}
んで更に戻る
そのHooksDispatcherがReactSharedInternals.Hに代入されるタイミングがあるということになる。
(さっき述べた2つのファイル内で)
ReactSharedInternals.H = HooksDispatcher;
んで、そのタイミングとはさっきのperformWork関数が実行されるタイミング。
このperformWork関数の中身と実行されるタイミングを理解できれば理解が進むはず
(結構重そう)
んーていうかこの記事見て気付いたけど、ReactSharedInternals.Hに値を代入してるのそのperformWork関数内の2か所だけじゃないんだよな
react\packages\react-reconciler\src\ReactFiberHooks.js
にあるrenderWithHooks関数内
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
...
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
HooksDispatcherOnMountとHooksDispatcherOnUpdate。
mount時かupdate時か分けるという情報までちゃんと区別されてるやつっぽい
この話はこの記事でも言われてたこと
うーん、そっちで探っていった方が良さそうだな、そっちに切り替えるか
自分が見つけたHooksDispatcherだと、2か所それぞれのファイル名が
react\packages\react-server\src\ReactFizzServer.js
react\packages\react-server\src\ReactFlightServer.js
でserver?fizz?flight?なんじゃそりゃ、ていう感じでよくわからん名前だから例外処理っぽいやつなんじゃないかと不安だったけど、今回のやつは
react\packages\react-reconciler\src\ReactFiberHooks.js
なのでreconcile!fiber!ということで大事なメイン処理なのではという勝手な予想があるので。。
(ていうかReactSharedInternals.H =
で検索かけたのに最初これ出てこなかったなぁ、大分遠回りしてしまった感。vscodeの検索ってあんま信用ならない?というかアクティブなエディタ内でしか文字列検索しない?いや、そんなはずはないが...)
ちょっと広げるとこんな感じ
if (__DEV__) {
if (current !== null && current.memoizedState !== null) {
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactSharedInternals.H = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
}
} else {
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
なので開発環境(__DEV__
)ではなく本番環境の話。
currentがnullか、もしくはcurrentがnullでなくてもcurrent.memoizedStateがnullなら、マウントとしてHooksDispatcherOnMountがReactSharedInternals.Hに入る。
そうでないならHooksDispatcherOnUpdateが入る。
currentとcurrent.memoizedStateとは何だろう。
current.memoizedStateは多分stateだよな?currentは?まずこれら調べるか
ん、currentは引数か
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
...
)
Fiber型とやらなのか、見てみよう
Fiberはこれ。でかいな
export type Fiber = {
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
tag: WorkTag,
// Unique identifier of this child.
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
// The resolved function/class/ associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
refCleanup: null | (() => void),
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
updateQueue: mixed,
// The state used to create the output
memoizedState: any,
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,
// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
mode: TypeOfMode,
// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
lanes: Lanes,
childLanes: Lanes,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descendants of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugInfo?: ReactDebugInfo | null,
_debugOwner?: ReactComponentInfo | Fiber | null,
_debugStack?: string | Error | null,
_debugTask?: ConsoleTask | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
};
1個1個見ていく
Fiber=実行される必要があるもしくは既に実行されたコンポーネント上での作業
んでコンポーネントごとに複数存在することもあると。それは納得できる、ファイバーはノードを仮想的に表現したようなものなので
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component
よくわからん。
「バグがあったからしゃーなしに1つの型に統合したんだよね~」
「まぁでも作るオブジェクトが1個だけで済むっていうメリットあるから今のところ1個にまとめてるで」
って感じか?
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
次
fiberの型を識別するタグ
// Tag identifying the type of fiber.
tag: WorkTag,
fiberの型っていうのはどういうことだろう。
WorkTagの型はこれ
export type WorkTag =
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18
| 19
| 20
| 21
| 22
| 23
| 24
| 25
| 26
| 27
| 28
| 29;
30個もあるのか。なぜだろう。
あ、確かにfiberってノードみたいなもんだから、htmlでいう属性とかそういうのを示す必要性があるから30個もあるのかもしれない。ふむふむ
いや、全然そういう訳でもなかった。。
下見るとちゃんと詳細が書いてあった。
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;
export const IncompleteFunctionComponent = 28;
export const Throw = 29;
なるほどー、0だったらFunctionComponent、1だったらClassComponentとかそういう感じらしいので、「そのfiberが属するコンポーネントはどんなものなのか」という情報がtagか。
fiberの情報というよりfiberが属すコンポーネントの情報か、わかりづらいなぁ
(だったらtagじゃなくてcomponentっていうプロパティ名にした方が直感的だと思うが...)
普通のhtmlの部分をHostComponentと呼ぶっぽい
じゃあtagはコンポーネントだけじゃなくて各要素のことも指すのか
というかコンポーネントの部分(<App />
)も要素と同列で同じように格納されるんか、へぇ
次
これの子の一意な識別子
// Unique identifier of this child.
key: null | string,
「これ」というのはfiberだとすると、fiberの子fiberを識別するのがkeyプロパティか。
ということはfiberは子孫情報を持っていることになるな。(nullでなければ)
string型のみでnumber型は無いのなぜだろう
次
これの子のreconcile時に同一性を保持するために使用されるelement.typeの値
つまり
子fiberのreconcile時に同一性を保持するために使用されるelement.typeの値
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
keyで一意性は保てると思うが...keyが無かった時のためのものなのかな?
んでelement.typeって何だ?普通にhtmlでいう要素みたいなことかな?それともnodeType的な?
ちょっと探して見たら、elementがReactElementとして扱われてるところを発見。
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
if (__DEV__) {
owner = element._owner;
}
const type = element.type;
...
でReactElementの型はこれ
export type ReactElement = {
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
// __DEV__ or for string refs
_owner: any,
// __DEV__
_store: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
_debugInfo: null | ReactDebugInfo,
_debugStack: Error,
_debugTask: null | ConsoleTask,
};
typeがany型やないか...結局わからんぞそれだと
いろいろ探して見たら、
テストコード見ると'div'とされてるとこもあるし、
it('returns an immutable element', () => {
const element = <Component />;
if (__DEV__) {
expect(() => (element.type = 'div')).toThrow();
} else {
expect(() => (element.type = 'div')).not.toThrow();
}
});
REACT_FRAGMENT_TYPEとされてるとこもある
if (element.type === REACT_FRAGMENT_TYPE) {
ちなみにREACT_FRAGMENT_TYPEはこれ
多分あのフラグメント(<></>
)のことだろう
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
以上を踏まえて、element.typeは「htlmlタグ(要素)や、それ以外のフラグメントなどの形状も含む、ReactElementの型」と言えそう
(一応gptメモ)
もしこれが正しいのであればコンポーネント自体もelement.typeに含まれそう
なので戻ると...
子fiberのreconcile時に同一性を保持するために使用されるelement.typeの値
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
さっきのtagと違ってelementTypeは「fiberそのもの(厳密にはReactElement)が何の要素・種類なのかみたいな情報を格納したもの」、かな
いや、tagもそのfiberそのものの情報だな。divなどもコンポーネントとみなすので
次
このfiberに関連する、解決された関数/クラス/
// The resolved function/class/ associated with this fiber.
type: any,
fiberが属するコンポーネント/クラスの情報ってことか?
でもそれだとtagとまったく一緒な気が...
いや、違うか。
tagはそもそも数字だし、関数コンポーネント、クラスコンポーネント、html要素などの情報を保持するけど、
typeはこのfiberが属する関数/クラスをそのまま保持しているみたいな感じか。
じゃあ多分tagは、1つのコンポーネントの中で言うと、FunctionComponentが1個だけあってあとはほぼ全部HostComponentみたいな感じ(でそれを数字で表してる)
でtypeはあるコンポーネント/クラス内ならどのfiberも同じで、そのコンポーネント/クラスがそもそも入ってる感じ
のはず。理解
(一応メモ)
FiberNodeっていう関数見つけたんだが...
もしかしてFiberってfiberノードじゃなくてfiberツリーのこと?
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = -0;
this.actualStartTime = -1.1;
this.selfBaseDuration = -0;
this.treeBaseDuration = -0;
}
そういう訳でもなさそう、普通にFiberはfiberノードのことっぽい
(じゃあこのFiberNodeはなんやねん)
型ではなく関数だから、fiberと同じ感じの型ではあるが処理をいろいろしたいみたいな感じか?
次
このファイバーに関連するローカルステート
// The local state associated with this fiber.
stateNode: any,
なんだこりゃ?...でもどっかで聞いたような
これだ!どんどん積み重なっていくやつか!
あーそうなのか。divとかでもコンポーネントと呼ぶのか。めっちゃ忘れてるな...
一部例外はありますが、1個のFiberは1個のコンポーネント(<MyComponent>や<div>など)管理するオブジェクトです。
ふむふむ、なるほど
stateNodeはFiberが持つプロパティの1つであり、自分と自分の子要素の全てのDOMをマージしたものを保存しています。引き返すにつれて段々とDOMが構築されていくのが分かります。ただし、関数コンポーネント(今回でいうApp)はstateNodeを持たず、nullが入ります。
じゃあまとめるとこのstateNodeプロパティは、
「そのfiberが(そのfiberの時点で)持っているDOMの状態」
だな
次
概念上のエイリアス
...?そんなこと言い出したらこの世のすべてのプロパティや言葉も概念上のエイリアスでは?
実体≠エイリアスなので。
まぁ、「もっと実体から離れてる呼び方をするプロパティですよ」みたいなことなんだろうと予想。
// Conceptual aliases
「parentは、returnされるfiberと偶然同じになる。なぜならfiberとinstanceをマージしたから」
???
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
「残りのフィールドはfiberに属する」
あぁーーーなるほど、ということは...
// Remaining fields belong to Fiber
前のこれの意味がわかったかも
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
こっちの最初の方のフィールド(のプロパティ)は、概念的にはインスタンスのメンバーであると。でも今さっきのフィールドはfiberに属すると。
で、「以前は別の型に分割され、他のFiberフィールドと交差していたが、Flowが交差のバグを修正するまでの間、これらを単一の型に統合した」という部分は、つまり今さっきのフィールドとこの最初のフィールドが別々だったけど、そうするとバグるから統一したんだよね、という感じだと思われる。
なるほど
なのでとりあえずここからはfiberに属するフィールド/プロパティが続くっぽい
(...じゃあさっきまでのはfiberに属してなかったんかい?それがちょっとよくわからん)
たしかにtagはfiberそのものの種類(数字)だし、keyは子fiberの識別子だし、typeはfiberが属するコンポーネントそのものだし、stateNodeはfiberというよりこれまでのfiberの情報を踏まえた上でのDOMなんだけど...
elementTypeはfiberそのものだしなぁ...ようわからん
次
このファイバーの処理を終えた後に戻るファイバー。これは事実上の親だが、親は複数(2つ)存在する可能性があるため、現在処理中のものの親に過ぎない。
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,
現在処理中のものの親に過ぎないというのがよく意味がわからないが...
とにかく、returnは親fiberのことだな
child、subling、returnプロパティを利用することで自分の子、兄弟、親にアクセスできます。
次、3つ連続してる
単一リンクリストツリー構造
???
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
childは子fiber、siblingは兄弟fiber
child、subling、returnプロパティを利用することで自分の子、兄弟、親にアクセスできます。
indexは何だろう?
うーん、兄弟fiber内での順序かな、と予想
次
このノードをアタッチするために最後に使用されたref
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
このfiber自身への参照、どうやってこのfiberにたどり着いたか、みたいなものか
次
refCleanup: null | (() => void),
多分refを消してくれる(クリーンアップする)関数?
次
Inputは、このファイバーを処理するために入ってくるデータである。引数。変数。」
???
タグをオーバーロードすると、この型はより具体的になる」
???
「出力を作成するために使用される変数」
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
ようわからん。
memoizedPropsは、stateNodeを作り出すための素材的な?
次
状態の更新とコールバックのキュー
// A queue of state updates and callbacks.
updateQueue: mixed,
mixedってなんや?と思ったら、flow独自の型なのか。だいぶ緩いやつっぽい
(GPTいわくtsのunknownってやつと似てるとのこと。初耳)
「どんな値でも代入可能だが操作するには型を確認する必要がある」という特徴がある。
updateQueueってことはupdateオブジェクトをたくさん持ってるキューなのかなぁ、そんな気がする
callocさんと話したやつなんだよなぁ、多分
fiberオブジェクトはupdateオブジェクトを溜め込む循環リストを持っていて
この「循環リスト」がupdateQueueのことではないだろか、と予想
ちなみにupdateオブジェクト自体は、仮想DOM②への更新内容そのものかと
次
出力を作成するために使用されるステート
// The state used to create the output
memoizedState: any,
(さっきのmemoizedPropsが「出力を作成するために使用される変数」だったがめっちゃ似てるな...)
これはまさにuseStateのstateそのものだと思われる
例えばmountStateというやつ(後で詳しく見るけど)なんかを覗いてみるとこうなってるので
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
// ...
return [hook.memoizedState, dispatch];
}
いや、でもそれはhook.memoizedStateであって...
今回はFiber.memoizedStateで、これは単なるstateではなく、「updateオブジェクトが格納された循環リスト」っぽいぞ
この循環リストをフックの持ち主であるFiberのmemoizedStateに保存しておきます。
追記:多分間違ってた。てかそもそもさっき、Fiber.updateQueueがupdateを格納してる循環リストだと思うって自分で言ってたんだから同じ訳ないやん、意味わからんこと言ってる、何してんねん自分...
Fiber.memoizedStateは「フック(もしくはフックス?)が格納されてるリスト」っぽい
フックスというより1つのフックオブジェクトを格納してるっぽい。
だがそのフックオブジェクト自体がnextというプロパティによって他のフックへの参照を持っていて、それがどんどんネストしていって...
みたいな感じなので、実質複数のフックを持っていると言えるよね、みたいな感じ
mountWorkInProgressHookを見れば、Fiber.memoizedStateがフックの連結リストであるはずだと予想できる
次
このファイバーが何か(any)を所持している場合、このファイバーの依存関係(コンテキスト、イベント)
???
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,
Dependenciesという型は同ファイルでこう定義されてる
export type Dependencies = {
lanes: Lanes,
firstContext:
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null,
_debugThenableState?: null | ThenableState, // DEV-only
};
お、レーンは聞き覚えがあるな。これだ
レーン = タスクの優先度 と言えそう。32ビット表現のフラグであり、number型。
その32ビットの数字を見れば自ずとタスクの名前もわかるようになってる。
気になること:
- 具体的な用途は?
- Lanesっていう複数形になってるのはなぜ?
それぞれについて見ていく
1. 具体的な用途は?
目立つ使われ方は主に2つで、レンダリング中に使われます。
- 1個1個のFiberにレーンのフラグを付与して、更新のレンダリング時に作業が必要なFiberか、スキップしてよいFiberか判定する。
- 「Renderフェーズ」を中断できるかどうか判定する(先程紹介したものです)。
fiberの「作業」ってなんだ?fiberからstateNodeを構築すること?
とりあえずレーン(タスクの優先度を表したもの)は、
- fiberの作業を実行すべきか否かを判定
- レンダーを中断できるか否かを判定
のどちらかで使われる
2. Lanesっていう複数形になってるのはなぜ?
LaneとLanesの型定義を見てみる
export type Lanes = number;
export type Lane = number;
どちらもnumberなのが驚き。LanesはLaneが複数入った配列なのかなと思っていたがそうでもないらしい。じゃあなんだろう
同ファイルにあるレーンの定義を見てみたらLaneにされているのがほとんど
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane =
SyncLane | InputContinuousLane | DefaultLane;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
export const SomeRetryLane: Lane = RetryLane1;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
しかし、その下にあるカスタムレーン的なもの、つまりいくつかのレーン同士でビット演算をしたものはLanesとして使われていた。
// Any lane that might schedule an update. This is used to detect infinite
// update loops, so it doesn't include hydration lanes or retries.
export const UpdateLanes: Lanes =
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes;
export const HydrationLanes =
SyncHydrationLane |
InputContinuousHydrationLane |
DefaultHydrationLane |
TransitionHydrationLane |
SelectiveHydrationLane |
IdleHydrationLane;
なるほど、そういうことか。つまりビット演算(今回はOR演算)の結果として導出したカスタムレーン的なものは、わかりやすいようにLanesという型にしてあるということだと思われる。
...んーでもビット演算の結果としての新しい32ビット文字列を保持しておく理由がわからん。そんなもんいつ使うんだ?
GPTに聞いたらなるほどーになった。
ある枠組みとしていくつかのレーンをまとめて、それと現在のレーンを比較して0以外になったらその枠組みの中のどれかのレーンとは一致したんだなということが判定できる、という例。
なるほどなぁ
const TransitionLanes: Lanes = 0b0000000001111111111111110000000;
if ((currentLanes & TransitionLanes) !== 0) {
// いずれかの TransitionLane がアクティブ
}
ということで戻ると、Dependenciesという型は、複数レーンをビット演算でマージした結果としてのビット列をプロパティとして持っているということになる。(他のプロパティは一旦無視する)
export type Dependencies = {
lanes: Lanes,
firstContext:
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null,
_debugThenableState?: null | ThenableState, // DEV-only
};
んでそのDependencies型であるdependenciesプロパティをfiberは持っている。
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,
次
ファイバーとそのサブツリーに関するプロパティを記述するビットフィールド。例えば、ConcurrentMode フラグは、サブツリーをデフォルトで非同期にするかどうかを示す。ファイバーが作成されると、親のモードを継承する。作成時に追加のフラグを設定することもできるが、その後は、ファイバーのライフタイム中(特に子ファイバーが作成される前)、値は変更されない。
// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
mode: TypeOfMode,
またビットフィールドか。親のモードを継承するらしい。
とりあえずTypeOfModeの型定義を見てみる
export type TypeOfMode = number;
export const NoMode = /* */ 0b0000000;
// TODO: Remove ConcurrentMode by reading from the root tag instead
export const ConcurrentMode = /* */ 0b0000001;
export const ProfileMode = /* */ 0b0000010;
export const DebugTracingMode = /* */ 0b0000100;
export const StrictLegacyMode = /* */ 0b0001000;
export const StrictEffectsMode = /* */ 0b0010000;
export const NoStrictPassiveEffectsMode = /* */ 0b1000000;
ふーん...まぁ、とりあえず雰囲気はOK
次
エフェクト
??
// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
deletionsはこれだろうな
次に、削除しなければいけないFiberを記憶しておきます。今回削除するのは<span>Hello</span>でした。親である<div>のFiberが持つdeletion配列に記憶しておきます。
なので「削除すべき子ファイバーが入った配列」だな
flagsとsubtreeFlagsはなんだ?
Flagsの型定義を一部抜粋するとこれ
export type Flags = number;
// Don't change these values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b0000000000000000000000000000;
export const PerformedWork = /* */ 0b0000000000000000000000000001;
export const Placement = /* */ 0b0000000000000000000000000010;
export const DidCapture = /* */ 0b0000000000000000000010000000;
export const Hydrating = /* */ 0b0000000000000001000000000000;
// You can change the rest (and add more).
export const Update = /* */ 0b0000000000000000000000000100;
export const Cloned = /* */ 0b0000000000000000000000001000;
ようわからんけどレーンと同じようにビットになってる感じかぁ
なのでとりあえずflagsとsubtreeFlagsはなんかの判定に使うっぽい
次
lanes: Lanes,
childLanes: Lanes,
ん、レーンはdependenciesプロパティの中のプロパティとして存在してたけど、直接保持もしているのか、なぜだ?
dependencies.lanesがレンダー中断可否判定用のレーンで、Fiber.laneがfiberの作業を実行すべきか否か判定する用のレーン、的な?(知らん)
とりあえずlanesは普通にこのfiberのレーンで、childLanesは子fiberのレーンってことだろうな、多分
childLanesは子fiberのレーンということではないかも。よくわからん
次
これはファイバーのプール版である。更新されるすべてのファイバーは、最終的にペアを持つことになる。必要であれば、メモリを節約するためにペアをクリーンアップできる場合もある。
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
すごい、コピー用のfiberも持ってるみたいなこと?初耳だ
クリーンアップできる余地もあるのか、へぇ
「何かの拍子でfiberが消し飛んだらこのalternateのfiberで復元する」みたいなことするんかな?(知らん)
復元用ではなく「差分検出用の古いfiberへのリンク」かも
あとのプロパティは全部オプショナルパラメータついてるからあんま大事ではない気がする
のでこれでFiberの中身はOK。
ここに戻ろう
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
...
)
ちなみにfiber.memoizedStateが循環リストであると言ったが、fiber自体も連結リストになってるらしい
fiberはそれ自体が連結リストになっていて、コンポーネントを連結できます。
ReactSharedInternals.Hに実質的な値を格納しているのは何か見ている途中だった
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// ...
if (__DEV__) {
} else {
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
// ...
return children;
}
ということで、currentとはfiberである。
恐らくCurrent(Fiberツリー①)とWIP(Fiberツリー②)のうちのCurrentのことを指しているのだと思われる
なのでHostRootが入ってるはず(HostRootは勿論FiberNodeつまりFiber)
それを踏まえてこれはどういう意味かというと、
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
currentのファイバーがnull、もしくはcurrentファイバーがあってもcurrent.memoizedStateがnullだったらmount時や!と判定してHooksDispatcherOnMountを代入してる
memoizedStateはさっきやった「updateオブジェクトを格納してる循環リスト」
それぞれがなぜmountと呼べるのか考えてみよう
- currentのファイバーがnull
そもそもまだFiberツリーが一度も作成されていないということになる。jsx()を実行したことがないということ。当然mount時
- currentのファイバーはnullでないが、current.memoizedStateはnull
うーん、currentのファイバーがnullでない理由がよくわからんが...
とりあえず、memoizedStateがnullってことはすなわちupdateオブジェクトが存在しないということであり、それはすなわち更新しようとしてないということ、なのでupdateではなくmountだよね、ということ。みたいな感じかな
理解
ということで同ファイルにあるHooksDispatcherOnMountとHooksDispatcherOnUpdateを見てみる
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
use,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
};
ということでuseStateの部分だけ見てみると、mountの方はmountState
という型に、updateの方はupdateState
という型になっている。
目的にたどり着いたのでこのスクラップはここで閉じる。
途中はこのスクラップで(まずmountStateから見ていくか)