創做本篇博客的初衷是,在瀏覽社區時發現了pomb.us/build-your-…這篇寶藏文章,該博主基於react16以後的fiber架構實現了一套react的簡易版本,很是有助於理解react工做原理。可是苦於只有英文版本,且偏向理論。html
本着提高自我、貢獻社區的理念。在此記錄下學習歷程,並盡本身微薄之力對重點部分(結合本身理解)進行翻譯整理。但願對你們有所幫助。node
建立項目(本身命名),下載文件包react
$ mkdir xxx
$ cd xxx
$ yarn init -y / npm init -y
$ yarn add react react-dom
複製代碼
創建以下目錄結構npm
- src/
- myReact/
- index.js
- index.html
- main.jsx
複製代碼
初始化文件內容json
//index.html
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>React App</title> </head> <body> <div id="root"></div> <script src="main.jsx"></script> </body> </html>
// main.jsx
import React from "react";
import ReactDom from "react-dom";
const App = () => {
return <div title="oliver">Hello</div>;
};
ReactDom.render(<App />, document.getElementById("root"));
// myReact/index.js
export default {}
複製代碼
安裝 parcel 用於打包和熱更新數組
$ yarn add parcel-bundler
複製代碼
// main.jsx
const element = (
<div id="foo"> <a>Hello</a> <span /> </div>
)
複製代碼
通過babel轉譯後的效果(使用plugin-transform-react-jsx
插件,www.babeljs.cn/docs/babel-…):瀏覽器
const element = React.createElement(
"div", //type
{ id: "foo" }, //config
React.createElement("a", null, "bar"), //...children
React.createElement("span")
)
複製代碼
plugin-transform-react-jsx
作的事情很簡單: 使用 React.createElement
函數來從處理.jsx文件中的jsx語法。import React from "react"
的緣由啦,不然插件會找不到React對象的!tips:筆者原本也打算使用 plugin-transform-react-jsx
插件,可是在調試中遇到了問題。查找後才知道最新版本的插件已經再也不是由 <h1>Hello World</h1>
到 React.createElement('h1', null, 'Hello world')
的簡單轉換了(具體見zh-hans.reactjs.org/blog/2020/0…),故退而求其次選擇了功能相似的 transform-jsx
bash
$ touch .babelrc
$ yarn add babel@transform-jsx
複製代碼
// .babelrc
{
"presets": ["es2015"],
"plugins": [
[
"transform-jsx",
{
"function": "React.createElement",
"useVariables": true
}
]
]
}
複製代碼
$ parcel src/index.html
複製代碼
此時頁面中能夠看到Hello字樣,說明咱們配置成功了!babel
transform-jsx
插件會將參數封裝在一個對象中,傳入createElement。markdown
// myReact/index.js
export function createElement(args) {
const { elementName, attributes, children } = args;
return {
type:elementName,
props: {
...attributes,
children
}
};
}
複製代碼
考慮到children中還可能包含基本類型如string,number。爲了簡化操做咱們將這樣的children統一使用 TEXT_ELEMENT
包裹。
// myReact/index.js
export function createElement(type, config, ...children) {
return {
type,
props: {
...attributes,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
}
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
export default { createElement }
複製代碼
React並不會像此處這樣處理基本類型節點,但咱們這裏這樣作:由於這樣能夠簡化咱們的代碼。畢竟這是一篇以功能而非細節爲主的文章。
首先爲咱們本身的庫起個名字吧!
// .babelrc
{
"presets": ["es2015"],
"plugins": [
[
"transform-jsx",
{
"function": "OllyReact.createElement",
"useVariables": true
}
]
]
}
複製代碼
引入時就使用本身寫的名字吧!
// main.jsx
import OllyReact from "./myReact/index";
import ReactDom from "react-dom"
const element = (
<div style="background: salmon"> <h1>Hello World</h1> <h2 style="text-align:right">—Oliver</h2> </div>
);
ReactDom.render(element, document.getElementById("root"));
複製代碼
此時頁面上已經出現了Hello
, 這證實咱們的React.createElement已經基本實現了React的功能。
接下來編寫render函數。
目前咱們只關注向DOM中添加內容。修改和刪除功能將在後續添加。
// React/index.js
export function render(element, container) {}
export default {
//...省略
render
};
複製代碼
本小節每一步內容主要參考思路便可,詳細的邏輯順序會在底部彙總。
首先使用對應的元素類型建立新DOM節點,並把該DOM節點加入股container中
const dom = document.createElement(element.type)
container.appendChild(dom)
複製代碼
而後遞歸地爲每一個child JSX元素執行相同的操做
element.props.children.forEach(child =>
render(child, dom)
)
複製代碼
考慮到TEXT節點須要特殊處理
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
複製代碼
最後將元素的props分配給真實DOM節點
Object.keys(element.props)
.filter(key => key !== "children") // children屬性要除去。
.forEach(name => {
dom[name] = element.props[name];
});
複製代碼
彙總:
export function render(element, container) {
const dom = element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
Object.keys(element.props)
.filter(key => key !== "children")
.forEach(name => {
dom[name] = element.props[name];
});
element.props.children.forEach(child =>
render(child, dom)
);
container.appendChild(dom);
}
複製代碼
// main.jsx
import OllyReact from "./myReact/index";
const element = (
<div style="background: salmon"> <h1>Hello World</h1> <h2 style="text-align:right">—Oliver</h2> </div>
);
OllyReact.render(element, document.getElementById("root"));
複製代碼
此時看到咱們的render函數也能夠正常工做了!
就是這樣!如今,咱們有了一個能夠將JSX呈現到DOM的庫(雖然它只支持原生DOM標籤且不支持更新 QAQ)。
實際上,以上的遞歸調用是存在問題的。
所以React16的concurrent模式實現了一種異步可中斷的工做方式。它將把工做分解成幾個小單元,完成每一個單元后,若是須要執行其餘任何操做,則讓瀏覽器中斷渲染。
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// todo
}
複製代碼
requestIdleCallback
來作一個循環。能夠將其requestIdleCallback
視爲一種異步任務,瀏覽器將在主線程空閒時運行回調,而不是告訴咱們什麼時候運行。requestIdleCallback
還爲咱們提供了截止日期參數。咱們可使用它來檢查瀏覽器須要再次控制以前有多少時間。performUnitOfWork
函數。要求它不只執行當前工做單元,而且要返回下一個工做單元。爲了組織工做單元的結構,咱們須要一棵 Fiber
樹。
rootFiber
節點,並將它做爲第一個 nextUnitOfWork(a instance of Fiber)
傳入performUnitOfWork
接受 nextUnitOfWork
做爲參數並作三件事:
這樣的數據結構的目的就在於更方便地找到下個工做單元:
fiber.child!==null
,則 fiber.child
節點將是下一個工做單元。fiber.sibling!==null
的狀況下, fiber.sibling
節點將是下一個工做單元。fiber.child===null && fiber.sibiling===null
的狀況下,fiber.parent
節點的 sibling
節點將是下一個工做單元。// 將render方法中建立DOM元素的邏輯抽離出來
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節點中初始化rootFiber根節點
export function render(element, container) {
nextUnitOfWork = { //rootFiber
dom: container,
props: {
children: [element]
},
}
}
function workLoop() {...}
function performUnitOfWork(){
//todo
}
requestIdleCallback(workLoop)
複製代碼
改造完成後而後,當瀏覽器準備就緒時,它將調用咱們workLoop
,咱們將開始在根目錄上工做。
function performUnitOfWork() {
//******** 功能1:建立dom ********
if (!fiber.dom) { //爲fiber節點綁定dom
fiber.dom = createDom(fiber);
}
if (fiber.parent) { //若存在父節點,則掛載到父節點下
fiber.parent.dom.appendChild(fiber.dom);
}
}
複製代碼
function performUnitOfWork() {
...
//******** 功能2:爲jsx元素的children建立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爲children
fiber.child = newFiber;
} else { //其餘子fiber依次用sibling做鏈接
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
複製代碼
function performUnitOfWork() {
...
//******** 功能3:返回下一個工做單元 ********
if (fiber.child) return fiber.child; //子節點存在,則返回子節點
let nextFiber = fiber;
while (nextFiber) { //子節點不存在則查找兄弟節點 or 父節點的兄弟節點
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
複製代碼
這裏咱們還有一個問題。
因爲每次在處理fiber時,都會建立DOM並插入一個新節點。而且fiber架構下的渲染是可打斷的。這就形成了用戶有可能看到不完整的UI。這不是咱們想要的。
所以咱們須要刪除插入dom的操做。
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
const elements = fiber.props.children
}
複製代碼
相反地,咱們追蹤 Fiber Tree
的根節點,稱之爲wipRoot
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
複製代碼
在 workLoop
完成後(不存在 nextUnitOfWork
),則使用 commitRoot
向 renderer
提交整棵 Fiber
樹。
function workLoop() {
...
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
...
}
複製代碼
使用commitWork來處理每個工做單元
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
複製代碼
到如今爲止咱們只實現了添加DOM,那麼如何更新或刪除呢?
這就是咱們如今要作的:對比在render函數中接收的Fiber樹與上一次提交的Fiber樹的差別。
因此咱們須要一個指針,指向上一次的Fiber樹,不如稱之爲 currentRoot
。
let currentRoot = null
function commitRoot() {
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
複製代碼
在每一個fiber節點數上,增長一個alternate屬性,指向舊的fiber節點。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
複製代碼
從performUnitOfWork中提取建立 Fiber
節點的代碼,抽離成 reconcileChildren
方法。
在此方法中,咱們將新jsx元素與舊Fiber節點進行 diff
。
function reconcileChildren(fiber, elements) {
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爲children
fiber.child = newFiber;
} else { //其餘子fiber依次用sibling做鏈接
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
複製代碼
接下來是diff的詳細過程,這裏再也不贅述。
目標:
import OllyReact from "./myReact/index";
const App = () => {
const element = (
<div style="background: salmon"> <h1>Hello World</h1> <h2 style="text-align:right">—Oliver</h2> </div>
);
return element;
};
OllyReact.render(<App/>, document.getElementById("root"));
複製代碼
函數組件與原生組件的主要區別:
Fiber
節點上 Fiber.dom
爲nullchildren
須要執行函數組件才能獲得,而不是直接從props裏獲取function performUnitOfWork() {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
...
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]; // 經過執行函數組件,得到jsx元素
reconcileChildren(fiber, children);
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
function commitWork() {
...
let domParentFiber = fiber.parent; //向上遍歷,直到找到帶有fiber.dom的父Fiber
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom
}
function commitDeletion(fiber, domParent) { //在刪除節點時,咱們還須要繼續操做,直到找到帶有DOM節點的子節點爲止。
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
複製代碼
經典的計數器
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}> Count: {state} </h1>
)
}
const element = <Counter />
複製代碼
let wipFiber = null //當前workInProgress Fiber節點
let hookIndex = null //hooks下標
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = [] //爲每一個fiber節點單獨維護一個hooks數組
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
複製代碼
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial, //存在舊值則使用舊值,不然使用初始值。
queue: []
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => { //遍歷舊hooks.queue中的每一個action,依次執行
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
wipRoot = { // 切換fiber tree
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot //從新設定nextUnitOfWork,觸發更新。
deletions = []
}
wipFiber.hooks.push(hook) //向hooks中push進當前的useState調用
hookIndex++ // hooks數組下標 +1 , 指針後移
return [hook.state, setState]
}
複製代碼
從本小節,咱們能夠獲得一些關於hooks的啓發。
爲何hooks不能寫在 if
中?
在本例中:由於每個hook都按照調用順序被維護在fiber節點上的hooks數組中。若某個hooks在 if
語句中,則可能會打亂數組應有的順序。這樣會致使hook的對應出錯。
在react中:使用next指針將hook串聯起來,這種狀況下一樣是不能容忍順序的打亂的。
type Hooks = {
memoizedState: any, // 指向當前渲染節點 Fiber
baseState: any, // 初始化 initialState, 已經每次 dispatch 以後 newState
baseUpdate: Update<any> | null,// 當前須要更新的 Update ,每次更新完以後,會賦值上一個 update,方便 react 在渲染錯誤的邊緣,數據回溯
queue: UpdateQueue<any> | null,// UpdateQueue 經過
next: Hook | null, // link 到下一個 hooks,經過 next 串聯每一 hooks
}
複製代碼
capture Value特性
capture Value沒什麼特別的。它只是個閉包。
每一次觸發rerender,都是去從新執行了函數組件。則上次執行過的函數組件的詞法環境應當被回收。可是因爲useEffect等hooks中保存了該詞法環境中的引用,造成了閉包,因此詞法環境仍然會存在一段時間。