Mini React 简明指南

2024年6月2日

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 的例子,它包含几个概念

  1. JSX 是一个表达式,它可以被赋值、被返回、被执行。
  2. 是数据的引用,当数据变化时,这个 JSX 相关的 DOM 要更新
  3. 使用大写字母开头的标签是引用一个函数,视为函数组件。(在 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
mini-react