【React】react-reconciler快速入门

贺博厚
2023-12-01

前言

  • 我最近在研究新版taro的原理,其中就有使用该库制作自定义渲染器。一起学习下吧。
  • 老版taro原理到时候会专门写个文章手写下。已经整理完成。

react-reconciler

  • 这个是react抽离的协调器逻辑,用于编写自定义渲染器。
  • https://www.npmjs.com/package/react-reconciler
const Reconciler = require('react-reconciler');

const HostConfig = {
  // You'll need to implement some methods here.
  // See below for more information and examples.
};

const MyRenderer = Reconciler(HostConfig);

const RendererPublicAPI = {
  render(element, container, callback) {
    // Call MyRenderer.updateContainer() to schedule changes on the roots.
    // See ReactDOM, React Native, or React ART for practical examples.
  }
};

module.exports = RendererPublicAPI;

快速上手

  • 创建项目:
npx create-react-app learn-react-reconciler
  • 将app的demo修改为加一减一以便更好的测试:
import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {
	constructor(props) {
		super(props);
		this.state = {
			counter: 0,
		};
	}
	render() {
		return (
			<div className="App">
				<header className="App-header" style={{ minHeight: 200 }}>
					<img src={logo} className="App-logo" alt="logo" />
					<h1 className="App-title">Welcome to React</h1>
				</header>
				<div className="App-intro">
					<div className="button-container">
						<button
							className="decrement-button"
							onClick={() =>
								this.setState({
									counter: this.state.counter - 1,
								})
							}
						>
							-
						</button>
						<div className="counter-text">{this.state.counter}</div>
						<button
							className="increment-button"
							onClick={() =>
								this.setState({
									counter: this.state.counter + 1,
								})
							}
						>
							+
						</button>
					</div>
				</div>
			</div>
		);
	}
}

export default App;
  • index中可以看到使用的是ReactDom的render方法进行渲染的:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
  • 下面我们使用自己的渲染器去替换ReactDom:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import MyCustomRenderer from "./myCustomRenderer";

// ReactDOM.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>,
//   document.getElementById('root')
// );
MyCustomRenderer.render(<App />, document.getElementById("root"));
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
  • 安装react-reconciler,添加以下内容
import ReactReconciler from "react-reconciler";

const hostConfig = {};
const ReactReconcilerInst = ReactReconciler(hostConfig);
// eslint-disable-next-line import/no-anonymous-default-export
export default {
	render: (reactElement, domElement, callback) => {
		// Create a root Container if it doesnt exist
		if (!domElement._rootContainer) {
			domElement._rootContainer = ReactReconcilerInst.createContainer(
				domElement,
				false
			);
		}

		// update the root Container
		return ReactReconcilerInst.updateContainer(
			reactElement,
			domElement._rootContainer,
			null,
			callback
		);
	},
};
  • 来到页面,可以发现收货报错

react-reconciler.development.js:5633 Uncaught TypeError: getRootHostContext is not a function

  • 这时候需要对hostconfig添加函数,直到没有报错。
  • 但是页面仍不会显示东西,需要对创建dom等操作进行定义:
const rootHostContext = {};
const childHostContext = {};

const hostConfig = {
	now: Date.now,
	getRootHostContext: () => {
		return rootHostContext;
	},
	prepareForCommit: () => {},
	resetAfterCommit: () => {},
	getChildHostContext: () => {
		return childHostContext;
	},
	shouldSetTextContent: (type, props) => {
		return (
			typeof props.children === "string" ||
			typeof props.children === "number"
		);
	},
	/**
   This is where react-reconciler wants to create an instance of UI element in terms of the target. Since our target here is the DOM, we will create document.createElement and type is the argument that contains the type string like div or img or h1 etc. The initial values of domElement attributes can be set in this function from the newProps argument
   */
	createInstance: (
		type,
		newProps,
		rootContainerInstance,
		_currentHostContext,
		workInProgress
	) => {
		const domElement = document.createElement(type);
		Object.keys(newProps).forEach((propName) => {
			const propValue = newProps[propName];
			if (propName === "children") {
				if (
					typeof propValue === "string" ||
					typeof propValue === "number"
				) {
					domElement.textContent = propValue;
				}
			} else if (propName === "onClick") {
				domElement.addEventListener("click", propValue);
			} else if (propName === "className") {
				domElement.setAttribute("class", propValue);
			} else {
				const propValue = newProps[propName];
				domElement.setAttribute(propName, propValue);
			}
		});
		return domElement;
	},
	createTextInstance: (text) => {
		return document.createTextNode(text);
	},
	appendInitialChild: (parent, child) => {
		parent.appendChild(child);
	},
	appendChild(parent, child) {
		parent.appendChild(child);
	},
	finalizeInitialChildren: (domElement, type, props) => {},
	supportsMutation: true,
	appendChildToContainer: (parent, child) => {
		parent.appendChild(child);
	},
	prepareUpdate(domElement, oldProps, newProps) {
		return true;
	},
	commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
		Object.keys(newProps).forEach((propName) => {
			const propValue = newProps[propName];
			if (propName === "children") {
				if (
					typeof propValue === "string" ||
					typeof propValue === "number"
				) {
					domElement.textContent = propValue;
				}
			} else {
				const propValue = newProps[propName];
				domElement.setAttribute(propName, propValue);
			}
		});
	},
	commitTextUpdate(textInstance, oldText, newText) {
		textInstance.text = newText;
	},
	removeChild(parentInstance, child) {
		parentInstance.removeChild(child);
	},
	clearContainer() {
		console.log("clear");
	},
};
  • 创建操作
    createInstance(type, newProps, rootContainerInstance, _currentHostContext, workInProgress) 由于目标是dom所以用dom方法。
    createTextInstance: 这个方法用来创建文本实例。
  • ui树操作
    appendInitialChild: 添加初始孩子的操作
    appendChild: 用于后续树操作的添加孩子
    removeChild: 删除
    appendChildToContainer: 提交阶段添加孩子
  • 更新操作
    prepareUpdate:这是我们想要区分 oldProps 和 newProps 并决定是否更新的地方,为了简单起见,将其设置为 true。
    commitUpdate(domElement, updatePayload, type, oldProps, newProps):此函数用于随后domElement从newProps值更新属性。
  • 这样一个自定义渲染器就做好了。页面也出现了和react-dom一样的效果。在更新时也能触发对应操作。
 类似资料: