Didactを学ぶ
Reactが動く原理を学ぶために、Didactの勉強をしました。
勉強を通してわからなかったこと、より理解を深めるために考えたことを備忘録としてここに残します。
なお、以下内容のソースは以下です。
https://pomb.us/build-your-own-react/
Reactとは
React(仮想DOM)は、実DOMの挙動の遅さを改善するための工夫が入っている。
仮想DOMは、React内部のアルゴリズムを解説したもののため、「仮想DOM=React」と見てよし。
本来のReactは、実DOMと仮想DOM、それらの差を無くすことが主目的のパッケージ(モジュール)。
変更必要箇所の検出処理はReact(仮想DOM)で行い、最終的に「ReactDOM.render(仮想DOMの要素,実DOMの要素)」で実DOMを更新している。
実DOMの初期時はまっさらな状態。
仮想DOMで作成した要素を最終的に実DOMに入れていく。
(仮想DOMの信頼性が無くなるため、実DOMは絶対触らない!)
更新内容の比較は、仮想node同士で行っているため、
高速に処理が可能になる。(実DOMのpropsは覗いてないね)
render()関数では、VNodeと実DOMの要素を引数でもらい、貰った実DOM(container)にVNodeを反映させるために一度「Fiber」という構造体に変換している!
VNodeを作るのはcreateElement()関数だけ、
rootFiberを作るのはrender()関数とsetState()関数。
その後の1つ1つのfiberを編んでいくのはperformUnitOfWork()関数だね!
▼VNode(ユーザーが入力した情報の構造化)の構造
//typeとpropsだけ
{
type : Counter,
props : {
children : [],
}
▼fiber(実domに必要な要素をVNodeに追加)の構造
{
//dom以下が追加されている
type : "TEXT_ELEMENT",
props : {nodeValue : 1},
dom : null,
parent : h1,
alternate : null,
effectTag : "PLACEMENT",
}
fiberは実domに入れていくものだから、
domの項目をすべて持っている。
ユーザーはVNodeの情報しか書かない。
<a href="google.com" target="_blank">hello</a>
//createElement()関数を通してVNodeに変換すると...
{
type: "a",
props : {
href: "google.com",
target : "_blank",
children : [{
type: "TEXT_ELEMENT",
node_value: "hello"
}],
}
ユーザーはfiberの情報は知らなくて良くて、
VNodeだけ知っていればいいよ。
<VNodeとfiberの関係>
BがFiber。
理由は、fiberはVNodeを元に実際のdomを作るから、fiberの情報は全てでないとだめ。
<Reactで言われるコンポーネント>
①VNodeをrenderメソッドで返すクラスと
②VNodeを返す関数だけ。
//①の例
class App extends React.Component {
render() {
//returnの外ならJavascriptが書ける!
const text = "Hello World";
return (
<div> {text} </div>
):
}
}
<stateについて>
Counter()関数が実行**されると、userState()関数が呼ばれて、setState()関数を使ってstateの値を更新している!
##どのタイミングでDOMを読み取り、更新する必要があるの?
ユーザー操作、時間、通信、windowイベントなど
##JSXをJSに変換してみる
//jsxバージョン
const element = <h1 title="foo">Hello</h1> // jsx
//JSバージョン(仮想DOMにnodeを作っている(Not実DOM))
const element = React.createElement(
"h1", //type
{ title: "foo" }, // html属性を表現。しなければ「null」
"Hello"
)
//もっと詳しいJSバージョン(仮想DOMのnode。本物のdomを表現している)
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
//reactバージョン:実DOM要素を仮想DOM要素に置き換える
ReactDOM.render(element, container)
jsxで簡単に書いたコードは、コンパイルされてJSのコードになる。
親要素は必ず1つ。
親要素のchildrenの中に配列で子要素、その中に子要素、、とつなげていくよ。(以下のように)
親要素:div
子要素:h1,h2
//React.createElement()関数(仮想DOMのnodeを作る関数)の実行結果
const element = {
type: "div",
props: {
title: "foo",
children: [{
type: "h1",
props: {
title: "foo",
children: [],
},
},{
type: "h2",
props: {
title: "foo",
children: [],
},
}
],
},
}
//React.createElement()関数(仮想DOMのnodeを作る関数)
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("h1", null, "bar"),
React.createElement(h2b")
)
▼ステップ0のコードまとめ
//①仮想DOMにnodeを作る
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
//②★仮想DOMのnodeを参考にして、実DOMを作っていく
const container = document.getElementById("root")
//実DOMのnodeをつくっていく
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
###ReactとJQueryの違い
ReactはJQueryのようにグローバルなnamespace(名前空間=スコープ)を触らずに、画面に変更を加えられる。
それぞれのチームが独立して動けるよ。(グローバルではないから、勝手に変更されるようなことがない)
例えば..
JQueryはPage1とPage2を操作するとき、id名が被っていて本来想定していなかった結果になるが、
ReactはPage1とPage2を別々のコンポーネントとして扱うことができる(外部の要素に依存しない)。
また、現在はclassは使わず、関数コンポーネントが使われているらしい。
const RootTree = () => (
<div id="root">
<Page1 visible />
<Page2 />
</div>
)
//not class(コンストラクターも不要)
class RootTree extends React {
render() {
return (
<div id="root">
<Page1 visible />
<Page2 />
</div>
)
}
}
##ステップ1:createElement関数(React.createElement()関数:仮想DOMのnodeを作る)
JSXをJSに変換して、createElement関数を呼び出して見よう。
//jsx
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
//以下同じ
const container = document.getElementById("root")
ReactDOM.render(element, container)
//JS
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
//以下同じ
const container = document.getElementById("root")
ReactDOM.render(element, container)
実際にcreateElement関数を作ってみる。
//React.createElement()関数の実装
function createElement(type, props, ...children) {
return {
type,
props: {
...props,//1つのスコープの引数
children, //子要素
},
}
}
上記のcreateElement関数を実行してみた場合、
以下のようになる。
For example, createElement("div") returns:
{
"type": "div",
"props": { "children": [] }
}
createElement("div", null, a) returns:
{
"type": "div",
"props": { "children": [a] }
}
and createElement("div", null, a, b) returns:
{
"type": "div",
"props": { "children": [a, b] }
}
children配列は、文字列や数値などのプリミティブ値を入れることができる。
プリミティブの場合は「document.createTextNode()」を使ってテキストノードを生成する。
**「document.createTextNode()」**は、「nodeValue」というプロパティに格納されている文字を画面上に表示する。
//プリミティブの場合は「createTextElement()」関数を実行
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
//「createTextElement()」関数の中身
function createTextElement(text) {
return {
type: "TEXT_ELEMENT", //プリミティブの場合
props: {
nodeValue: text,//「nodeValue」は「createTextElement()」関数により画面上に表示されるテキストを格納する
children: [],//配列の1つの要素のため、必ず空の配列が返る
},
}
}
Reactはすべてオブジェクトで管理されている。
onst React = {
createElement,
render,
}
//関数の呼び出し
React.createElement();
React.render();
##ステップ2:render()関数
「ReactDOM.render()」は、画面(仮想DOMの内容を実DOMに入れる)に反映させる関数。
//render関数(仮想DOMのnodeを作り、実DOMに描画する関数)の実装
//引数のelementには、内部的にReact.createElement()の返り値である仮想DOMのnodeオブジェクトが入ってくる
function render(element, container) {
//domは実domのnode
const dom =
element.type == "TEXT_ELEMENT" //プリミティブ型
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
//Object.keys():指定されたオブジェクトが持つプロパティの名前の配列を通常のループで処理するのと同じ順序で返す
Object.keys(element.props) //propsのkeyが配列になる
.filter(isProperty)
.forEach(name => {
//例:「a herf」 だと、「a」がdomで「href」がname
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
//実DOM(container)に仮想DOM要素(dom)を追加
container.appendChild(dom)
}
▼Object.keys()メソッドの例文
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
// expected output: Array ["a", "b", "c"]
▼ステップ2までのコード
//JSXをJSのオブジェクトに変換する関数
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
//createElement関数から呼び出される関数
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
//仮想DOMのnodeから実DOMを作る
//引数のelementにはDiact.createElement()関数の戻り値である仮想DOMのnodeが入っている
function render(element, container) {
//domは実domのnode
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
//実DOMに実DOMのnodeを追加
container.appendChild(dom)
}
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
//内部的にDiact.createElement()関数が実行され、仮想DOMのnodeが作られる
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
//引数のelementにはDiact.createElement()関数の戻り値である仮想DOMのnodeが入っている
Didact.render(element, container)
##ステップ3:並行モード
この再帰呼び出しに問題があります。
element.props.children.forEach(child =>
render(child, dom)
)
レンダリング(描画)を開始したら、完全な要素ツリーをレンダリングするまで停止しません。
要素ツリーが大きい場合、メインスレッドが長時間ブロックされる可能性があります。
したがって、作業を小さな単位に分割し、各単位を終了した後、他に必要なことがあれば、ブラウザーにレンダリングを中断させます。
「window.requestIdleCallback()」メソッドは、ユーザーの待ち時間(何もやってない時間)に引数の関数を実行するメソッド。
引数にはイベントループがアイドル状態の時に実行したいコールバック関数が渡され、コールバック関数にはIdleDeadLineオブジェクトが渡される。
IdleDeadLineオブジェクトは、アイドル状態の際に余っている時間とコールバックが実行されたかされていないかを示す。
let nextUnitOfWork = null
//「workLoop()」はデーモンのようにバックグラウンドでずっと動いているような関数(メインスレッドで動く)
function workLoop(deadline) {
//shouldYield:「workLoop()」をレンダリング(描画)を中断するかどうか
let shouldYield = false
//否定演算子「!」は変数(shouldYield)内の値(false)は反転させずに、演算の結果のみ反転(true)させる
//★演算子により値を返る方法は、代入演算子しかない!
while (nextUnitOfWork && !shouldYield) {
//performUnitOfWork()は、今の仕事をこなして、次のfiber(作業)を生成する関数
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
//「deadline.timeRemaining()」関数は、現在のアイドル時間(待ち時間:使っていい時間)の残りの推定ミリ秒数を返す
//待ち時間が1ミリ秒より小さければ、作業をストップさせる
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
//【書き換え:開始】workLoop()関数を以下のように否定演算子を使わずわかりやすく書くことも可能
function workLoop(deadline) {
let doMoreWork = true
while (nextUnitOfWork && doMoreWork) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
doMoreWork = 0 < deadline.timeRemaining()
}
requestIdleCallback(workLoop)
}
//↑【書き換え:終了】workLoop()関数の書き換えはここまで
//一番最初のworkLoop()関数の呼び出し部分
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
下記のコードのDOMを作る部分を「createDOM()」関数として、独自の関数で保持します。
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
let nextUnitOfWork = null
▼createDOM(独自の関数)
//createDom()関数は、仮想DOMのnode「fiber(引数)」から実DOMのnodeを作る。
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
//render()関数は、最初の1回だけ実行され、与えられたelement(仮想DOMのnode)をfiberの形にして、nextUnitOfWorkに置いておくだけの関数。
//nextUnitOfWorkはfiberの形
//children:[element]の中に仮想DOMのnodeが入る
function render(element, container) {
// TODO set next unit of work
}
let nextUnitOfWork = null
次に、一番最初のnode(fiber)を作る時だけ1度実行される、render関数を定義する。
//render()関数は一番最初のnodeを作る時だけ実行(1回限り)
//nextUnitOfWork:次の仕事に関するオブジェクトを返す。(fiberと呼ばれる)
function render(element, container) {
nextUnitOfWork = {
dom: container, //実DOMの要素
props: {
children: [element], //element:仮想DOMの要素が入れ子になって配列に入る
},
}
}
let nextUnitOfWork = null
続いて、今の仕事をこなして、次のfiber(作業)を生成する関数である「performUnitOfWork()」関数を定義する。
function performUnitOfWork(fiber) {
// ①TODO add dom node
// ②TODO create new fibers
// ③TODO return next unit of work
}
まずは①新しいノードを作成してDOMに追加するところから!
function performUnitOfWork(fiber) {
//最初の要素はrender()関数が実行されるので、該当しないが、2回目以降すべて該当する
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
//最初の要素は一番親のため該当しないが、2回目以降すべて該当する
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// TODO create new fibers
// TODO return next unit of work
}
次に、**②子供ごとに新しいfiber(1つのnode)**を作成する。
function performUnitOfWork(fiber) {
// TODO create new fibers
const elements = fiber.props.children
//例[a, b]
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props, //例:props={title :"href",children:["bar"]}と入っている
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// TODO return next unit of work
}
最後に、**③次の作業単位を検索(next unit of work)**します。
function performUnitOfWork(fiber) {
// TODO return next unit of work
if (fiber.child) {
return fiber.child
}
//childがない一番末端のもの(例:下記Fibber Tree表のp,a,h2)
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
//returnなしで、while (nextFiber){}に移る
nextFiber = nextFiber.parent
}
}
▼ステップ4のコードまとめ(performUnitOfWork()関数の部分)
//performUnitOfWork()関数は、新しいfiberを作る関数
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
//fiberのオブジェクト(=const newFiberのところ)の中にchildという項目が作られる
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
ステップ5:レンダリング(描画)およびコミットフェーズ
ここで別の問題があります。
要素を操作するたびに、DOMに新しいノードを追加しています。
また、ツリー全体のレンダリングが完了する前に、ブラウザが作業を中断する可能性があることを覚えておいてください。
その場合、ユーザーには不完全なUIが表示されます。(ちらつきの問題)
function performUnitOfWork(fiber) {
//fiber(仮想DOMnodeと実DOMnodeの混合)ごとにappendChildしているため、作業途中の状態が画面に表示されている
//実DOMにappendchildしている
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
したがって、DOMを変更する部分「appendChild()」をここから削除する必要があります。
//削除すべき部分(親要素にappendChild()して都度表示箇所を増やしている部分)
function performUnitOfWork(fiber) {
...
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
...
}
function render(element, container) {
//【変更点①】「nextUnitOfWork =」を「wipRoot =」に変更。wip(WorkInProgress:作業中)Rootという作業中のfiberrootを作成!wipRootを更新するのは、一番最初に実行するrender()関数(最初のnodeを作る)と一番最後に実行するcommitRoot()関数(rootnodeに対してchildrenをつけていく)の2つのみ。
wipRoot = { //最初のrootnode
dom: container,
props: {
children: [element],
},
}
//【変更点②】nextUnitOfWorkにwipRootを代入
nextUnitOfWork = wipRoot
}
//nextUnitOfWorkとwipRootの初期値設定
let nextUnitOfWork = null
//【変更点③】wipRootの初期値設定
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
今回変わるのは以下の2箇所。
//★【①】commitRoot()関数
//一番最後に1度だけ実行される
function commitRoot() {
// TODO add nodes to dom
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
//★「nextUnitOfWork」は「wipRoot」と同じオブジェクトを参照しているため、「nextUnitOfWork」の変更は「wipRoot」にも適用される!
//「nextUnitOfWork」は最終的にundefinedになるが、「wipRoot」は常にrootを指す。
//この時点で「wipRoot」は子ども、子孫たちはfiber上では繋がっているが、DOM上では繋がっていない。
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
//★【②】「次の仕事が無くなったら(undefined)」、一番最後にcommitRoot()関数を実行
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
//一番最後に1度だけ実行される
function commitRoot() {
commitWork(wipRoot.child)
//作業が完了したから、nullになる
wipRoot = null
}
//↑のcommitRoot()関数により、最後に1度だけ実行される
function commitWork(fiber) {
//以下のif文は、「子要素、兄弟要素がないときは、関数を実行しないで」という意味
if (!fiber) {
return
}
const domParent = fiber.parent.dom
//createElement()関数でアイドル時間でelementを作り、appendChild()は一気にすることで、画面表示の不具合を解消する
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
//★commitWork()関数をわかりやすく書いてみると
function commitWork(fiber) {
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
//childがあったら、commitWork(fiber.child)を実行してね(無かったら実行しないでね)
if (fiber.child) {
commitWork(fiber.child)
}
//siblingがあったら、commitWork(fiber.sibling)を実行してね(無かったら実行しないでね)
if (fiber.sibling) {
commitWork(fiber.sibling)
}
}
##ステップ6:調整
これまではDOMに要素を追加するだけでしたが、ノードの更新または削除についてはどうですか?
これが今から行うことです。
render関数で受け取った要素を、DOMにコミットした最後のファイバーツリーと比較する必要があります。
function commitRoot() {
commitWork(wipRoot.child)
//★【変更点①】wipRootはすべてappendしたあとの最終形(=dom:containerに入れた最終形)
currentRoot = wipRoot
//更新していくため、wipRootをnullで上書きしている(currentRootにはwipRootのコピーした値を入れたため、nullにはならない)
wipRoot = null //nullはプリミティブ型。プリミティブ型は値のコピーを保持するよ。
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
//★【変更点②】「alternate:代替」currentRootの初期値は「null」。
//commitRoot()関数の「currentRoot = wipRoot」ですべてappendしたあとの最終形が入るよ。(前回の要素)
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
//★【変更点③】currentRootの初期値をnullに設定
let currentRoot = null
let wipRoot = null
「alternate: currentRoot」の初期値は「null」。
render()関数は一番最初のwipRootnodeをつくるための関数のため、初期値は「null」。
最終的にcommitRoot()関数によって、currentRootに完成したwipRoot(fiberのrootnode)が代入される。
「currentRoot」は、以前のコミットフェーズでDOMにコミットした古いファイバーへのリンクとなる。
次に、fiberを生成する部分をperformUnitOfWork()関数から切り出して、reconcileChildren()関数を実装する。
//以下前回と変わらない参照コード
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
上記の前回作成したperformUnitOfWork()関数から新しいファイバーを作成するコードを抽出してみましょう
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
//★children(props)は[a,b]と串刺しになっているが、child(fiber)は[a]のみ、[b]はsiblingになる。
const elements = fiber.props.children
reconcileChildren(fiber, elements)
//次の仕事を返す部分はそのままperformUnitOfWork()関数に置いておく
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
//fiberを作る作業をreconcileChildren()関数へ移している
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
let newFiber = null
if (index === 0) {
wipFiber.child = newFiber
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
【参考】
performUnitOfWork()関数の処理の流れ一覧
それでは、reconcileChildren()関数にて、古いfiberと新しい要素を調整します。
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
// wipFiber.alternateは、前回の同じ要素部分
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
//①現在の作業中のfiber
index < elements.length ||
//②oldfiber(①か②どちらも存在する場合true)
oldFiber != null
) {
const element = elements[index]
let newFiber = null
// TODO compare oldFiber to element
const sameType =
oldFiber && //不要
element && //不要
element.type == oldFiber.type
//前回と同じタイプだが、props部分を確認する必要あり。
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber, //前回の要素部分
effectTag: "UPDATE",
}
if (element && !sameType) {
// TODO add this node
}
//上記でelementを作成するなら、oldFiberを消さなければならない
if (oldFiber && !sameType) {
// TODO delete the oldFiber's node
}
削除するために、render()関数で、wipRootの初期値に「deletions」(削除するノードを追跡するための配列)を追加。最後にこの配列の値を消すよ。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
//【★追加①】
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
//【★追加②】
let deletions = null
function commitRoot() {
//★【追加①】一番最後にDOMをcommitする前に、削除すべきoldFiberを削除
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
//【追加①】typeが違うから作り直す(PLACEMENT)
//【重要】domはずっと使いまわしているため、今回も前回もdom、parentDomは同じ。
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
//【追加②】同じ要素だったら、属性などを更新するやつ(UPDATE)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
//【追加③】削除だったら、親から該当のoldfiberを消してね
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
同じ要素だったら、属性などを更新する、
updateDom()関数を実装してみよう!
const isProperty = key => key !== "children"
//isNewは高階関数(矢印が2つ)。返り値は関数が返る。
const isNew = (prev, next) => key =>
prev[key] !== next[key]
//消えたpropsを空文字にするための処理
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
propsにイベントがあった場合の処理も追加しよう!
//「onChange」は「addEventListener("change",)...」のこと。「on」で残った単語はaddEventListener関数の第一引数となる。
//例:props: { onChange: this.handleChange }
//this.handleChangeは関数
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
イベントリスナーを削除する処理を書いていこう!
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) || //前にはあって、nextにはない
isNew(prevProps, nextProps)(key) //前はあって、nextにもある=>同じコールバック関数かを判定したい(イベントの名前しか比較出来ていないから)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
//コールバック関数の比較
//nameはonChangeなど
.forEach(name => {
const eventType = name
.toLowerCase() //全部を小文字に変換
.substring(2) //onをカット
dom.removeEventListener(
eventType,
prevProps[name] //コールバック関数
)
})
イベントリスナーを追加する処理を書いていこう!
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))//nextPropsから追加すべきイベントハンドラーをとってくる=>イベントから新たに追加されたイベントだけが残る
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
//createDomは初回のみ実行されるため、2回目以降は初回に作ったdomを使い回す
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
//【★更新】初回だけ実行する。初回はprevがないため、{}を渡す
updateDom(dom, {}, fiber.props)
return dom
}
##ステップ7:関数コンポーネント
次に追加する必要があるのは、関数コンポーネントのサポートです。
最初に例を変更しましょう。
h1要素を返すこの単純な関数コンポーネントを使用します。
jsxをjsに変換すると、次のようになります。
//jsx
/** @jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
//今回はtypeに関数を入れている
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
//js
//Didact.createElement()関数で仮想node(VNode)を作って返している
function App(props) {
return Didact.createElement(
"h1", //type
null, //DOMに入れる属性とイベント
"Hi ", //子要素
props.name //子要素
)
}
//Didact.createElement(type,props)
//今回はtype(HTMLのタグの名前)に関数を入れている
const element = Didact.createElement(App, {
name: "foo",
})
//仮想nodeの実態
{
type: "h1",
props: {
children: ["Hi", props.name]
},}
####【★かひろ塾★】
JSXの変換ルールで、
文字列・変数・関数関係なく、もしそのシンボル(変数/定数)がそのスコープから参照できる場合、それをcreateElementの第一引数に当てはめるルールがある。
createElementの第一引数は、文字列、変数、関数のどれか。
自分で作るオリジナルなタグは、一般的なHTMLの名前を使わない。
Reactでは、実行するとVNodeが返却される関数はすべて
最初の文字はすべて大文字にする。
//例
//levelの変数宣言をしていた場合
let level = `h${1}`
<level>hello</level>
createElement(level
//levelの変数宣言をしていない場合
//let level = `h${1}`
<level>hello</level>
createElement("level"
Reactは関数を実行した結果、必ずVNode(仮想node)を返すように期待する。
また、**グループ(部品:VNode)を返す関数をコンポーネント(関数記法のコンポーネント)**と呼ぶ。
VNodeをrender()メソッドで返すクラス(クラス記法のコンポーネント)もコンポーネントと呼ばれる。
元々はクラスだけだったが、便利な関数(createElement()関数の第一引数に関数を入れる)の書き方が後から導入されたらしい。
元々はcreateElement()関数の第一引数には、
HTMLのタグ名もしくはクラスを入れていた。
通常、VNodeを作る場合は、公式で提供されている**「createElement()」**に該当する関数を使用するべき。
★ライブラリを使用する側は、①VNode作成部分と②VNodeを実DOMに反映させる関数(Reactの場合:ReactDOM.render()関数)のみを操作する。
//①VNode作成部分の例
const element = (
<div id="foo">
<button onClick={() => console.log("clicked!!!")}>click me</button>
</div>
)
//②VNodeを実DOMに反映させる関数部分
ReactDOM.render(element, container)
関数コンポーネントは、次の2つの点で異なります。
- 関数はtypeが関数だから、実DOMを持っていない。(関数の返り値が実DOMを持っている)関数コンポーネントのfiberにはDOMノードがありません(関数の実行結果は必ずVNode。functionのときは、VNodeを返すよう記述する)
- 今まではマークアップした通りにVNodeの構造が決定されていたが、関数にChildrenを当てはめる場合は最終的な結果がそのとおりになるとは限らない。引数propsのChildrenの使い方は、関数が決定する。関数によっては、propsを並べ替えるようなことができるらしい。
次に、ファイバータイプが関数かどうかを確認し、それに応じて別の更新関数に移動します。
関数かどうかを判定する方法は、JSだと有名な方法が2種類ある。
typeof () => {} === 'function' // 1
() => {} instanceof Function // 2
typeofだと、プリミティブもしくはそうでないかまで判断可能。
instanceofだと、どのコンストラクターから生成されたオブジェクトなのかが判断可能。
オブジェクトの種類を判定したい場合に「instanceof」を使うらしい。
★document.createElement()関数の第一引数にはHTMLのタグ名しか入れられないため、関数による分岐を作成している!
//performUnitOfWork()関数は、workLoop()関数で呼ばれ、1回呼ばれるごとに最大1つしかdomを作らない関数
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
//typeof fiber.type === "function"でも可
if (isFunctionComponent) {
//typeがfunctionだった場合の処理
updateFunctionComponent(fiber)
} else {
//typeが文字列だった場合の処理
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function updateFunctionComponent(fiber) {
//childrenには、App()関数の実行結果であるVNodeが代入される。functionの場合、childrenは要素1つの配列。
const children = [fiber.type(fiber.props)]
//childとsiblingのfiberを生成するreconcileChildren()関数を利用するために、上記でchilrenを配列に入れている。
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
変更する必要があるのは、実domを繋げていくcommitWork関数です。
**DOMノードのないファイバー(typeof "function")**ができたので、2つの点を変更する必要があります。
まず、DOMノードの親を見つけるには、DOMノードを持つファイバーが見つかるまでファイバーツリーを上に行く必要があります。(commitWork()関数はあくまでも作られた実DOMをつなげる関数だから)
上記図のように、関数の場合は実DOMがないため、処理を飛ばされて、DOMノードを持つファイバーが見つかるまで上に行く。(関数は無視される)
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
//もしparentが実domを持っていなかったら(functionだったら)、parentをparentのparentにする(実domがあるまでwhileで繰り返す)
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
また、ノードを削除するときは、DOMノードを持つ子が見つかるまで続ける必要があります。
} else if (fiber.effectTag === "DELETION") {
//★【変更点①】新たにcommitDeletion()関数を追加
commitDeletion(fiber, domParent)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
//★【変更点②】commitDeletion()関数を追加
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
//fiberにdomがなかったら(functionだったら)、fiber.childでその下に移動する
commitDeletion(fiber.child, domParent)
}
}
★function自体に実domがなく、実行結果に実domがあるため、関数の返り値は必ずVNode(仮想node)である必要がある!
関数の返り値がVNodeである理由は、
ReactはVNodeを作っていくものだから、VNodeを返さないと意味がない!
##ステップ8.フック
最後のステップ♪
関数コンポーネントがあるので、状態も追加しましょう。
これまでは差分更新で、全体を見ていたが、
Didact.useState()関数が実装されると、
そのコンポーネント(VNodeを返す関数)範囲でのVNodeの簡単な変更が可能となる!
Reactのルールで、Didact.useState()関数は
コンポーネント(VNodeを返す関数)の中でしか使えない!
フックスとは、関数記法のコンポーネント専用の関数のこと。
Reactのライブラリの関数の大半は、useState()関数(最小構成要素)が元となっている!
const Didact = {
createElement,
render,
//★【追加点①】 useStateを追加
useState,
}
//★【追加点②】
/** @jsx Didact.createElement */
function Counter() {
//Didact.useState(1)の返り値は[値,値を更新する関数]となり、それぞれを分割代入している
const [state, setState] = Didact.useState(1)
return (
//★JSXにJSの値を紐付ける場合は、「{}」で囲う!
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
//★【追加点③】
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
ここから、useState()関数を自分で実装してみよう!
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
関数内で使用できるように、関数コンポーネントを呼び出す前にいくつかのグローバル変数を初期化する必要があります。
最初に、作業中fiber(wipFiber)を設定します。
また同じコンポーネントの中で、useState()関数を複数回呼べるようにするために、「fookIndex」を用意している。
//wipFiber:作業中のfiber
let wipFiber = null
//hookIndex:useState()関数の呼び出し順序と対応
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
//★functionのfiberは実domを持っていない代わりに、hooksプロパティを持てる
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
useState()関数に乱数を使用した場合、hookIndexが機能しなくなるため、乱数の使用は禁止!if文の中useState()関数で使うのも禁止!
ただし、if文の中で、setState()関数を使うのはOK!
//例
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
関数コンポーネントを呼び出すとき、useStateに古いフックがあるかどうかを確認します。
alternateフックインデックスを使用してファイバーをチェックインします。
古いフックがある場合は、状態を初期化しなければ、古いフックから新しいフックに状態をコピーします。
次に、新しいフックをファイバーに追加し、フックインデックスを1つ増やし、状態を返します。
function useState(initial) {
const oldHook =
//undefinedが出るとエラーになるため、細かく条件を設定している
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
//古い値があれば使い、無かったらuseState()関数の引数であるinitial(数字)を使う
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
ユーザーのアクションがあった時やdocument.addEventListener()関数実行時などに利用できるsetState()関数を実装する。
useState()関数は、状態を更新する関数を返す必要があるため、setState()関数、アクションを受け取る関数を定義します。
setState()関数には、以下のように関数を引数に渡している。
<h1 onClick={() => setState(c => c + 1)}>
const hook = {
state: oldHook ? oldHook.state : initial,
//★【追加点①】queue(キュー)は先入れ先出しを実現するためのデータ構造体
queue: [],
}
//★【追加点②】
const setState = action => {
hook.queue.push(action)
//currentRootには最後のwipRootが入っている。
//setState()関数を実行すると、画面が更新されるため、
//画面を更新するためにwipRoot(作業中のfiber)を再度作成
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
//これから更新されるから、alternateには最後のwipRoot(currentRoot)を入れている
alternate: currentRoot,
}
//nextUnitOfWorkに値を入れると、performUnitOfWork()関数(実domを1つずつ作る関数)が実行される!
nextUnitOfWork = wipRoot
//setState()関数を実行した影響により、コンポーネントが返却するVNodeの構造に変化が出る場合可能性がある。
//その影響により削除されるVNodeも出てくる。(例:h1→h2になる)
//deletionsは削除されるVNodeを入れる配列
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
//★【追加点③】useState()関数の戻り値として、hook.stateの値とsetState()関数を返している。
return [hook.state, setState]
}
```
しかし、**stateの値を算出する部分**を書いていません。書いてみよう!
```react
//★【追加点①】
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
上記で「hook.state」を更新することにより、
useState()関数のreturn値である[hook.state]が意図した値で返る!
▼全体像
https://whimsical.com/Dv1pgPinhqpPX8UVVcnFYy
▼完成コード
//didact 「完全版」
//(0番目)Diact.createElement関数を実装
//createElement()関数の返り値が「仮想node:画面の設計図」
//createElement()関数はJSX記法で書かれた部分すべてで呼ばれる
function createElement(type, props, ...children) {
return {
type,
props: { //propsには「属性」「イベントハンドラー」「children」が入ってくる
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
//hooksが追加される
}
}
//⑤初回のdom生成時だけ実行する。2回目以降は初回に作ったdomを使い回す
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)//document.createElement()関数の第一引数はタグ名のみ!
//初回のdom生成時だけ実行する。初回はprevがないため、{}を渡す(2回目以降はdomを使い回す)
updateDom(dom, {}, fiber.props)
return dom
}
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
//isNew()関数は関数を返す高階関数
const isNew = (prev, next) => key =>
prev[key] !== next[key]
//以下引数の「pre」は使用されていないが、消すと引数の順番が変わるため全体的に変更が必要となる
const isGone = (prev, next) => key => !(key in next)
//⑨updateDom()関数は唯一、実DOMを操作する関数。
//updateDom()関数を使う場面
//(1)新たな作成されたまっさらな実DOMに対して、設定されている属性(props)とイベントを適用させる
//(2)すでに作成されている実DOMに対して、前回設定されている属性(Props)/イベントと今回作成された属性/イベントを比較し、最小限の実DOMへのアクセスで更新する
//引数:fiber.dom,fiber.alternate.props,fiber.props
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||//新しいpropsにはない=>前のイベント削除
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2) //最初の2文字削除
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps)) //新しいpropsにない=>前のプロパティ削除
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))//新しく追加
.forEach(name => {
dom[name] = nextProps[name]
})
}
//⑦
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)//wipRoot(container)は変わらないため、その次にrootに近い「wipRoot.child」を引数に渡している
currentRoot = wipRoot
wipRoot = null //すべての処理が完了!
}
//⑧commitWork()関数で、実DOMをつなげて反映させる。
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
//もしparentが実domを持っていなかったら(functionだったら)、
//parentをparentのparentにする(実domがあるまでwhileで繰り返す)
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (
//typeが違うから作り直す(PLACEMENT)
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
//同じ要素だったら、属性などを更新するやつ(UPDATE)
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
//commitDeletion()関数を実行
commitDeletion(fiber, domParent)
}
//「wipRoot.child」の子要素、兄弟要素も変更点をチェックしたいため、引数に入れて実行
//引数fiberとwipRootは同じ(呼び方が違うだけ)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
//commitDeletion()関数を追加
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
//fiberにdomがなかったら(functionだったら)、fiber.childでその下に移動する
commitDeletion(fiber.child, domParent)
}
}
//②初回のみ実行
//render()関数は、mountTarget(container)をdomプロパティに持つfiber(rootのfiber)を作る関数
//表面では、render()関数は、画面に要素を反映させる関数(内部的には様々な関数のトリガー関数である)
//render()関数の引数の「element」はVNodeのこと。
//VNodeと実DOMの要素を引数でもらい、貰った実DOM(container)にVNodeを反映させるために一度「Fiber」という構造体に
//変換して、wipRoot(Fiber)に配置する。
//★render()関数の引数であるelement(VNode)とcontainer(実DOM)を結びつけるために「fiber」という構造体を作っている
function render(element, container) {
wipRoot = {
dom: container, //rootのdomは「container」
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
//③
function workLoop(deadline) {
//shouldYieldは「中断すべきか」
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
//「nextUnitOfWork」には、render()関数で作ったwipRootが入っている
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
//「deadline」はどのくらいの時間作業しても良いのかが入っている
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
//render()関数を実行した後、workLoop()関数が実行される
requestIdleCallback(workLoop)
//④performUnitOfWork()関数は、workLoop()関数で呼ばれ、1回呼ばれるごとに最大1つしかdomを作らない関数
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function //typeof fiber.type === "function"でも可
if (isFunctionComponent) {
//typeがfunctionだった場合の処理
updateFunctionComponent(fiber)
} else {
//typeが文字列だった場合の処理
updateHostComponent(fiber)
}
if (fiber.child) {
//reconcileChildren()関数を実行したから「fiber.child」がtrueになる
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
//returnなしで、while (nextFiber){}に移る
nextFiber = nextFiber.parent
}
}
//wipFiber:作業中のfiber
let wipFiber = null
//hookIndex:useState()関数の呼び出し順序と対応
let hookIndex = null
//fiberのtypeがfunctionの場合、performUnitOfWork()関数の中で呼ばれる
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
//★functionのfiberは実domを持っていない代わりに、hooksプロパティを持てる
//hooksにはhookのオブジェクトが実行順に格納される
wipFiber.hooks = []
//childrenには、App()関数の実行結果であるVNodeが代入される。functionの場合、childrenは要素1つの配列。
const children = [fiber.type(fiber.props)]
//childとsiblingのfiberを生成するreconcileChildren()関数を利用するために、上記でchilrenを配列に入れている。
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
//undefinedが出るとエラーになるため、細かく条件を設定している
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
//古い値があれば使い、無かったらuseState()関数の引数であるinitial(数字)を使う
state: oldHook ? oldHook.state : initial,
//★queue(キュー)は先入れ先出しを実現するためのデータ構造体
//queueの配列には、setState()関数のactionを入れる
queue: [],
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
//currentRootには最後のwipRootが入っている。
//setState()関数を実行すると、画面が更新されるため、
//画面を更新するためにwipRoot(作業中のfiber)を再度作成
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
//これから更新されるから、alternateには最後のwipRoot(currentRoot)を入れている
alternate: currentRoot,
}
//nextUnitOfWorkに値を入れると、performUnitOfWork()関数(実domを1つずつ作る関数)が実行される!
nextUnitOfWork = wipRoot
//setState()関数を実行した影響により、コンポーネントが返却するVNodeの構造に変化が出る場合可能性がある。
//その影響により削除されるVNodeも出てくる。(例:h1→h2になる)
//deletionsは削除されるVNodeを入れる配列
deletions = []
}
//hookがstateの値とsetState()のactionの2つを持ち、
//hooksはhookのオブジェクトが実行順で格納される場所
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
//fiberのtypeが文字列の場合、performUnitOfWork()関数の中で呼ばれる
function updateHostComponent(fiber) {
//初回のみcreateDom()関数を実行。2回目以降の更新はdomを使い回す。
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
//⑥
//引数「wipFiber」は現在のfiber
//reconcileChildren()関数は、childとsiblingのfiberを生成する関数
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
index < elements.length || //新しいfiberがあるか
oldFiber != null //前回のfiberがあるか
) {
const element = elements[index]
let newFiber = null
// TODO compare oldFiber to element
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
//typeが同じ場合
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
//typeが違う場合、新しくfiberを作り直す
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null, //createDom(fiber)でdomが更新されるから初期値は「null」
parent: wipFiber,
alternate: null, //前回のものは削除するから「null」
effectTag: "PLACEMENT",
}
}
//typeが違う場合、古いfiberを削除する
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
//Elementsと同じようにoldFiberも1つずつ右にズレていく(while文の中で)
if (oldFiber) {
oldFiber = oldFiber.sibling //★なぜchildではない?=>Elementsで横に移動するから
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const Didact = {
createElement,
render,
useState,
}
/** @jsx Didact.createElement */
function Counter() {
//Didact.useState(1)の返り値は[値,値を更新する関数]となり、それぞれを分割代入している
const [state, setState] = Didact.useState(1)
return (
//★JSXにJSの値を紐付ける場合は、「{}」で囲う!
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
// <Counter />だけだと関数は呼ばれない。updateFunctionComponent()関数でしかCounter()関数は呼ばれない
const element = <Counter />
const container = document.getElementById("root")
//①
Didact.render(element, container)