Mini React 简明指南
背景
什么是 mini react ? 为什么要用它?
顾名思义,mini react 是指 react 的 mini 版本,我们用于学习 react 的核心思想,以至于在日常使用中对于出现意外的场景,能通过核心原理推导出来现状。
若 React 的概念不熟悉,请到 react.dev 学习。
流程概要
设计DSL、 API
首先介绍 DSL的概念,DSL 中文译名是领域特定语言,在这篇文章特指描述 UI 的语言。
UI 上的描述最直观的是 HTML 语言。
React 的基本概念是 UI = fn(state), 是通过数据驱动视图的更新,所以数据与视图必然会耦合,单纯使用 HTML 是无法记录数据与节点的关系,所以我们需要制造一门新的语言。
一门新的语言要考虑的是输入是什么?输出是什么?
熟悉 React 的都知道,输入就是 JSX 、输出是 ReactElement 对象 (Virtual Dom 结构)
const title = "Mini React 简明指南"
const Header = (
<header>
{title}
<Logo />
</header>
)
const Logo = () => {
const onClick = () => {
window.location.href = '/';
}
return <img src="logo.png" onClick={onClick} title="title" alt="alt" />
}
上述代码是一个 JSX 的例子,它包含几个概念
- JSX 是一个表达式,它可以被赋值、被返回、被执行。
- 是数据的引用,当数据变化时,这个 JSX 相关的 DOM 要更新
- 使用大写字母开头的标签是引用一个函数,视为函数组件。(在 React 中是组件化的架构)
经过 babel 或者 tsc 编译后,其代码如下:
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const tilte = "Mini React 简明指南";
const Header = _jsxs("header", {
children: [title, /*#__PURE__*/_jsx(Logo, {})]
});
const Logo = () => {
const onClick = () => {
window.location.href = '/';
};
return _jsx("img", {
src: "logo.png",
onClick: onClick,
title: title,
alt: "alt"
});
};
_jsxs 和 _jsx 的区别只是有没有 children, 单一元素是 _jsx, 有chilren 就是 _jsxs
实际上,_jsx 就是 createElement 函数,用来生成 ReactElement 结构。
其调用后的 ReactElement 结构如下:
{
type: "header",
props: {
children: [
"Mini React 简明指南", // 文本子节点
{
type: "img", // Logo 组件实际渲染的 img 标签
props: {
src: "logo.png",
onClick: [Function: onClick], // 事件函数
title: "title",
alt: "alt"
},
key: null,
ref: null,
$$typeof: Symbol(react.element) // react元素 的标识
}
]
},
key: null,
ref: null,
$$typeof: Symbol(react.element)
}
API 设计如下:
const MiniReact = {
createElement: Function
render: Function,
useState: Function,
useEffect: Function
};
window.MiniReact = MiniReact; // 方便调试
/**
* @params{string|Function} type - HTML 标签 或者函数
* @params{Object} props - 属性、样式、事件对象
* @params{Array<ReactELement>} children - 子元素
* @returns {ReactElement}
*/
function createElement(type, props, ...children) {
// ......
}
/**
* @param{Object} element - JSX ReactElement
* @param{HTMLElement} container - HTML Element
* @returns {void}
*/
function render(element, container) {
// ......
}
/**
*
* @params{any} initialState - 初始状态
* @returns {[state: any, setState:Function<any>()]}
*/
function useState(initialState) {
// ......
}
/**
* @params{Function} effect - 执行的副作用
* @params{Array<any>} deps - 副作用调用所依赖的值
* @returns{Function} cleanup - 清除副作用的函数
*/
function useEffect(effect, deps) {
// ......
}
createElement
/**
* @params{string|Function} type - HTML 标签 或者函数
* @params{Object} props - 属性、样式、事件对象
* @params{Array<ReactELement>} children - 子元素
* @returns {ReactElement}
*/
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
const isTextElement = typeof child === 'string' || typeof child === 'number';
// 统一逻辑: 这里把文本节点也当成一个 ReactElement 进行渲染
return isTextElement ? createTextElement(child) : child;
})
}
}
}
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
// 如果是文本类型的节点, diff 只看 nodeValue 即可, 它没有标签名、属性、与子元素的。
nodeValue: text,
children: [],
}
}
}
render 阶段
let wipRoot = null; // 正在工作的 Fiber
let currentRoot = null; // 当前显示页面的 Fiber
let nextUnitOfWork = null; // 下一个要处理的 Fiber 节点
function workLoop(deadline) {
let shouldYield = false;
while(nextUnitOfWork!== null && !shouldYield) { // 一直遍历 Fiber 链表,直到节点为空
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 消费 Fiber
shouldYield = deadline.timeRemaining() < 1; // 依赖 requestIdleCallback 来调度
}
// 处理完成 Fiber 后,需要将此次更新提交到页面上,也就是更新 DOM
if (nextUnitOfWork === null && wipRoot !== null) {
commitRoot(wipRoot);
}
// 提交此次更新成功后,则继续空闲时间调度渲染任务
window.requestIdleCallback(workLoop);
}
window.requestIdleCallback(workLoop); // 初始执行
// 消费工作单元 Fiber
function performUnitOfWork(fiber) {
// 深度遍历以 fiber 为父节点的链表,若是函数组件,则调用 updateFunctionComponent,
// 若是 HTML 标签类型,则直接创建 Dom, 调用 updateHostComponent
const isFunctionComponent = typeof fiber.type === 'function';
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
// 需要返回下一个处理单元
// 遵循 child -> sibling -> return 顺序
if (fiber.child) {
return child;
}
let nextFiber = fiber;
while(nextFiber !== null) {
if (nextFiber.silbing) { // 先查找当前的 sibling
return nextFiber.sibling;
}
nextFiber = nextFiber.return; // 返回上一级继续查找 sibling
}
}
let wipFiber = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
const children = [fiber.type(fiber.props)]; // 先执行函数,返回节点
// 处理子元素的渲染逻辑, 将 ReactElement 变成 Fiber 链表
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
if (!fiber.dom) { // 创建节点
fiber.dom = createDom(fiber)
}
// 处理子节点
reconcileChildren(fiber, fiber.props?.children ?? [])
}
function createDom(fiber) {
const isTextElement = fiber.type === 'TEXT_ELEMENT';
const dom = isTextElement ? document.createTextElement('') : document.createElement(fiber.type);)
// 更新节点 的 props (包括 children)
updateDom(dom, {}, fiber.props);
return dom;
}
const isEvent = (key) => (key.startWiths('on'))
const isProperty = (key) => (key !== 'children' && !isEvent(key))
const isGone = (prevProps, nextProps) => (key) => (key in prevProps && !(key in nextProps))
const isNew = (prevProps, nextProps) => (key) => (prevProps[key] !== nextProps[key])
function updateDom(dom, prevProps, nextProps) {
// 删除旧属性
if (prevProps) {
Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps))
.forEach(key => {
dom[key] = '';
})
// 移除旧事件
Object.keys(prevProps).filter(isEvent).filter((key) => {
return isGone(prevProps, nextProps)(key) || isNew(prevProps, nextProps)(key)
).forEach(key => {
dom.removeEventListeners(key.toLowerCase().substring(2), prevProps[key])
});
}
// 添加新属性
if (nextProps) {
Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps))
.forEach(key => {
dom[key] = nextProps[key];
})
// 添加新事件
Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps))
.filter((key) => {
dom.addEventListeners(key.toLowerCase().substring(2), nextProps[key])
})
}
}
let delections = [];
function reconcileChildren(wipFiber, elements = []) {
let index = 0;
let currentFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while(index < elements.length) {
const element = elements[i];
// 判断 element 节点跟页面显示的 Fiber 的节点是否同一个类型
const isSameType = element.type === currentFiber.type;
let newFiber = null; // 构造 Fiber
// 相同类型,打更新的 Tag
if (isSameType) {
newFiber = {
type: currentFiber.type,
props: element.props,
dom: currentFiber.dom,
return: wipFiber,
alternate: currentFiber,
effectTag: 'UPDATE'
}
}
// 不是相同的类型
if (!isSameType) {
if (element) {
// 如果 element 存在, 则新增
newFiber = {
type: element.type,
props: element.props,
dom: null,
return: wipFiber,
alternate: null,
effectTag: 'PLACEMENT'
}
}
if (currentFiber) {
// 如果之前有的节点,但现在 type 不一样,则删除
currentFiber.effectTag = "DELETION"
deletions.push(currentFiber); // 全局存放需要删除的节点
}
}
if (index === 0) { // 如果 index 是 0 ,则是头结点,将 child 赋值给 wipFiber 即可
wipFiber.child = newFiber;
} else { // 中间节点需要上一个节点的指针
prevSibling.sibling = newFiber;
}
// 更新下一个节点
if (currentFiber) {
currentFiber = currentFiber.sibling;
}
prevSibling = newFiber;
index++;
}
}
useState
// 前面 updateFunctionComponent 要存放 state, 这里采用数组
let wipFiber = null;
let stateHookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
stateHookIndex = 0;
wipFiber.stateHooks = [];
// effect 下节用到
wipFiber.effectHooks = [];
const children = [fiber.type(fiber.props)]; // 先执行函数,返回节点
// 处理子元素的渲染逻辑, 将 ReactElement 变成 Fiber 链表
reconcileChildren(fiber, children)
}
/**
*
* @params{any} initialState - 初始状态
* @returns {[state: any, setState:Function<any>()]}
*/
function useState(initialState) {
const currentFiber = wipFiber;
const oldHook = wipFiber?.alternate?.stateHooks?.[stateHookIndex];
// 每一个 fiber.stateHooks 的结构
// state 存放之前的值
// queue 存放 setState 的 callback
const stateHook = {
state: oldHook ? oldHook.state : initialState,
queue: oldHook ? oldHook.queue : [],
}
// 先计算当前 state
stateHook.queue.forEach(action => {
stateHook.state = action(stateHook.state); // 累积状态变更
});
// 计算完就清空
stateHook.queue = [];
function setState(actionOrValue) {
const isFunction = typeof actionOrValue=== 'function';
stateHook.queue.push(isFunction ? actionOrValue : () => actionOrValue);
// 将当前处理的节点顶层置为函数组件 Fiber 节点
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
// 更新下一个消费的 Fiber 节点
nextUnitOfWork = wipRoot;
}
return [stateHook.state, setState]
}
useEffect
/**
* @params{Function} effect - 执行的副作用
* @params{Array<any>} deps - 副作用调用所依赖的值
* @returns{Function} cleanup - 清除副作用的函数
*/
function useEffect(effect, deps) {
wipFiber.effectHooks.push({
callback: effect,
deps,
cleanup: undefined,
})
}
commit 阶段
// Commit 阶段主要执行 effect 、更新Dom Effect
function commitRoot() {
// 更新 Dom Effect
commitWork(wipRoot.child)
// 执行 Effect
commitEffectHooks();
// 更新指针
currentRoot = wipRoot;
wipRoot = null;
// reset
deletions = [];
}
function commitWork(fiber) {
if (!fiber) return;
// 找到父节点
let domParentFiber = fiber.return;
while(domParentFiber && !domParentFiber.dom) {
domParentFiber = domParentFiber.return;
}
const domParent = domParentFiber.dom;
// 根据 tag 进行更新节点
if (fiber.effectTag === 'PLACEMENT' && domParent) {
domParent.appendChild(fiber.dom);
}
if (fiber.effectTag === 'UPDATE') {
updateDom(fiber.dom, fiber?.alternate?.props, fiber.props)
}
if (fiber.effectTag === 'DELETTION' && domParent) {
commitDeletion(fiber, domParent); // 递归删除
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, parent) {
if (fiber.dom) {
parent.removeChild(fiber.dom)
}
commitDeletion(fiber.child);
commitDeletion(fiber.sibling);
}
// 提交当前 wipRoot 的 effect
function commitEffectHooks() {
// 先处理 cleanup
// 执行 effect
function runCleanup(fiber) {
if (fiber.alternate && fiber.alternate.effectHooks) {
fiber.alternate.effectHooks.forEach((hook, index) => {
// 如果 deps 不相同,则触发
const deps = fiber.effectHooks[index]?.deps;
if (!deps || isDepsEqual(deps, hook.deps)) {
hook?.cleanup?.();
}
})
}
// 递归子 fiber 节点
runCleanup(fiber.child);
runCleanup(fiber.sibling);
}
function run(fiber) {
if (!fiber) return;
if (fiber.effectHooks) {
fiber.effectHooks.forEach((hook, index) => {
if (!fiber.alternate){
// 首次渲染, 直接执行
hook.cleanup = hook?.callback();
return;
}
// 判断 deps 是否变更再执行
if (!hook.deps) {
hook.cleanup = hook?.callback();
} else {
if (!isDepsEqual(hook.deps, fiber.alternate?.effectHooks[index].deps)) {
hook.cleanup = hook?.callback();
}
}
})
}
// 递归子 fiber 节点
run(fiber.child);
run (fiber.sibling);
}
runCleanup(wipRoot);
run(wipRoot);
}
function isDepsEqual(newDeps, deps) {
if (newDeps.length !== deps.length) return false;
for(let i = 0;i < newDeps.length;i++) {
if (!Object.is(newDeps[i], deps[i])) return false;
}
return true;
}
添加入口文件
// index.jsx
const { render, useState, useEffect } = window.MiniReact;
function App() {
const [count,setCount] = useState(0);
function add(){
setCount((count) => count + 1);
}
function sub() {
setCount((count) => count - 1);
}
return (
<div>
<button onClick={sub}>-1</button>
<p>{count}</p>
<button onClick={add}>+1</button>
</div>
);
}
render(<App/>, document.getElementById('root'));
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scal=1.0">
<title>Mini React</title>
<meta http-equiv="cache-control" content="no-cache">
</head>
<body>
<div id="root"></div>
<script src="dist/mini-react.js"></script>
<script src="dist/index.js"></script>
</body>
</html>
package.json 需要安装 typescript 依赖
{
"name": "mini-react",
"version": "0.0.1",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"serve": "npx http-server .",
"start": "npm run watch & npm run serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.7.3"
}
}
tsconfig.json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"jsx": "react", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
"jsxFactory": "MiniReact.createElement", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
示例
npm run start
