创做本篇博客的初衷是,在浏览社区时发现了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中保存了该词法环境中的引用,造成了闭包,因此词法环境仍然会存在一段时间。