「手写系列」从 0 到 1 实现 Micro React
前言
本文基于React 16.8
实现一个micro
版本的React
,利于快速理解 React
的源码设计思想以及熟悉React
的工作流程,对阅读React
源码有一定帮助,预计阅读用时约30分钟。
在开始实现 micro react
之前,先来看一段代码:
const element = <h1 title='world'>Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container);
相信作为React
的使用者,你已经接触过JSX
。如果你还不了解他,可以看下官网对于JSX的解释。
通过 BABEL 编译 JSX Demo, 可以看到BABEL
会把JSX
转译成一个名为 React.createElement()
函数调用。
// 转换前
const element = <h1 title='world'>Hello</h1>
// 转换后
"use strict";
const element = /*#__PURE__*/React.createElement("h1", {
title: "world"
}, "Hello");
打印 element
可以在控制台看到一个包含 props
、type
等属性的对象。
我们尝试不用 ReactDom.render()
去将 element
挂载到id
为root
的容器中。
const container = document.getElementById("root");
const node = document.createElement(element.type);
node.title = element.props.title;
const text = document.createTextNode("");
text.nodeValue = element.props.children;
node.append(text);
container.append(node);
浏览器查看发现 element
成功挂载,在浏览器中正常显示。
到此可以得知,createElement
会创建一个描述 DOM节点
的对象即虚拟DOM
, render
会将生成的虚拟DOM
挂载到指定 DOM节点
中进行渲染。
本文将从createElement && render
入手逐步实现一个micro react
。
代码地址:lazylwz/micro-react
初始化项目
yarn create vite // 项目模板选择 原生js 或者 react 都行
yarn
yarn dev
在根目录下新增 React 文件夹,将无用文件进行清理,最终项目结构如下
createElement && render
新建createElement.js
文件,编写createElement
函数,入参分别为节点类型(type)
、节点属性(props)
、子节点(children)
。
在构造虚拟DOM树的过程中,若children
的值的类型为基本类型如String
Number
等,直接将该值渲染即可,将此类节点类型赋值为TEXT_ELEMENT
用来区分createElement
创建的节点,在render
遍历虚拟DOM树的过程中可直接渲染,无需递归遍历该类型节点。
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
};
const createTextElement = (text) => {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [], // 无子节点
},
};
};
export default createElement;
// 转换前
const element = createElement(
<h1 title="world">
Hello<div>!</div>
</h1>
);
// 转换后
const element = createElement("h1", {
title: "world"
}, "Hello", createElement("div", null, "!"));
打印 element
可以在控制台看到DOM树基本构建完成,接下来就是渲染至浏览器页面。
新建 render.js
文件,编写render
函数,入参分别为element
、container
。
- element:经过
createElement
包装过的虚拟DOM节点 - container:真实的DOM节点
将虚拟DOM渲染成真实DOM节点并挂载需要以下几步:
- 根据节点类型创建对应如元素节点或文本节点;
- 将属性添加到对应节点;
- 递归处理虚拟DOM中的节点;
- 将处理完的节点挂载到真实的DOM节点。
代码描述如下:
const 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);
};
z
export default render;
来看下 render
函数写的是否能够完成渲染
import { createElement, render } from "./react";
const element = createElement(
"h1",
{
title: "world",
},
"Hello",
createElement("div", null, '!')
);
const container = document.getElementById("root");
render(element, container);
显示效果达成预期,至此简单实现了
createElement
render
。
Concurrent Mode && Fiber
在render.js
中,采用递归的方式处理节点渲染,递归过程是不能中断的。如果虚拟DOM树的层级很深,递归会占用线程很多时间,造成页面卡顿,所以需要将同步递归的渲染重构为异步可中断的渲染。
为此借助浏览器 API requestIdleCallback 在浏览器空闲时期被调用,并且在其回调中利用 IdleDeadline
可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态这些特性,实现异步渲染
,再将虚拟DOM树处理成Fiber
树就可完成异步可中断的渲染
。
并发模式处理
处理虚拟DOM树
只在浏览器空闲时进行,不影响浏览器主线程中其他事件如用户事件、动画渲染等事件的执行,需要利用requestIdleCallback
处理。简单描述如下:
// 需要处理的工作单元
let nextUnitOfWork = null;
// 处理当前工作单元,返回下一个需要执行的工作单元
const performUnitOfWork = (work) => {
// TODO:
return work;
};
const workLoop = (deadline) => {
let shouldYield = false; // 不应该交出控制权或不应该停止
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行工作单元
shouldYield = deadline.timeRemaining() < 1; // 检测浏览器是否还有空余时间
}
// 没有空余时间,将工作单元放到浏览器下一次空闲时执行
requestIdleCallback(workLoop);
};
// 初始化,第一次请求
requestIdleCallback(workLoop);
目前浏览器大多是 60Hz(60帧/s),每一帧耗时约为 16.6ms
现在来假设一个场景,nextUnitOfWork
即虚拟DOM树
的渲染时间为100ms
,远远超过16.6ms
,按照并发模式处理页面还是会卡顿。
如果将100ms
这个大的工作单元拆分为10个的小工作单元,每个工作单元花费为10ms
,那么就可以将每个小的工作单元放在requestIdleCallback
中执行,这些工作单元会在浏览器每一帧的空闲时间去执行,空余时间有就继续执行工作单元,没有就处理浏览器自身事件,不会造成渲染阻塞,也就达到了异步渲染的效果。
但是目前还存在问题,拆分后的工作单元如小的虚拟DOM节点
如何知道自身应插入位置呢?如何建立彼此之间的联系呢?
为解决这个问题,React
官方引入了Fiber
。
Fiber
Fiber
通过链表
的形式来记录节点之间的关系,它与传统的虚拟DOM
最大的区别是多加了几个属性:
parent
:指向父节点fiber
child
:指向子节点的第一个fiber
sibling
:指向下一个兄弟节点的fiber
下面是一个虚拟DOM
,每一个DOM
节点对应一个Fiber
对象,DOM
树对应的Fiber
结构如下:
<h1>
<div>
hello
<span>
world !
</span>
</div>
</h1>
可以看出每个fiber
都记录了上一个节点的信息和下一个节点的信息,利用fiber
就可以将一棵完整的虚拟DOM树
转化为基于链表的由fiber
构成的Fiber tree
。
现在将刚构成的 Fiber tree
用刚写的并发模式试运行下,具体过程如下:
- 首先给
requestIdleCallback
传入第一个根fiberh1
先进行渲染工作, 当该fiber
渲染完成时利用deadline.timeRemaining()
检测浏览器是否还有空余时间,如果有空闲时间,那么就从该fiber
中取出child
对应的fiber
继续进行渲染,如果没有空余时间,那么就等到浏览器下一次的空闲时间再继续对该fiber
中的child
进行渲染,该过程一直循环执行,直到没有需要处理的fiber为止。 - 当
h1
div
hello
都处理完时,此时hello
中child
没有指向fiber
,也就是没有child
时,取该fiber
的sibling
中对应的fiber
进行渲染。 - 当
h1
div
hello
span
world!
都处理完时,此时world!
中child
sibling
均没有指向的fiber
,此时开始向上寻找parent
对应的sibling
指向的fiber
,span
div
h1
均没有sibling
指向的fiber
,此时结束所有的渲染工作。
总结:在 Fiber tree
的遍历过程中,先找child
指向的fiber
,当所有的child
均处理完时,开始从向上找parent
对应的sibling
指向的fiber
。
有点类似与二叉树的先序遍历,即'根左右',把左子树节点当作 child
,右子树当作 sibling
的集合,从根节点开始,先遍历左子树,再遍历右子树的第一个节点,当右子树的节点数量大于1时,把右子树的第二个节点开始当作根节点依次处理。
在
render.js
中,因为渲染DOM
的逻辑要频繁使用,所以先将其移出,接着初始化rootFiber
,相关代码如下。
const createDom = (fiber) => {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
Object.keys(fiber.props)
.filter((key) => key !== "children")
.forEach((name) => (dom[name] = fiber.props[name]));
return dom;
};
// 初始化第一个工作单元 rootFiber
const render = (element, container) => {
nextUnitOfWork = {
dom: container,
props: {
children: [element],// 对于 container 来说,element 均为子节点
},
sibling: null,
child: null,
parent: null,
};
};
接着开始写虚拟DOM Tree
构造Fiber Tree
渲染的逻辑。
首先从根节点开始遍历所有的子节点数组,构造fiber
,遍历的过程中,第一个子节点作为 child
,其余子节点均为 sibling
,当前节点fiber
渲染完成后返回下一个工作单元需要的fiber
即当前fiber
的child
,若所有的child
均渲染完成,开始向上处理parent
的sibling
对应的fiber
,若存在sibling
对应的fiber
则把该fiber
当作下一个工作单元返回,当所有的sibling
都渲染结束时,虚拟DOM Tree
利用Fiber Tree
异步渲染的过程就完成了。
const performUnitOfWork = (fiber) => {
// 创建 DOM 元素
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 追加到父节点
if (fiber.parent) {
fiber.parent.dom.append(fiber.dom);
}
// 给children添加fiber
const elements = fiber.props.children;
let prevSibling = null;
// 构建Fiber tree
for (let i = 0; i < elements.length; i++) {
const newFiber = {
type: elements[i].type,
props: elements[i].props,
parent: fiber,
dom: null,
child: null,
sibling: null,
};
// children 数组中第一个节点当作 child ,其余均为 sibling 节点
if (i === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
}
// 返回下一个fiber
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
};
每一个 fiber
均可以找到parent
child
sibling
的信息,浏览器在异步渲染时不会丢失节点信息。
Render and Commit Phases
目前为止,虚拟DOM
的渲染已经被我们改为Fiber Tree
结构进行异步渲染,但是如果此时浏览器动画渲染或处理用户事件时间较长,导致整颗虚拟DOM Tree
未完成全部渲染,浏览器将会渲染不完整的DOM
结构,用户将会看到不完整的UI
,如此例中的helloworld!
只渲染了hello
,对于用户来说是不友好的,我们应该让用户看到完整的UI
,为此需要对根fiber
进行追踪,当整颗Fiber Tree
中的fiber
均处理完时再进行渲染。
改动如下:
- 定义
workInProgressRoot
即wipRoot
对 根fiber
进行存储追踪 - 将原先一个
fiber
处理完就提交至浏览器进行渲染修改为所有的fiber
即Fiber Tree
处理完成再交给浏览器渲染
let nextUnitOfWork = null;
+ let wipRoot = null; // 工作中的 Fiber tree
// 处理完成的 fiber tree 提交至浏览器进行渲染
+ const commitRoot = () => {
+ commitWork(wipRoot.child);
+ wipRoot = null; // 渲染完成进行清空
+ };
+ // 递归处理每个小的 fiber
+ const commitWork = (fiber) => {
+ if (!fiber) return;
+ const domParent = fiber.parent.dom;
+ domParent.appendChild(fiber.dom);
+ if (fiber.child) commitWork(fiber.child);
+ if (fiber.sibling) commitWork(fiber.sibling);
+ };
const workLoop = (deadline) => {
let shouldYield = false; // 不应该交出控制权或不应该停止
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行工作单元
shouldYield = deadline.timeRemaining() < 1; // 检测浏览器是否还有空余时间
}
+ // 如果没有下一个需要处理的 fiber 并且有全部处理完成的需要提交的 wipRoot,就提交至浏览器进行渲染
+ if (!nextUnitOfWork && wipRoot) {
+ commitRoot();
+ }
// 没有空余时间,将工作单元放到浏览器下一次空闲时执行
requestIdleCallback(workLoop);
};
const performUnitOfWork = (fiber) => {
// 创建 DOM 元素
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
- // 追加到父节点
- if (fiber.parent) {
- fiber.parent.dom.append(fiber.dom);
- }
// 给children添加fiber
const elements = fiber.props.children;
let prevSibling = null;
};
Reconciliation
Reconciliation
具体细节可以看官方对于这一节的阐述 协调 – React (reactjs.org)
React 采取Diff
最优解策略的限制条件是
- 只对同级元素进行
Diff
。如果一个DOM节点
在前后两次更新中跨越了层级,那么React
不会尝试复用; - 两个不同类型的元素会产生出不同的树。如果元素由
div
变为p
,React会销毁div
及其子孙节点,并新建p
及其子孙节点; - 开发者指定
key
区分子节点遍历时是否复用。
我们这里简单限制为:
- 元素类型相同:复用节点进行更新
- 元素类型不同:创建新的DOM节点,旧节点存在进行删除
具体比较过程如下
对于fiber
的调度我们需要添加一些属性和全局变量来完成
currentRoot
:全局存储上一个fiber
deletions
:需要删除的fiber
alternate
:上一个fiber
的备份effectFlag
:标识fiber
节点的操作类型,添加(PLACEMENT
)、更新(UPDATE
)或删除(DELETION
)
+ let currentRoot = null;
+ let deletions = null;
const commitRoot = () => {
+ deletions.forEach(commitWork); // 删除不需要的 fiber
commitWork(wipRoot.child);
+ currentRoot = wipRoot; // 上一个 fiber
wipRoot = null;
};
// 初始化第一个工作单元 rootFiber
const render = (element, container) => {
wipRoot = {
dom: container,
props: {
children: [element],
},
sibling: null,
child: null,
parent: null,
+ alternate: currentRoot, // 上一次fiber
};
+ deletions = [];
nextUnitOfWork = wipRoot;
};
将原先构建Fiber Tree
的代码提取出单独用新的调度函数reconcileChildren
处理,添加diff
后的逻辑如下:
const performUnitOfWork = (fiber) => {
// 创建 DOM 元素
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 追加到父节点
if (fiber.parent) {
fiber.parent.dom.append(fiber.dom);
}
// 给children添加fiber
const elements = fiber.props.children;
let prevSibling = null;
- // 构建Fiber tree
- for (let i = 0; i < elements.length; i++) {
- const newFiber = {
- type: elements[i].type,
- props: elements[i].props,
- parent: fiber,
- dom: null,
- child: null,
- sibling: null,
- };
- // children 数组中第一个节点当作 child ,其余均为 sibling 节点
- if (i === 0) {
- fiber.child = newFiber;
- } else {
- prevSibling.sibling = newFiber;
- }
- prevSibling = newFiber;
- }
+ // 新建newFiber
+ reconcileChildren(fiber, elements);
// 返回下一个fiber
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
};
const reconcileChildren = (wipFiber, elements) => {
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
let newFiber = null;
for (let i = 0; i < elements.length; i++) {
const sameType =
oldFiber && elements[i] && elements[i].type === oldFiber.type;
/*
类型相同:
复用节点进行更新
类型不同:
创建新的DOM节点
旧节点存在进行删除
*/
if (sameType) {
newFiber = {
type: oldFiber.type,
props: elements[i].props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectFlag: "UPDATE",
};
}
if (!sameType && elements[i]) {
newFiber = {
type: elements[i].type,
props: elements[i].props,
dom: null,
parent: wipFiber,
alternate: null,
effectFlag: "PLACEMENT",
};
}
if (!sameType && oldFiber) {
oldFiber.effectFlag = "DELETION";
deletions.push = [oldFiber];
}
if (oldFiber) oldFiber = oldFiber.sibling;
if (i === 0) {
wipFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
}
};
经过diff
后的fiber
后会存在被删除或更新,之前只写了添加的情况处理,需补充剩余情况,对于更新操作,需考虑对应事件、属性的处理,具体如下:
const commitWork = (fiber) => {
if (!fiber) return;
const domParent = fiber.parent.dom;
- domParent.appendChild(fiber.dom);
+ if (fiber.effectFlag === "PLACEMENT" && fiber.dom !== null) {
+ domParent.appendChild(fiber.dom);
+ } else if (fiber.effectFlag === "DElETION") {
+ domParent.removeChild(fiber.dom);
+ } else if (fiber.effectFlag === "UPDATE" && fiber.dom !== null) {
+ updateDom(fiber.dom, fiber.alternate.props, fiber.props);
+ }
if (fiber.child) commitWork(fiber.child);
if (fiber.sibling) commitWork(fiber.sibling);
};
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (next) => (key) => !(key in next);
const updateDom = (dom, prevProps, nextProps) => {
// 删除已经不存在的 props
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(nextProps))
.forEach((name) => (dom[name] = ""));
// 添加新的或者改变的 props
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => (dom[name] = nextProps[name]));
// 删除没有的或者改变的 events
Object.keys(prevProps)
.filter(isEvent)
.filter(isGone(nextProps) || isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// 添加新 events
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
};
Function Components
先来看一段代码,试着用micro react
运行,看能否正常渲染?
import { createElement, render } from "../react";
const funComponent = () => createElement("我是函数式组件", null, null)
const element = createElement(
"h1",
null,
createElement(
"div",
null,
"hello",
createElement("span", null, "world !"),
createElement(funComponent, null, null)
)
);
const container = document.getElementById("root");
render(element, container);
答案是否定的,这是因为函数组件没有自身DOM结构,而在我们编写的createElement
时需要有DOM
结构的入参,后续需要使用其DOM
进行渲染,所以不能正常渲染。
再使用 React 开发时,函数式组件经常是第一选择,所以为了支持函数式组件开发,需要处理这个问题。
在
createElement
中打印type
分析可以得出,函数组件funComponent
对应的type
为函数,而我们需要的是该函数的返回结果值,所以需将函数组件与原生组件区分处理。
const performUnitOfWork = (fiber) => {
- // 创建 DOM 元素
- if (!fiber.dom) {
- fiber.dom = createDom(fiber);
- }
-
- // 给children添加fiber
- const elements = fiber.props.children;
- // 新建newFiber
- reconcileChildren(fiber, elements);
+ //区分函数组件,渲染 DOM 特殊处理
+ const isFunctionComponent = fiber.type instanceof Function;
+ isFunctionComponent
+ ? updateFunctionComponent(fiber)
+ : updateHostComponent(fiber);
// 返回下一个fiber
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
};
const updateHostComponent = (fiber) => {
// 创建 DOM 元素
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 新建newFiber
reconcileChildren(fiber, fiber.props.children);
};
const updateFunctionComponent = (fiber) => {
const children = [fiber.type(fiber.props)]; // 将函数运行取其对应 DOM
reconcileChildren(fiber, children);
};
- 添加时,要找到
DOM
节点的父节点,需要沿着当前fiber
的parent
向上查找,直到找到存在DOM
的fiber
节点 - 删除时由于函数组件没有自身DOM结构,所以向下寻找最近的
child
节点DOM
进行删除
const commitWork = (fiber) => {
if (!fiber) return;
- const domParent = fiber.parent.dom;
let domParentFiber = fiber.parent;
+ while (!domParentFiber.dom) {
+ domParentFiber = domParentFiber.parent; // 向上寻找存在 DOM 的 fiber 节点
+ }
+ const domParent = domParentFiber.dom;
if (fiber.effectFlag === "PLACEMENT" && fiber.dom !== null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectFlag === "DElETION") {
- domParent.removeChild(fiber.dom);
+ commitDeletion(fiber, domParent);
} else if (fiber.effectFlag === "UPDATE" && fiber.dom !== null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
if (fiber.child) commitWork(fiber.child);
if (fiber.sibling) commitWork(fiber.sibling);
};
// 函数组件(没有自己的DOM)特殊处理:向下寻找最近的 child 节点dom进行删除
const commitDeletion = (fiber, domParent) => {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
};
继续运行本节开头的例子,发现正常渲染,支持函数式组件功能基本实现。
Hooks
函数式组件离不开hooks
,所以这里以useState
为例实现一个简单的hooks
。
// 使用时
const [state, setState] = useState(0)
// 转换时推测应该返回这样的一个数组,只需关心中间的逻辑处理即可
export const useState = (init) => {
// ...逻辑处理
return [state, setState];
};
考虑到使用setState
时需要对之前的state
进行比较以及使用多个useState
,所以定义一些全局变量方便使用:
wipFiber
:存储记录上一个fiber
hookIndex
:存储记录 每一个hook
对应的位置
+ let wipFiber = null;
+ let hookIndex = null;
const updateFunctionComponent = (fiber) => {
+ wipFiber = fiber; //默认当前 fiber 为 wipFiber
+ hookIndex = 0;
+ wipFiber.hooks = []; // 初始化 hooks 数组,用于存储 hook
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
};
接着开始处理 useState
的逻辑:
- 将旧的
hook
从上一个fiber
即wipFiber.alternate
中取出; - 定义
hook
对象,包含状态值state
和存储改变其状态的函数数组queue
,其中queue
记录每一次调用的setState
函数; - 从
queue
中的函数遍历取出,将当前hook
的state
作为参数传入,循环调用,使state
的值变为最新; - 最后在当前
fiber
中存储该hook
,hookIndex
加1方便对该hook
追踪记录。
export const useState = (init) => {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : init,
queue: [],
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => (hook.state = action(hook.state)));// 执行传入的函数
const setState = (action) => {
hook.queue.push(action); // 记录调用的函数
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot; // 将执行完毕的 wipRoot 提交至浏览器渲染
deletions = [];
};
// 记录该 hook
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
};
来尝试下使用useState
:
const funcComponent = () => {
const [state, setState] = useState(0);
console.log('state:', state);
return createElement(
"h1",
{ onclick: () => setState((state) => state + 1) },
state
);
}
const element = createElement(
"h1",
null,
createElement(
"div",
null,
"hello",
createElement("span", null, "world !"),
createElement(funcComponent, null, null)
)
);
const container = document.getElementById("root");
浏览器正常显示,控制台输出结果一致,效果实现。
Dom style props && Flat children arrays
目前 micro react
的功能基本完成,有些优化可以放在这里补充下。
dom style props
在添加属性时遍历props
对象,但是DOM
中的样式对应着一个style
对象,所以在遍历style
属性时需当作对象处理。
const createDom = (fiber) => {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
console.log(fiber)
Object.keys(fiber.props)
.filter((key) => key !== "children")
.forEach((name) => (dom[name] = fiber.props[name]));
console.log( Object.keys(fiber.props)
.filter((key) => key === "style"))
+ // 设置默认样式属性
+ const defaultStyle = fiber.props.style || {};
+ Object.keys(defaultStyle).forEach((name) => (dom.style[name] = defaultStyle[name]));
return dom;
};
const isEvent = (key) => key.startsWith("on");
- const isProperty = (key) => key !== "children" && !isEvent(key);
+ const isStyle = (key) => key === "style";
+ const isProperty = (key) => key !== "children" && !isEvent(key) && !isStyle(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (next) => (key) => !(key in next);
const updateDom = (dom, prevProps, nextProps) => {
const prevStyle = prevProps.style || {};
const nextStyle = nextProps.style || {};
// 删除已经不存在的 props
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.filter(isGone(nextProps))
.forEach((name) => (dom[name] = ""));
// 添加新的或者改变的 props
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => (dom[name] = nextProps[name]));
// 删除没有的或者改变的 events
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps))
.filter(isGone(nextProps) || isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// 添加新的 events
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
+ // 删除已经不存在的 style
+ Object.keys(prevStyle)
+ .filter(isGone(nextStyle))
+ .forEach((name) => (dom.style[name] = ""));
+ // 添加新的 style
+ Object.keys(nextStyle).forEach((name) => (dom.style[name] = nextStyle[name]));
};
flat children arrays
对于多节点元素需单独处理,如列表ul
ol
。
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
- children: children.map((child) =>
+ children: children.flat().map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
};
const nodes = ["节点1", "节点2", "节点3"];
const list = createElement(
"ul",
null,
nodes.map((node) => createElement("li", null, node))
);
const renderer = () => {
const container = document.querySelector("#root");
const element = createElement("h1", null, list);
render(element, container);
};
renderer();
浏览器正常显示,控制台输出结果一致,效果实现。
总结
目前为止一个基础版的micro react
就基本实现了,相信React
的工作流程及思想有了更深的认识,对于类组件以及其他hooks
等react特性功能感兴趣的朋友可以自行实现。
本文源码放在下方,需要可以自取,与本文章节对应。
参考资料
[1] Build your own React:pomb.us/build-your-…
[2] React 官网:reactjs.org
[3] React 技术解密:kasong.gitee.io/just-react
转载自:https://juejin.cn/post/7159439522759966733