React是一个由 Facebook 开发和维护的开源 JavaScript 库,用于构建用户界面,特别是单页面应用程序(SPA)。以下是 React 的主要特点和概念:
虚拟 DOM(Virtual DOM)是 React 中的核心概念之一。为了理解它,首先需要知道直接操作真实的 DOM 是代价高昂的,因为这可能引起浏览器的重排和重绘,导致性能瓶颈。虚拟 DOM 旨在最小化与真实 DOM 的交互次数,从而提高性能。 以下是关于 React 中的虚拟 DOM 的详细解释:
npm install create-a-react-app -g
cra study-react
src\index.js
import React from "./react";
import utils from "zhang-utils";
const element = React.createElement(
"div",
{
style: {
color: "red",
},
className: "wrapper",
},
"hello",
React.createElement(
"span",
{
style: {
color: "blue",
},
},
"world"
)
);
console.log(
JSON.stringify(utils.removePrivateProps(element, ["key", "ref"]), null, 2)
);
src\react.js
function createElement(type, config, children) {
let props = { ...config };
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2);
} else {
props.children = children;
}
return {
type,
props,
};
}
const React = {
createElement,
};
export default React;
package.json
{
"scripts": {
"start": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts start",
"build": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts build",
"test": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts test",
"eject": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts eject"
}
}
src\index.js
import React from "./react";
+import ReactDOM from "./react-dom/client";
const element = React.createElement(
"div",
{
style: {
color: "red",
},
className: "wrapper",
},
"hello",
React.createElement(
"span",
{
style: {
color: "blue",
},
},
"world"
)
);
+ReactDOM.createRoot(document.getElementById("root")).render(element);
src\react-dom\client.js
function createRoot(container) {
return {
render(reactElement) {
const domElement = renderElement(reactElement);
container.appendChild(domElement);
},
};
}
function renderElement(element) {
if (typeof element === "string") {
return document.createTextNode(element);
}
const { type, props } = element;
const domElement = document.createElement(type);
Object.keys(props).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, props.style);
} else if (name.startsWith("on")) {
const eventName = name.toLowerCase().substring(2);
domElement.addEventListener(eventName, props[name]);
} else {
domElement[name] = props[name];
}
});
if (typeof props.children !== "undefined") {
const children = Array.isArray(props.children)
? props.children
: [props.children];
children.forEach((child) => domElement.appendChild(renderElement(child)));
}
return domElement;
}
const ReactDOMClient = {
createRoot,
};
export default ReactDOMClient;
React 使用 JSX 为了提供一种更加直观、声明式的方式来描述 UI,并且在编写 UI 代码时能够保持 JavaScript 的所有功能。 核心概念:
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
+const jsxElement = (
+ <div style={{ color: "red" }} className="wrapper">
+ hello<span style={{ color: "blue" }}>world</span>
+ </div>
+);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(jsxElement);
什么是 React 组件? React 组件是独立、可重用的代码片段,用于描述 UI 的一部分。组件可以是简单的 UI 元素(如一个按钮)或包含其他组件的复杂容器。 共同点:
props
,并依据 props
渲染 UI。useEffect
Hook,函数组件也可以模拟大多数生命周期行为。
不同点:this
绑定。state
或生命周期方法时。PureComponent
或 shouldComponentUpdate
进行细粒度控制。React.memo
和 useMemo
。props
方面有很多相似之处。但随着 Hooks 的引入,函数组件变得更加强大和灵活,它们更简洁,易于读写,逻辑复用更直观,并且更符合 React 的未来发展方向。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
function FunctionComponent() {
return (
<div style={{ color: "red" }} className="wrapper">
hello<span style={{ color: "blue" }}>world</span>
</div>
);
}
const functionElement = <FunctionComponent name="函数组件" />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(functionElement);
src\react-dom\client.js
function createRoot(container) {
return {
render(reactElement) {
container.innerHTML = "";
const domElement = renderElement(reactElement);
container.appendChild(domElement);
},
};
}
function renderElement(element) {
if (typeof element === "string") {
return document.createTextNode(element);
}
const { type, props } = element;
if (typeof type === "function") {
const functionElement = type(props);
return renderElement(functionElement);
}
const domElement = document.createElement(type);
Object.keys(props).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, props.style);
} else if (name.startsWith("on")) {
const eventName = name.toLowerCase().substring(2);
domElement.addEventListener(eventName, props[name]);
} else {
domElement[name] = props[name];
}
});
if (typeof props.children !== "undefined") {
const children = Array.isArray(props.children)
? props.children
: [props.children];
children.forEach((child) => domElement.appendChild(renderElement(child)));
}
return domElement;
}
const ReactDOMClient = {
createRoot,
};
export default ReactDOMClient;
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
+class ClassComponent extends React.Component {
+ render() {
+ return (
+ <div style={{ color: "red" }} className="wrapper">
+ hello<span style={{ color: "blue" }}>world</span>
+ </div>
+ );
+ }
+}
+const classElement = <ClassComponent name="类组件" />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(classElement);
src\react.js
function createElement(type, config, children) {
let props = { ...config };
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2);
} else {
props.children = children;
}
return {
type,
props,
};
}
+class Component {
+ static isReactComponent = true;
+ constructor(props) {
+ this.props = props;
+ }
+}
const React = {
createElement,
+ Component,
};
export default React;
src\react-dom\client.js
function createRoot(container) {
return {
render(reactElement) {
container.innerHTML = "";
const domElement = renderElement(reactElement);
container.appendChild(domElement);
},
};
}
function renderElement(element) {
if (typeof element === "string") {
return document.createTextNode(element);
}
const { type, props } = element;
if (typeof type === "function") {
+ if (type.isReactComponent) {
+ const classInstance = new type(props);
+ const classElement = classInstance.render();
+ return renderElement(classElement);
+ } else {
+ const functionElement = type(props);
+ return renderElement(functionElement);
+ }
}
const domElement = document.createElement(type);
Object.keys(props).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, props.style);
} else if (name.startsWith("on")) {
const eventName = name.toLowerCase().substring(2);
domElement.addEventListener(eventName, props[name]);
} else {
domElement[name] = props[name];
}
});
if (typeof props.children !== "undefined") {
const children = Array.isArray(props.children)
? props.children
: [props.children];
}
children.forEach((child) => domElement.appendChild(renderElement(child)));
return domElement;
}
const ReactDOMClient = {
createRoot,
};
export default ReactDOMClient;
React 的合成事件 (SyntheticEvent) 是 React 团队为解决跨浏览器的事件一致性问题而设计的。它是一个浏览器的原生事件的跨浏览器包装器,具有与原生事件相同的接口,但提供了更多的功能和与所有浏览器一致的行为。
浏览器中的事件模型主要围绕用户与网页互动时发生的各种事件(如点击、键盘输入、鼠标移动等)进行设计。这些事件可以在 DOM (Document Object Model) 元素上监听和处理。以下是浏览器事件模型的核心概念:
Document
开始,向下传递至目标元素的外层,但不包括目标元素本身。Document
。click
事件:element.addEventListener("click", function () {
console.log("Element was clicked!");
});
当指定事件发生时,回调函数会被调用。element.addEventListener("click", function (event) {
console.log(event.target); // 返回触发事件的元素
});
<a>
标签会导航到其 href
属性指定的 URL。如果你想阻止这个默认行为,可以使用事件对象的 preventDefault
方法:linkElement.addEventListener("click", function (event) {
event.preventDefault();
console.log("Link was clicked but default navigation was prevented");
});
stopPropagation
方法:element.addEventListener("click", function (event) {
event.stopPropagation();
console.log("Element was clicked and event propagation was stopped");
});
removeEventListener
方法移除事件监听器。
总的来说,浏览器的事件模型提供了一种机制,允许开发者监听和响应在网页上发生的各种交互行为。function onClick(event) {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
const eventType = event.type; // => "click"
setTimeout(function () {
console.log(event.type); // => null
console.log(eventType); // => "click"
}, 0);
// 不要这样做!
this.setState({ clickEvent: event });
// 你可以这样做:
this.setState({ eventType: event.type });
}
SyntheticEvent
对象都有一个 nativeEvent
属性,它指向原生事件。SyntheticEvent
对象。这个对象模仿了浏览器的原生事件,但它是跨浏览器的,确保所有浏览器都有相同的事件属性和方法。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class ClassComponent extends React.Component {
parentBubble() {
console.log("parentBubble 父节点在冒泡阶段执行");
}
childBubble() {
console.log("childBubble 子节点在冒泡阶段执行");
}
parentCapture() {
console.log("parentCapture 父节点在捕获阶段执行");
}
childCapture() {
console.log("childCapture 子节点在捕获阶段执行");
}
render() {
return (
<div
id="parent"
onClick={this.parentBubble}
onClickCapture={this.parentCapture}
>
<button
id="child"
onClick={this.childBubble}
onClickCapture={this.childCapture}
>
点击
</button>
</div>
);
}
}
const element = <ClassComponent />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element);
setTimeout(() => {
document.getElementById("root").addEventListener(
"click",
() => {
console.log(` Native rootCapture 原生的root的捕获`);
},
true
);
document.getElementById("root").addEventListener(
"click",
() => {
console.log(` Native rootBubble 原生的root的捕获`);
},
false
);
document.getElementById("parent").addEventListener(
"click",
() => {
console.log(` Native parentCapture 原生的父亲的捕获`);
},
true
);
document.getElementById("child").addEventListener(
"click",
() => {
console.log(` Native childCapture 原生的儿子的捕获`);
},
true
);
document.getElementById("parent").addEventListener("click", () => {
console.log(` Native parentBubble 原生的父亲的冒泡`);
});
document.getElementById("child").addEventListener("click", () => {
console.log(` Native childBubble 原生的儿子的冒泡`);
});
}, 1000);
/**
parentCapture 父节点在捕获阶段执行
childCapture 子节点在捕获阶段执行
Native rootCapture 原生的root的捕获
Native parentCapture 原生的父亲的捕获
Native childCapture 原生的儿子的捕获
Native childBubble 原生的儿子的冒泡
Native parentBubble 原生的父亲的冒泡
Native rootBubble 原生的root的捕获
childBubble 子节点在冒泡阶段执行
parentBubble 父节点在冒泡阶段执行
*/
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
}
function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props } = vdom;
const classInstance = new type(props);
const renderVdom = classInstance.render();
return createDOMElement(renderVdom);
}
function createNativeDOMElement(vdom) {
const { type, props } = vdom;
const domElement = document.createElement(type);
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\react-dom\event.js
const eventTypeMethods = {
click: {
capture: "onClickCapture",
bubble: "onClick",
},
};
export default function setupEventDelegation(container) {
if (container._reactEventDelegated) return;
["capture", "bubble"].forEach((phase) => {
Reflect.ownKeys(eventTypeMethods).forEach((type) => {
container.addEventListener(
type,
(nativeEvent) => {
const path = nativeEvent.composedPath();
const methodName = eventTypeMethods[type][phase];
const elements = phase === "capture" ? path.reverse() : path;
for (let element of elements) {
element.reactEvents?.[methodName]?.(nativeEvent);
}
},
phase === "capture"
);
});
});
container._reactEventDelegated = true;
}
src\constant.js
export const REACT_TEXT = Symbol.for("react.text");
export function wrapToVdom(element) {
return typeof element === "string" || typeof element === "number"
? { type: REACT_TEXT, props: element }
: element;
}
src\react.js
import { wrapToVdom } from "./utils";
function createElement(type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
}
}
const React = {
createElement,
Component,
};
export default React;
src\utils.js
import { REACT_TEXT } from "./constant";
export function isUndefined(v) {
return v === undefined || v === null;
}
export function isDefined(v) {
return v !== undefined && v !== null;
}
export function wrapToArray(value) {
return Array.isArray(value) ? value.flat() : [value];
}
export function wrapToVdom(element) {
return typeof element === "string" || typeof element === "number"
? { type: REACT_TEXT, props: element }
: element;
}
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class ClassComponent extends React.Component {
parentBubble() {
console.log("parentBubble 父节点在冒泡阶段执行");
}
childBubble(event) {
console.log("childBubble 子节点在冒泡阶段执行");
+ event.stopPropagation();
}
parentCapture(event) {
console.log("parentCapture 父节点在捕获阶段执行");
+ //event.stopPropagation();
}
childCapture() {
console.log("childCapture 子节点在捕获阶段执行");
}
+ clickLink(event) {
+ event.preventDefault();
+ }
render() {
return (
<div
id="parent"
onClick={this.parentBubble}
onClickCapture={this.parentCapture}
>
<button
id="child"
onClick={this.childBubble}
onClickCapture={this.childCapture}
>
点击
</button>
+ <a onClick={this.clickLink} href="https://www.baidu.com">
+ clickLink
+ </a>
</div>
);
}
}
const element = <ClassComponent />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element);
setTimeout(() => {
document.getElementById("root").addEventListener(
"click",
() => {
console.log(` Native rootCapture 原生的root的捕获`);
},
true
);
document.getElementById("root").addEventListener(
"click",
() => {
console.log(` Native rootBubble 原生的root的冒泡`);
},
false
);
document.getElementById("parent").addEventListener(
"click",
() => {
console.log(` Native parentCapture 原生的父亲的捕获`);
},
true
);
document.getElementById("child").addEventListener(
"click",
() => {
console.log(` Native childCapture 原生的儿子的捕获`);
},
true
);
document.getElementById("parent").addEventListener("click", () => {
console.log(` Native parentBubble 原生的父亲的冒泡`);
});
document.getElementById("child").addEventListener("click", () => {
console.log(` Native childBubble 原生的儿子的冒泡`);
});
}, 1000);
/**
parentCapture;event.stopPropagation();
parentCapture 父节点在捕获阶段执行
Native rootCapture 原生的root的捕获
childBubble;event.stopPropagation();
parentCapture 父节点在捕获阶段执行
childCapture 子节点在捕获阶段执行
Native rootCapture 原生的root的捕获
Native parentCapture 原生的父亲的捕获
Native childCapture 原生的儿子的捕获
Native childBubble 原生的儿子的冒泡
Native parentBubble 原生的父亲的冒泡
childBubble 子节点在冒泡阶段执行
Native rootBubble 原生的root的冒泡
clickLink
parentCapture 父节点在捕获阶段执行
Native rootCapture 原生的root的捕获
Native parentCapture 原生的父亲的捕获
Native parentBubble 原生的父亲的冒泡
parentBubble 父节点在冒泡阶段执行
Native rootBubble 原生的root的冒泡
*/
src\react-dom\event.js
const eventTypeMethods = {
click: {
capture: "onClickCapture",
bubble: "onClick",
},
};
+function createSyntheticEvent(nativeEvent) {
+ let isPropagationStopped = false;
+ const handlers = {
+ get(target, key) {
+ if (target.hasOwnProperty(key)) return Reflect.get(target, key);
+ if (typeof nativeEvent[key] === "function") {
+ return nativeEvent[key].bind(nativeEvent);
+ } else {
+ return nativeEvent[key];
+ }
+ },
+ };
+ const syntheticEvent = new Proxy(
+ {
+ nativeEvent,
+ preventDefault() {
+ if (nativeEvent.preventDefault) {
+ nativeEvent.preventDefault();
+ } else {
+ nativeEvent.returnValue = false;
+ }
+ },
+ stopPropagation() {
+ if (nativeEvent.stopPropagation) {
+ nativeEvent.stopPropagation();
+ } else {
+ nativeEvent.cancelBubble = true;
+ }
+ isPropagationStopped = true;
+ },
+ isPropagationStopped() {
+ return isPropagationStopped;
+ },
+ },
+ handlers
+ );
+ return syntheticEvent;
+}
export default function setupEventDelegation(container) {
if (container._reactEventDelegated) return;
["capture", "bubble"].forEach((phase) => {
Reflect.ownKeys(eventTypeMethods).forEach((type) => {
container.addEventListener(
type,
(nativeEvent) => {
+ const syntheticEvent = createSyntheticEvent(nativeEvent);
+ const path = syntheticEvent.composedPath();
const methodName = eventTypeMethods[type][phase];
const elements = phase === "capture" ? path.reverse() : path;
for (let element of elements) {
+ if (syntheticEvent.isPropagationStopped()) {
+ break;
+ }
+ element.reactEvents?.[methodName]?.(syntheticEvent);
}
},
phase === "capture"
);
});
});
container._reactEventDelegated = true;
}
在React中,状态更新(通过setState
调用)可以是同步的也可以是异步的,这取决于被调用的上下文。
当setState
被直接调用,例如作为事件处理器的一部分,例如绑定到按钮点击事件的函数中,React会将这些更新批处理(或“批量更新”),因为isBatchingUpdates
标志被设置为true
。这意味着React会积累所有的setState
调用,并在事件处理结束后统一处理,以避免不必要的重渲染和性能问题。
但是,如果setState
在一个异步函数中被调用,如在setTimeout
、setInterval
或者addEventListener
的回调中,React就不会批处理这些更新,因为在这些情况下isBatchingUpdates
标志通常是false
。这意味着每次setState
调用都会立即导致组件的重新渲染。
因此,当回答面试题时,您可以说明:setState
通常在React事件处理器中是异步批处理的,以提高性能和减少不必要的渲染。但是,在诸如setTimeout
、setInterval
或DOM事件监听器中,setState
可能会表现为同步的,除非使用了特定的技巧或者使用了React 17及以上版本的新特性。这个细微的差别是非常重要的,因为它影响到组件的渲染和性能优化。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
return <button onClick={this.handleClick}>{this.state.number}</button>;
}
}
const classElement = <Counter />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
+import { getDOMElementByVdom, createDOMElement } from "./react-dom/client";
function createElement(type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
}
+ setState(partialState) {
+ const newState =
+ typeof partialState === "function"
+ ? partialState(this.state)
+ : partialState;
+ this.state = {
+ ...this.state,
+ ...newState,
+ };
+ this.forceUpdate();
+ }
+ forceUpdate() {
+ const renderVdom = this.render();
+ const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
+ const parentDOM = oldDOMElement.parentNode;
+ const newDOMElement = createDOMElement(renderVdom);
+ parentDOM.replaceChild(newDOMElement, oldDOMElement);
+ this.oldRenderVdom = renderVdom;
+ }
}
const React = {
createElement,
Component,
};
export default React;
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
}
+export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
+ vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props } = vdom;
const classInstance = new type(props);
+ vdom.classInstance = classInstance;
const renderVdom = classInstance.render();
+ classInstance.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createNativeDOMElement(vdom) {
const { type, props } = vdom;
const domElement = document.createElement(type);
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
+ vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
+export function getDOMElementByVdom(vdom) {
+ if (isUndefined(vdom)) return null;
+ let { type } = vdom;
+ if (typeof type === "function") {
+ if (type.isReactComponent) {
+ return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
+ } else {
+ return getDOMElementByVdom(vdom.oldRenderVdom);
+ }
+ } else {
+ return vdom.domElement;
+ }
+}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
//import ReactDOM from "react-dom";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);
this.setState({ number: this.state.number + 1 });
console.log(this.state);
setTimeout(() => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);
this.setState({ number: this.state.number + 1 });
console.log(this.state);
});
};
render() {
return <button onClick={this.handleClick}>{this.state.number}</button>;
}
}
const classElement = <Counter />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
//ReactDOM.render(classElement, document.getElementById("root"));
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, createDOMElement } from "./react-dom/client";
+let isBatchingUpdates = false;
+let dirtyComponents = new Set();
+export function setIsBatchingUpdates(value) {
+ isBatchingUpdates = value;
+}
+export function flushDirtyComponents() {
+ dirtyComponents.forEach((component) => component.forceUpdate());
+ dirtyComponents.clear();
+ isBatchingUpdates = false;
+}
function createElement(type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
+ if (isBatchingUpdates) {
+ dirtyComponents.add(this);
+ this.pendingStates.push(partialState);
+ } else {
+ const newState =
+ typeof partialState === "function"
+ ? partialState(this.state)
+ : partialState;
+ this.state = {
+ ...this.state,
+ ...newState,
+ };
+ this.forceUpdate();
+ }
}
+ accumulateState() {
+ let state = this.pendingStates.reduce((state, update) => {
+ const newState = typeof update === "function" ? update(state) : update;
+ return { ...state, ...newState };
+ }, this.state);
+ this.pendingStates.length = 0;
+ return state;
+ }
forceUpdate() {
+ this.state = this.accumulateState();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const newDOMElement = createDOMElement(renderVdom);
parentDOM.replaceChild(newDOMElement, oldDOMElement);
this.oldRenderVdom = renderVdom;
}
}
const React = {
createElement,
Component,
};
export default React;
src\react-dom\event.js
+import { setIsBatchingUpdates, flushDirtyComponents } from "../react";
const eventTypeMethods = {
click: {
capture: "onClickCapture",
bubble: "onClick",
},
};
function createSyntheticEvent(nativeEvent) {
let isPropagationStopped = false;
const handlers = {
get(target, key) {
if (target.hasOwnProperty(key)) return Reflect.get(target, key);
if (typeof nativeEvent[key] === "function") {
return nativeEvent[key].bind(nativeEvent);
} else {
return nativeEvent[key];
}
},
};
const syntheticEvent = new Proxy(
{
nativeEvent,
preventDefault() {
if (nativeEvent.preventDefault) {
nativeEvent.preventDefault();
} else {
nativeEvent.returnValue = false;
}
},
stopPropagation() {
if (nativeEvent.stopPropagation) {
nativeEvent.stopPropagation();
} else {
nativeEvent.cancelBubble = true;
}
isPropagationStopped = true;
},
isPropagationStopped() {
return isPropagationStopped;
},
},
handlers
);
return syntheticEvent;
}
export default function setupEventDelegation(container) {
if (container._reactEventDelegated) return;
["capture", "bubble"].forEach((phase) => {
Reflect.ownKeys(eventTypeMethods).forEach((type) => {
container.addEventListener(
type,
(nativeEvent) => {
const syntheticEvent = createSyntheticEvent(nativeEvent);
const path = syntheticEvent.composedPath();
const methodName = eventTypeMethods[type][phase];
const elements = phase === "capture" ? path.reverse() : path;
+ setIsBatchingUpdates(true);
for (let element of elements) {
if (syntheticEvent.isPropagationStopped()) {
break;
}
element.reactEvents?.[methodName]?.(syntheticEvent);
}
+ flushDirtyComponents();
},
phase === "capture"
);
});
});
container._reactEventDelegated = true;
}
在 React 中,ref
(参照)是一个重要的属性,它允许我们直接访问 DOM 元素或组件实例。虽然 React 鼓励使用 props 和 state 来控制渲染和组件交互,但有时我们仍然需要直接操作 DOM,或者访问 React 组件实例上的某些方法。这时,ref
就显得特别有用。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class RefComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
handleButtonClick = () => {
this.inputRef.current.focus();
};
render() {
return (
<div>
<input
ref={this.inputRef}
type="text"
placeholder="Click button to focus me"
/>
<button onClick={this.handleButtonClick}>Focus Input</button>
</div>
);
}
}
const classElement = <RefComponent />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class RefComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
handleButtonClick = () => {
this.inputRef.current.focus();
};
render() {
return (
<div>
<input
ref={this.inputRef}
type="text"
placeholder="Click button to focus me"
/>
<button onClick={this.handleButtonClick}>Focus Input</button>
</div>
);
}
}
const classElement = <RefComponent />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, createDOMElement } from "./react-dom/client";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.forceUpdate());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
+ let { ref, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
+ ref,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
forceUpdate() {
this.state = this.accumulateState();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const newDOMElement = createDOMElement(renderVdom);
parentDOM.replaceChild(newDOMElement, oldDOMElement);
this.oldRenderVdom = renderVdom;
}
}
+function createRef() {
+ return {
+ current: null,
+ };
+}
const React = {
createElement,
Component,
+ createRef,
};
export default React;
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props } = vdom;
const classInstance = new type(props);
vdom.classInstance = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createNativeDOMElement(vdom) {
+ const { type, props, ref } = vdom;
const domElement = document.createElement(type);
+ if (ref) {
+ ref.current = domElement;
+ }
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
import React from "./react";
import ReactDOM from "./react-dom/client";
class ChildComponent extends React.Component {
alertMessage = () => {
alert("Hello from ChildComponent!");
};
render() {
return <div>I'm the child component.</div>;
}
}
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.childRef = React.createRef();
}
handleButtonClick = () => {
this.childRef.current.alertMessage();
};
render() {
return (
<div>
<ChildComponent ref={this.childRef} />
<button onClick={this.handleButtonClick}>Call Child Method</button>
</div>
);
}
}
const classElement = <ParentComponent />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
vdom.classInstance = classInstance;
+ if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
React.forwardRef
是 React 的一个功能,它允许你将 ref
从父组件传递到子组件。这在你需要在子组件内部访问 DOM 节点或者某个 React 元素时非常有用。React.forwardRef
是通过接收一个渲染函数来实现的,该函数接受 props
和 ref
为参数,然后返回一个 React 元素。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const ForwardedButton = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
class App extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return (
<div>
<ForwardedButton ref={this.inputRef} />
<button onClick={() => this.inputRef.current.focus()}>focus</button>
</div>
);
}
}
const classElement = <App />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, createDOMElement } from "./react-dom/client";
+import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.forceUpdate());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
let { ref, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
forceUpdate() {
this.state = this.accumulateState();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const newDOMElement = createDOMElement(renderVdom);
parentDOM.replaceChild(newDOMElement, oldDOMElement);
this.oldRenderVdom = renderVdom;
}
}
function createRef() {
return {
current: null,
};
}
+function forwardRef(render) {
+ return {
+ $$typeof: FORWARD_REF,
+ render,
+ };
+}
const React = {
createElement,
Component,
createRef,
+ forwardRef,
};
export default React;
src\constant.js
export const REACT_TEXT = Symbol.for("react.text");
+export const FORWARD_REF = Symbol.for("react.forward_ref");
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
+import { REACT_TEXT, FORWARD_REF } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
}
+export function createReactForwardDOMElement(vdom) {
+ const { type, props, ref } = vdom;
+ const renderVdom = type.render(props, ref);
+ vdom.oldRenderVdom = renderVdom;
+ return createDOMElement(renderVdom);
+}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
+ if (type.$$typeof === FORWARD_REF) {
+ return createReactForwardDOMElement(vdom);
+ } else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
React 的组件生命周期可以被大致分为三个主要阶段:Mounting(挂载),Updating(更新)和 Unmounting(卸载)。
this.props
和this.state
并返回以下类型之一:React 元素,字符串和数字,fragments,Portals,布尔值或 null。true
。这是一个优化性能的点,可以避免不必要的渲染。Unmounting(卸载): 当组件从 DOM 中移除时会调用。
componentDidMount
中创建的订阅。
此外,React 还引入了 Error Boundaries,用于捕获子组件树的 JS 错误,渲染备用 UI,而不是使整个组件树崩溃:static getDerivedStateFromError: 此生命周期被调用后,你可以渲染一个备用 UI。
componentWillMount
, componentWillReceiveProps
和componentWillUpdate
。推荐使用新的生命周期方法。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class Counter extends React.Component {
static defaultProps = {
name: "Counter",
};
constructor(props) {
super(props);
this.state = {
number: 0,
};
console.log("Counter 1.constructor");
}
componentWillMount() {
console.log("Counter 2.componentWillMount");
}
componentDidMount() {
console.log("Counter 4.componentDidMount");
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
shouldComponentUpdate(nextProps, nextState) {
console.log("Counter 5.shouldComponentUpdate");
return nextState.number % 2 === 0;
}
componentWillUpdate() {
console.log("Counter 6.componentWillUpdate");
}
componentDidUpdate() {
console.log("Counter 7.componentDidUpdate");
}
render() {
console.log("Counter 3.render");
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
const classElement = <Counter />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
/**
Counter 1.constructor
Counter 2.componentWillMount
Counter 3.render
Counter 4.componentDidMount
2Counter 5.shouldComponentUpdate
Counter 6.componentWillUpdate
Counter 3.render
Counter 7.componentDidUpdate
2Counter 5.shouldComponentUpdate
Counter 6.componentWillUpdate
Counter 3.render
Counter 7.componentDidUpdate
*/
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, createDOMElement } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
+ dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
let { ref, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
+ updateIfNeeded() {
+ const nextState = this.accumulateState();
+ const shouldUpdate = this.shouldComponentUpdate?.(
+ this.nextProps,
+ nextState
+ );
+ this.state = nextState;
+ if (shouldUpdate === false) return;
+ this.forceUpdate();
+ }
forceUpdate() {
+ this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const newDOMElement = createDOMElement(renderVdom);
parentDOM.replaceChild(newDOMElement, oldDOMElement);
this.oldRenderVdom = renderVdom;
+ this.componentDidUpdate?.(this.props, this.state);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
const React = {
createElement,
Component,
createRef,
forwardRef,
};
export default React;
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
+ domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
+ const classInstance = new type(props);
classInstance?.componentWillMount();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
+ const domElement = createDOMElement(renderVdom);
+ if (typeof classInstance.componentDidMount === "function") {
+ domElement.componentDidMount =
+ classInstance.componentDidMount.bind(classInstance);
+ }
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class Counter extends React.Component {
static defaultProps = {
name: "Counter",
};
constructor(props) {
super(props);
this.state = {
number: 0,
};
console.log("Counter 1.constructor");
}
componentWillMount() {
console.log("Counter 2.componentWillMount");
}
componentDidMount() {
console.log("Counter 4.componentDidMount");
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
shouldComponentUpdate(nextProps, nextState) {
console.log("Counter 5.shouldComponentUpdate");
return nextState.number % 2 === 0;
}
componentWillUpdate() {
console.log("Counter 6.componentWillUpdate");
}
componentDidUpdate() {
console.log("Counter 7.componentDidUpdate");
}
render() {
console.log("Counter 3.render");
return (
<div>
<p>{this.state.number}</p>
{this.state.number === 4 ? null : (
<ChildCounter count={this.state.number} />
)}
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
class ChildCounter extends React.Component {
componentWillUnmount() {
console.log(" ChildCounter 6.componentWillUnmount");
}
componentWillMount() {
console.log("ChildCounter 1.componentWillMount");
}
render() {
console.log("ChildCounter 2.render");
return <div>{this.props.count}</div>;
}
componentDidMount() {
console.log("ChildCounter 3.componentDidMount");
}
componentWillReceiveProps(newProps) {
console.log("ChildCounter 4.componentWillReceiveProps");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("ChildCounter 5.shouldComponentUpdate");
return nextProps.count % 3 === 0;
}
}
const classElement = <Counter />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
+import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
let { ref, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
const nextState = this.accumulateState();
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
+ if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
+ emitUpdate(nextProps) {
+ this.nextProps = nextProps;
+ if (this.nextProps || this.pendingStates.length > 0) {
+ this.updateIfNeeded();
+ }
+ }
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
+ compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
const React = {
createElement,
Component,
createRef,
forwardRef,
};
export default React;
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
+ vdom.domElement = domElement;
+ return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
+function updateReactTextComponent(oldVdom, newVdom) {
+ let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
+ if (oldVdom.props !== newVdom.props) {
+ domElement.textContent = newVdom.props;
+ }
+}
+function updateClassComponent(oldVdom, newVdom) {
+ let classInstance = (newVdom.classInstance = oldVdom.classInstance);
+ classInstance.componentWillReceiveProps?.(newVdom.props);
+ classInstance.emitUpdate(newVdom.props);
+}
+function updateNativeComponent(oldVdom, newVdom) {
+ let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
+ updateProps(domElement, oldVdom.props, newVdom.props);
+ updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
+}
+function updateChildren(parentDOM, oldVChildren, newVChildren) {
+ oldVChildren = wrapToArray(oldVChildren);
+ newVChildren = wrapToArray(newVChildren);
+ let maxLength = Math.max(oldVChildren.length, newVChildren.length);
+ for (let i = 0; i < maxLength; i++) {
+ const nextVdom = getNextVdom(oldVChildren, i);
+ compareVdom(
+ parentDOM,
+ oldVChildren[i],
+ newVChildren[i],
+ nextVdom && getDOMElementByVdom(nextVdom)
+ );
+ }
+}
+function getNextVdom(vChildren, startIndex) {
+ for (let i = startIndex + 1; i < vChildren.length; i++) {
+ if (vChildren[i] && getDOMElementByVdom(vChildren[i])) {
+ return vChildren[i];
+ }
+ }
+ return null;
+}
+function updateFunctionComponent(oldVdom, newVdom) {
+ let { type, props } = newVdom;
+ let newRenderVdom = type(props);
+ compareVdom(
+ getDOMElementByVdom(oldVdom).parentNode,
+ oldVdom.oldRenderVdom,
+ newRenderVdom
+ );
+ newVdom.oldRenderVdom = newRenderVdom;
+}
+function updateReactForwardComponent(oldVdom, newVdom) {
+ let { type, props, ref } = newVdom;
+ let renderVdom = type.render(props, ref);
+ compareVdom(
+ getDOMElementByVdom(oldVdom).parentNode,
+ oldVdom.oldRenderVdom,
+ renderVdom
+ );
+ newVdom.oldRenderVdom = renderVdom;
+}
+function updateVdom(oldVdom, newVdom) {
+ if (oldVdom.type.$$typeof === FORWARD_REF) {
+ return updateReactForwardComponent(oldVdom, newVdom);
+ } else if (oldVdom.type === REACT_TEXT) {
+ return updateReactTextComponent(oldVdom, newVdom);
+ } else if (typeof oldVdom.type === "string") {
+ return updateNativeComponent(oldVdom, newVdom);
+ } else if (typeof oldVdom.type === "function") {
+ if (oldVdom.type.isReactComponent) {
+ updateClassComponent(oldVdom, newVdom);
+ } else {
+ updateFunctionComponent(oldVdom, newVdom);
+ }
+ }
+}
+function unMountVdom(vdom) {
+ if (!vdom) return;
+ let { props, ref } = vdom;
+ let domElement = getDOMElementByVdom(vdom);
+ vdom?.classInstance?.componentWillUnmount();
+ if (ref) {
+ ref.current = null;
+ }
+ wrapToArray(props.children).forEach(unMountVdom);
+ domElement?.remove();
+}
+export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
+ if (!oldVdom && !newVdom) {
+ return;
+ } else if (!!oldVdom && !newVdom) {
+ unMountVdom(oldVdom);
+ } else if (!oldVdom && !!newVdom) {
+ let newDOMElement = createDOMElement(newVdom);
+ if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
+ else parentDOM.appendChild(newDOMElement);
+ newDOMElement?.componentDidMount?.();
+ } else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
+ let newDOMElement = createDOMElement(newVdom);
+ unMountVdom(oldVdom);
+ newDOMElement?.componentDidMount?.();
+ } else {
+ updateVdom(oldVdom, newVdom);
+ }
+}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
在 React 中,key
是一个特殊的属性(attribute)你应该在创建列表元素时包括它们。key
帮助 React 识别哪些项目已经改变,被添加,或者被移除。Keys 应当给予数组内的每一个元素一个独一无二的标识符。
这里是一些关于 key
的要点:
性能优化:
key
,React 可以追踪每个子元素的身份,在数据改变时有效地重新渲染和更新列表。稳定的身份:
id
作为 key
。如果你没有稳定的 id
,你可能会使用项目索引作为最后的手段,但这不推荐因为它可能导致性能问题和组件状态的问题。组件重排序:
key
在不同的渲染之间改变,React 会重新创建组件而不是更新它。与组件状态的关系:
key
,尤其是在使用局部状态或与其它生命周期相关的组件时,你可能会遇到一些意想不到的行为。例如,如果 key
不是稳定的,组件的状态可能会在重新渲染时丢失。为了给你更好的理解,这里有一个简单的例子。假设我们有一个任务列表,我们想要渲染出来:
const todoItems = todos.map((todo) => <li key={todo.id}>{todo.text}</li>);
在这个例子中,每个 todo
的 id
唯一地标识了一个列表元素。当 todos
数组更新时,React 将使用 id
来匹配老的 todos
与新的 todos
,从而决定哪些元素需要更新、添加或删除。
记住,只有当创建动态子元素列表时,key
才是必须的。如果你渲染一个静态列表,或者一个对象的子集,并且不关心是否会重新排序或更新,你可能不需要使用 key
。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class Hello extends React.Component {
render() {
return <p>hello</p>;
}
}
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ["A", "B", "C", "D", "E", "F"],
num: 1,
};
}
handleClick = () => {
this.setState({
list: ["A", "C", "E", "B", "G"],
num: 2,
});
};
render() {
return (
<div>
<div>
<Hello />
{this.state.list.map((item) => (
<div key={item} contentEditable>
{item}
</div>
))}
{this.state.num === 1 ? <Hello /> : null}
</div>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
const classElement = <Counter />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
+ let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
+ key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
const nextState = this.accumulateState();
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
const React = {
createElement,
Component,
createRef,
forwardRef,
};
export default React;
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
+function isSameVnode(oldVnode, newVnode) {
+ return (
+ oldVnode &&
+ newVnode &&
+ oldVnode.type === newVnode.type &&
+ oldVnode.key === newVnode.key
+ );
+}
+function updateChildren(parentDOM, oldVChildren, newVChildren) {
+ oldVChildren = wrapToArray(oldVChildren);
+ newVChildren = wrapToArray(newVChildren);
+ let lastPlaceNode = null;
+ for (let index = 0; index < newVChildren.length; index++) {
+ const newChild = newVChildren[index];
+ if (!newChild) continue;
+ const oldChildIndex = oldVChildren.findIndex((oldChild) =>
+ isSameVnode(oldChild, newChild)
+ );
+ const oldChild = oldVChildren[oldChildIndex];
+ if (oldChild) {
+ updateVdom(oldChild, newChild);
+ const oldDOMElement = getDOMElementByVdom(oldChild);
+ if (isDefined(lastPlaceNode)) {
+ if (lastPlaceNode.nextSibling !== oldDOMElement) {
+ parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
+ }
+ } else {
+ parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
+ }
+ lastPlaceNode = oldDOMElement;
+ oldVChildren.splice(oldChildIndex, 1);
+ } else {
+ const newDOMELement = createDOMElement(newChild);
+ if (isDefined(lastPlaceNode)) {
+ parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
+ } else {
+ parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
+ }
+ lastPlaceNode = newDOMELement;
+ }
+ }
+ oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
+}
function updateFunctionComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
getDerivedStateFromProps
是 React 的生命周期方法,专为 class 组件设计。它的主要目的是让组件在接收新的 props(属性)时有机会产生新的 state(状态)。这是一个静态方法,这意味着它不能访问组件实例(即不能使用this
关键字)。
以下是 getDerivedStateFromProps
的一些关键特点:
调用时机:该方法在组件实例化后和每次组件接收新的 props 时都会被调用。
参数:它接受两个参数:nextProps
和 prevState
。
nextProps
:组件将要接收的新属性。prevState
:组件当前的状态。返回值:它应该返回一个对象来更新状态,或者返回null
来表示不需要更新任何状态。
无副作用:此方法只应用于返回一个 state 更新的对象或 null
,而不应该有任何副作用(如发送 HTTP 请求或调用 setTimeout 等)。
静态方法:由于它是一个静态方法,所以不能访问 this
。
使用场景:当组件的状态依赖于 props 的更改时,可以考虑使用此方法。但是,大多数情况下,最好使用其他方法,如 componentDidUpdate
,或者完全避免将 props 映射到 state。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class ShoppingCart extends React.Component {
constructor(props) {
super(props);
this.state = {
itemCount: props.itemCount,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.itemCount !== prevState.itemCount) {
return {
itemCount: nextProps.itemCount,
};
}
return null;
}
render() {
return <div>Items in cart: {this.state.itemCount}</div>;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
itemCount: 0,
};
}
addItemToCart = () => {
this.setState((prevState) => ({
itemCount: prevState.itemCount + 1,
}));
};
render() {
return (
<div>
<h1>Online Store</h1>
<button onClick={this.addItemToCart}>Add Item to Cart</button>
<ShoppingCart itemCount={this.state.itemCount} />
</div>
);
}
}
const classElement = <App />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
+ let nextState = this.accumulateState();
+ if (this.constructor.getDerivedStateFromProps) {
+ const derivedState = this.constructor.getDerivedStateFromProps(
+ this.nextProps,
+ nextState
+ );
+ if (derivedState !== null) {
+ nextState = { ...nextState, ...derivedState };
+ }
+ }
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
const React = {
createElement,
Component,
createRef,
forwardRef,
};
export default React;
getSnapshotBeforeUpdate
是一个生命周期方法,它允许您在 DOM 的实际更改之前捕获一些信息(例如滚动位置)。然后,该信息可以传递给componentDidUpdate
方法,这样您就可以在更新之后使用这些信息。
一般用途:当您的组件渲染的内容可能会改变滚动位置(例如,一个正在增长的列表),并且您想在 React 更新 DOM 后保持当前的滚动位置时,这个生命周期方法非常有用。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class ScrollList extends React.Component {
constructor(props) {
super(props);
this.state = {
items: Array.from({ length: 5 }, (_, i) => i).reverse(),
};
this.listRef = React.createRef();
}
addMoreItem = () => {
this.setState((state) => ({
items: [state.items.length, ...state.items],
}));
};
componentDidMount() {
setInterval(() => {
this.addMoreItem();
}, 1000);
}
getSnapshotBeforeUpdate(prevProps, prevState) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
componentDidUpdate(prevProps, prevState, snapshot) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
render() {
return (
<div>
<button onClick={this.addMoreItem}>addMoreItem</button>
<ul
ref={this.listRef}
style={{
overflowY: "auto",
height: "150px",
border: "1px solid black",
}}
>
{this.state.items.map((item, index) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
}
const classElement = <ScrollList />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
+ const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
+ this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
const React = {
createElement,
Component,
createRef,
forwardRef,
};
export default React;
React 的Context
是一种传递数据的方法,允许数据能够被传递到组件树中的任何层级,而不必通过每一个层级的组件明确地传递。它被设计为解决当有许多层嵌套时,只是为了把数据传递给较低层级的组件而手动传递props
的问题。
使用Context
的步骤:
React.createContext
创建一个新的上下文。Context.Provider
组件为子组件提供上下文值。Context.Consumer
组件或useContext
hook 在任何子组件中访问上下文值。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const ThemeContext = React.createContext();
const { Provider, Consumer } = ThemeContext;
const commonStyle = {
margin: "5px",
padding: "5px",
};
function BorderBox(props) {
return (
<Consumer>
{(context) => (
<div
style={{
...commonStyle,
border: `5px solid ${context.color}`,
}}
>
{props.children}
</div>
)}
</Consumer>
);
}
function ThemedButton(props) {
return (
<Consumer>
{(context) => (
<button
style={{
color: props.color,
}}
onClick={() => context.changeColor(props.color)}
>
{props.label}
</button>
)}
</Consumer>
);
}
function Title() {
return <BorderBox>Title</BorderBox>;
}
class Header extends React.Component {
static contextType = ThemeContext;
render() {
return (
<BorderBox>
Header
<Title />
</BorderBox>
);
}
}
function Content() {
return (
<BorderBox>
Content
<ThemedButton color="red" label="变红" />
<ThemedButton color="green" label="变绿" />
</BorderBox>
);
}
class Main extends React.Component {
static contextType = ThemeContext;
render() {
return (
<BorderBox>
Main
<Content />
</BorderBox>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
color: "black",
};
}
changeColor = (color) => {
this.setState({
color,
});
};
render() {
const contextValue = {
color: this.state.color,
changeColor: this.changeColor,
};
return (
<Provider value={contextValue}>
<BorderBox>
Page
<Header />
<Main />
</BorderBox>
</Provider>
);
}
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src\react.js
import { wrapToVdom } from "./utils";
import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
+function createContext(defaultValue) {
+ const context = {
+ _currentValue: defaultValue,
+ Provider: function Provider(props) {
+ context._currentValue = props.value;
+ return props.children;
+ },
+ Consumer: function Consumer(props) {
+ return props.children(context._currentValue);
+ },
+ };
+ return context;
+}
const React = {
createElement,
Component,
createRef,
forwardRef,
+ createContext,
};
export default React;
PureComponent
是 React 提供的一个组件基类,它的核心特性是只有当它的 props 或 state 发生浅层变化时,它才会重新渲染。浅层比较会检查对象顶层的属性,而不是深度检查。如果对象的顶层属性没有变化,那么 PureComponent
就不会重新渲染,这可以提高性能。
使用 PureComponent
最适合于那些组件的 props 和 state 结构较简单,或者可以确保结构不会经常发生深度变化的场景。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
class RegularComponent extends React.Component {
render() {
console.log("Rendering RegularComponent");
return <div>Regular Component {this.props.value}</div>;
}
}
class PureComp extends React.PureComponent {
render() {
console.log("Rendering PureComp");
return <div>Pure Component {this.props.value}</div>;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {
value: 1,
},
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
data: this.state.data,
});
}, 2000);
}
render() {
return (
<div>
<RegularComponent value={this.state.data.value} />
<PureComp value={this.state.data.value} />
</div>
);
}
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src\react.js
+import { wrapToVdom, shallowEqual } from "./utils";
import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
import { FORWARD_REF } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
+ delete config.__self;
+ delete config.__source;
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
+ shouldComponentUpdate(nextProps, nextState) {
+ return true;
+ }
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
function createContext(defaultValue) {
const context = {
_currentValue: defaultValue,
Provider: function Provider(props) {
context._currentValue = props.value;
return props.children;
},
Consumer: function Consumer(props) {
return props.children(context._currentValue);
},
};
return context;
}
+class PureComponent extends Component {
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ !shallowEqual(this.props, nextProps) ||
+ !shallowEqual(this.state, nextState)
+ );
+ }
+}
const React = {
createElement,
Component,
createRef,
forwardRef,
createContext,
+ PureComponent,
};
export default React;
src\utils.js
import { REACT_TEXT } from "./constant";
export function isUndefined(v) {
return v === undefined || v === null;
}
export function isDefined(v) {
return v !== undefined && v !== null;
}
export function wrapToArray(value) {
return Array.isArray(value) ? value.flat() : [value];
}
export function wrapToVdom(element) {
return typeof element === "string" || typeof element === "number"
? { type: REACT_TEXT, props: element }
: element;
}
+export function shallowEqual(obj1, obj2) {
+ if (obj1 === obj2) {
+ return true;
+ }
+ if (
+ typeof obj1 !== "object" ||
+ obj1 === null ||
+ typeof obj2 !== "object" ||
+ obj2 === null
+ ) {
+ return false;
+ }
+ const keys1 = Object.keys(obj1);
+ const keys2 = Object.keys(obj2);
+ if (keys1.length !== keys2.length) {
+ return false;
+ }
+ for (let key of keys1) {
+ if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
+ return false;
+ }
+ }
+ return true;
+}
React.memo
是一个高阶组件,它可以用于优化那些仅仅依赖于其 props 变化的组件的重新渲染行为。换句话说,如果组件的 props 在连续的渲染之间没有发生变化,那么使用React.memo
可以避免组件的不必要的重新渲染。
如果你有一个功能组件并且你想避免该组件因父组件的重新渲染而不必要地重新渲染,你可以通过React.memo
包裹该组件:
工作原理
React.memo
包裹时,React 会记住该组件上一次渲染的结果。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
function Counter(props) {
console.log("Rendering Counter");
return <h1>{props.count}</h1>;
}
const MemoCounter = React.memo(Counter);
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
incrementCount = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<MemoCounter count={this.state.count} />
<button onClick={this.incrementCount}>Increase Count</button>
</div>
);
}
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src\react.js
import { wrapToVdom, shallowEqual } from "./utils";
import { getDOMElementByVdom, compareVdom } from "./react-dom/client";
+import { FORWARD_REF, REACT_MEMO } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
delete config.__self;
delete config.__source;
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
shouldComponentUpdate(nextProps, nextState) {
return true;
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
function createContext(defaultValue) {
const context = {
_currentValue: defaultValue,
Provider: function Provider(props) {
context._currentValue = props.value;
return props.children;
},
Consumer: function Consumer(props) {
return props.children(context._currentValue);
},
};
return context;
}
class PureComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState)
);
}
}
+function memo(render, compare = shallowEqual) {
+ return {
+ $$typeof: REACT_MEMO,
+ render,
+ compare,
+ };
+}
const React = {
createElement,
Component,
createRef,
forwardRef,
createContext,
PureComponent,
+ memo,
};
export default React;
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
+import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
function createRoot(container) {
return {
render(rootVdom) {
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
+export function createReactMemoDOMElement(vdom) {
+ const { type, props } = vdom;
+ const renderVdom = type.render(props);
+ vdom.oldRenderVdom = renderVdom;
+ return createDOMElement(renderVdom);
+}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
+ if (type.$$typeof === REACT_MEMO) {
+ return createReactMemoDOMElement(vdom);
+ } else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
+ if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateFunctionComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
+function updateReactMemoComponent(oldVdom, newVdom) {
+ let { type, props } = newVdom;
+ const { render, compare } = type;
+ if (compare(props, oldVdom.props)) {
+ newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
+ return;
+ }
+ let renderVdom = render(props);
+ compareVdom(
+ getDOMElementByVdom(oldVdom).parentNode,
+ oldVdom.oldRenderVdom,
+ renderVdom
+ );
+ newVdom.oldRenderVdom = renderVdom;
+}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\constant.js
export const REACT_TEXT = Symbol.for("react.text");
export const FORWARD_REF = Symbol.for("react.forward_ref");
+export const REACT_MEMO = Symbol.for("react.memo");
useReducer
是 React Hooks 中的一个非常有用的 Hook,它是一个更复杂的版本的 useState
。当你的 state 逻辑变得更加复杂或需要之前的状态来计算下一个状态时,useReducer
是非常有用的。
基本使用
useReducer
接受一个 reducer 函数和一个初始状态作为参数,返回当前的 state 和一个与该 reducer 函数关联的 dispatch 方法。
基本的 useReducer
使用方法如下:
const [state, dispatch] = useReducer(reducer, initialState);
为什么使用 useReducer
?
更加可预测: 由于它是基于 reducer 的,因此 useReducer
允许你的 state 逻辑以更可预测的方式运行,这在处理更复杂的 state 逻辑时特别有用。
更好的组织: 当有多种状态更新逻辑时,使用 useReducer
可以帮助你更好地组织代码。
中间件和增强器: 像 Redux 这样的库允许你使用中间件来增强 reducer 的功能。虽然 React 的 useReducer
没有这个功能,但你可以模仿类似的行为。
更好地处理副作用: 当与 useEffect
结合使用时,useReducer
可以更好地处理和 orchestrate 副作用。
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<Counter />);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
+let currentRoot = null;
+let currentRootVdom = null;
+let currentVdom = null;
+export function useReducer(reducer, initialState) {
+ const { hooks } = currentVdom;
+ const { hookIndex, hookStates } = hooks;
+ const hookState = hookStates[hookIndex];
+ if (isUndefined(hookState)) {
+ hookStates[hookIndex] = initialState;
+ }
+ function dispatch(action) {
+ hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
+ currentRoot?.update();
+ }
+ return [hookStates[hooks.hookIndex++], dispatch];
+}
function createRoot(container) {
return {
render(rootVdom) {
+ currentRoot = this;
+ currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
+ update() {
+ compareVdom(container, currentRootVdom, currentRootVdom);
+ },
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createReactMemoDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type.render(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
+ vdom.hooks = {
+ hookIndex: 0,
+ hookStates: [],
+ };
+ currentVdom = vdom;
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateFunctionComponent(oldVdom, newVdom) {
+ const hooks = (newVdom.hooks = oldVdom.hooks);
+ hooks.hookIndex = 0;
+ currentVdom = newVdom;
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\react.js
import { wrapToVdom, shallowEqual } from "./utils";
+import * as client from "./react-dom/client";
import { FORWARD_REF, REACT_MEMO } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
delete config.__self;
delete config.__source;
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
shouldComponentUpdate(nextProps, nextState) {
return true;
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
function createContext(defaultValue) {
const context = {
_currentValue: defaultValue,
Provider: function Provider(props) {
context._currentValue = props.value;
return props.children;
},
Consumer: function Consumer(props) {
return props.children(context._currentValue);
},
};
return context;
}
class PureComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState)
);
}
}
function memo(render, compare = shallowEqual) {
return {
$$typeof: REACT_MEMO,
render,
compare,
};
}
const React = {
createElement,
Component,
createRef,
forwardRef,
createContext,
PureComponent,
memo,
+ ...client,
};
export default React;
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
function Counter1() {
const [number1, setNumber1] = React.useState(0);
const [number2, setNumber2] = React.useState(0);
let handleClick1 = () => setNumber1(number1 + 1);
let handleClick2 = () => setNumber2(number2 + 1);
return (
<div>
<p>{number1}</p>
<button onClick={handleClick1}>+</button>
<p>{number2}</p>
<button onClick={handleClick2}>+</button>
</div>
);
}
function Counter2() {
const [number1, setNumber1] = React.useState(0);
const [number2, setNumber2] = React.useState(0);
let handleClick1 = () => setNumber1(number1 + 1);
let handleClick2 = () => setNumber2(number2 + 1);
return (
<div>
<p>{number1}</p>
<button onClick={handleClick1}>+</button>
<p>{number2}</p>
<button onClick={handleClick2}>+</button>
</div>
);
}
let number = 1;
function App() {
number += 1;
return (
<div>
{number % 2 === 0 ? <Counter1></Counter1> : null}
<hr />
<Counter2></Counter2>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
let currentRoot = null;
let currentRootVdom = null;
let currentVdom = null;
export function useReducer(reducer, initialState) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const hookState = hookStates[hookIndex];
if (isUndefined(hookState)) {
hookStates[hookIndex] = initialState;
}
function dispatch(action) {
hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
currentRoot?.update();
}
return [hookStates[hooks.hookIndex++], dispatch];
}
+export function useState(initialState) {
+ return useReducer((oldState, newState) => {
+ return typeof newState === "function" ? newState(oldState) : newState;
+ }, initialState);
+}
function createRoot(container) {
return {
render(rootVdom) {
currentRoot = this;
currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
update() {
compareVdom(container, currentRootVdom, currentRootVdom);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createReactMemoDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type.render(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
vdom.hooks = {
hookIndex: 0,
hookStates: [],
};
currentVdom = vdom;
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateFunctionComponent(oldVdom, newVdom) {
const hooks = (newVdom.hooks = oldVdom.hooks);
hooks.hookIndex = 0;
currentVdom = newVdom;
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
首先,我要解释 useMemo
和 useCallback
的原理。
useMemo
:返回一个 memoized 值。只有当依赖项改变时,它才会重新计算这个值。useCallback
:返回一个 memoized 的 callback。它会返回一个不变的函数,直到依赖项改变。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const ChildButton = React.memo(({ displayValue, onClickHandler }) => {
console.log("ChildButton render");
return <button onClick={onClickHandler}>{displayValue}</button>;
});
function App() {
console.log("App render");
const [userName, setUserName] = React.useState("zhufeng");
const [count, setCount] = React.useState(0);
const displayData = React.useMemo(
() => ({
number: count,
}),
[count]
);
const incrementCount = React.useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<input
type="text"
value={userName}
onChange={({ target: { value } }) => setUserName(value)}
/>
<ChildButton
displayValue={displayData.number}
onClickHandler={incrementCount}
/>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
let currentRoot = null;
let currentRootVdom = null;
let currentVdom = null;
export function useReducer(reducer, initialState) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const hookState = hookStates[hookIndex];
if (isUndefined(hookState)) {
hookStates[hookIndex] = initialState;
}
function dispatch(action) {
hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
currentRoot?.update();
}
return [hookStates[hooks.hookIndex++], dispatch];
}
export function useState(initialState) {
return useReducer((oldState, newState) => {
return typeof newState === "function" ? newState(oldState) : newState;
}, initialState);
}
+export function useMemo(factory, deps) {
+ const { hooks } = currentVdom;
+ const { hookIndex, hookStates } = hooks;
+ const prevHook = hookStates[hookIndex];
+ if (prevHook) {
+ const [prevMemo, prevDeps] = prevHook;
+ if (deps.every((dep, i) => dep === prevDeps[i])) {
+ hooks.hookIndex++;
+ return prevMemo;
+ }
+ }
+ const newMemo = factory();
+ hookStates[hookIndex] = [newMemo, deps];
+ hooks.hookIndex++;
+ return newMemo;
+}
+export function useCallback(callback, deps) {
+ return useMemo(() => callback, deps);
+}
function createRoot(container) {
return {
render(rootVdom) {
currentRoot = this;
currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
update() {
compareVdom(container, currentRootVdom, currentRootVdom);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createReactMemoDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type.render(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
vdom.hooks = {
hookIndex: 0,
hookStates: [],
};
currentVdom = vdom;
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateFunctionComponent(oldVdom, newVdom) {
const hooks = (newVdom.hooks = oldVdom.hooks);
hooks.hookIndex = 0;
currentVdom = newVdom;
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
React.useContext
是 React 的一个 Hook,它允许你无需明确地传递 props,就能让组件订阅 context 的变化。
为了理解 useContext
,首先你需要知道 React 的 Context
API。Context
提供了一种在组件之间共享此类值的方式,而不必明确地通过组件树的每个层级传递 props。
Context
主要由两个核心组件组成:Provider
和 Consumer
。
Provider
读取到当前的 context 值。然而,在函数组件中,你不必使用 Consumer
。你可以简单地使用 useContext
Hook
src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const MyContext = React.createContext("defaultValue");
function App() {
return (
<MyContext.Provider value="Hello from context!">
<Child />
</MyContext.Provider>
);
}
function Child() {
const value = React.useContext(MyContext);
return <div>{value}</div>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
src\react.js
import { wrapToVdom, shallowEqual } from "./utils";
import * as client from "./react-dom/client";
import { FORWARD_REF, REACT_MEMO } from "./constant";
let isBatchingUpdates = false;
let dirtyComponents = new Set();
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
export function flushDirtyComponents() {
dirtyComponents.forEach((component) => component.updateIfNeeded());
dirtyComponents.clear();
isBatchingUpdates = false;
}
function createElement(type, config, children) {
delete config.__self;
delete config.__source;
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
shouldComponentUpdate(nextProps, nextState) {
return true;
}
setState(partialState) {
if (isBatchingUpdates) {
dirtyComponents.add(this);
this.pendingStates.push(partialState);
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
this.forceUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
const oldDOMElement = getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
function createContext(defaultValue) {
const context = {
_currentValue: defaultValue,
Provider: function Provider(props) {
context._currentValue = props.value;
return props.children;
},
Consumer: function Consumer(props) {
return props.children(context._currentValue);
},
};
return context;
}
class PureComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState)
);
}
}
function memo(render, compare = shallowEqual) {
return {
$$typeof: REACT_MEMO,
render,
compare,
};
}
+function useContext(context) {
+ return context._currentValue;
+}
const React = {
createElement,
Component,
createRef,
forwardRef,
createContext,
PureComponent,
memo,
+ useContext,
...client,
};
export default React;
React.useEffect
是 React 的一个 Hook,允许你在函数组件中执行有副作用的操作。它与类组件中的生命周期方法类似,如 componentDidMount
、componentDidUpdate
和 componentWillUnmount
,但更加强大和灵活。
使用方法:
useEffect(() => {
// 副作用操作
return () => {
// 清除副作用,类似于 componentWillUnmount
};
}, [依赖项]);
参数解释
useEffect
只会在这些依赖发生变化时执行。注意事项
useEffect
:它应该总是在你的 React 函数的顶层被调用。useEffect
可以返回一个函数,这个函数会在组件卸载前或重新执行副作用前被调用。这是清除副作用(如事件监听器、取消网络请求、清除定时器)的好地方。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
function Counter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("开启一个新的定时器");
const timerId = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => {
console.log("销毁老的定时器");
clearInterval(timerId);
};
});
return <p>{count}</p>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<Counter />);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
let currentRoot = null;
let currentRootVdom = null;
let currentVdom = null;
export function useReducer(reducer, initialState) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const hookState = hookStates[hookIndex];
if (isUndefined(hookState)) {
hookStates[hookIndex] = initialState;
}
function dispatch(action) {
hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
currentRoot?.update();
}
return [hookStates[hooks.hookIndex++], dispatch];
}
export function useState(initialState) {
return useReducer((oldState, newState) => {
return typeof newState === "function" ? newState(oldState) : newState;
}, initialState);
}
export function useMemo(factory, deps) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const prevHook = hookStates[hookIndex];
if (prevHook) {
const [prevMemo, prevDeps] = prevHook;
if (deps.every((dep, i) => dep === prevDeps[i])) {
hooks.hookIndex++;
return prevMemo;
}
}
const newMemo = factory();
hookStates[hookIndex] = [newMemo, deps];
hooks.hookIndex++;
return newMemo;
}
export function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
+export function applyEffectHook(effect, dependencies, scheduleTask) {
+ const { hooks } = currentVdom;
+ const { hookIndex, hookStates } = hooks;
+ const previousHookState = hookStates[hookIndex];
+ let shouldRunEffect = true;
+ let previousCleanup;
+ if (previousHookState) {
+ const { cleanup, prevDeps } = previousHookState;
+ previousCleanup = cleanup;
+ if (dependencies) {
+ shouldRunEffect = dependencies.some(
+ (dep, index) => !Object.is(dep, prevDeps[index])
+ );
+ }
+ }
+ if (shouldRunEffect) {
+ scheduleTask(() => {
+ previousCleanup?.();
+ const cleanup = effect();
+ hookStates[hookIndex] = { cleanup, prevDeps: dependencies };
+ });
+ }
+ hooks.hookIndex++;
+}
+export function useEffect(effect, deps) {
+ applyEffectHook(effect, deps, setTimeout);
+}
function createRoot(container) {
return {
render(rootVdom) {
currentRoot = this;
currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
update() {
compareVdom(container, currentRootVdom, currentRootVdom);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createReactMemoDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type.render(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
vdom.hooks = {
hookIndex: 0,
hookStates: [],
};
currentVdom = vdom;
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateFunctionComponent(oldVdom, newVdom) {
const hooks = (newVdom.hooks = oldVdom.hooks);
hooks.hookIndex = 0;
currentVdom = newVdom;
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
useLayoutEffect
是 React 的一个 Hook,它和 useEffect
有相同的函数签名,但它有一些关键的差异。让我们深入了解这两者之间的差异以及 useLayoutEffect
的工作原理:
执行时机:
useEffect
: 是在浏览器绘制后异步执行的,这意味着执行这个 effect 可能会导致延迟。useLayoutEffect
: 是在浏览器执行绘制之前同步执行的。由于这个原因,如果你在 useLayoutEffect
中执行一些会导致额外绘制的代码(例如修改 DOM),它不会导致额外的浏览器绘制。用途:
useLayoutEffect
。这通常用于读取例如布局或尺寸等与视觉相关的属性。useEffect
更为合适。useRef
是 React 提供的一个 Hook,它返回一个可变的 ref 对象,这个对象的 .current
属性可以被修改,并且它不会导致组件重新渲染。
下面是 useRef
的一些核心点:
创建 Ref: 当你调用 useRef()
,它会返回一个像这样的对象: { current: initialValue }
。你可以为它提供一个初始值,例如 useRef(0)
,但这并不是必需的。
持久性: 不像 useState
,每次组件重新渲染时都会返回一个新的 state,useRef
会在整个组件生命周期中保持其对象不变。
使用场景:
useRef
创建一个 ref,然后将它附加到 JSX 元素上,从而在组件内部访问该 DOM 元素。useRef
来跟踪前一个渲染周期中的值。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const Animation = () => {
const ref = React.useRef();
React.useLayoutEffect(() => {
ref.current.style.transform = `translate(500px)`;
ref.current.style.transition = `all 500ms`;
});
let style = {
width: "100px",
height: "100px",
borderRadius: "50%",
backgroundColor: "red",
};
return <div style={style} ref={ref}></div>;
};
ReactDOM.createRoot(document.getElementById("root")).render(<Animation />);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
let currentRoot = null;
let currentRootVdom = null;
let currentVdom = null;
export function useReducer(reducer, initialState) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const hookState = hookStates[hookIndex];
if (isUndefined(hookState)) {
hookStates[hookIndex] = initialState;
}
function dispatch(action) {
hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
currentRoot?.update();
}
return [hookStates[hooks.hookIndex++], dispatch];
}
export function useState(initialState) {
return useReducer((oldState, newState) => {
return typeof newState === "function" ? newState(oldState) : newState;
}, initialState);
}
export function useMemo(factory, deps) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const prevHook = hookStates[hookIndex];
if (prevHook) {
const [prevMemo, prevDeps] = prevHook;
if (deps.every((dep, i) => dep === prevDeps[i])) {
hooks.hookIndex++;
return prevMemo;
}
}
const newMemo = factory();
hookStates[hookIndex] = [newMemo, deps];
hooks.hookIndex++;
return newMemo;
}
export function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
export function applyEffectHook(effect, dependencies, scheduleTask) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const previousHookState = hookStates[hookIndex];
let shouldRunEffect = true;
let previousCleanup;
if (previousHookState) {
const { cleanup, prevDeps } = previousHookState;
previousCleanup = cleanup;
if (dependencies) {
shouldRunEffect = dependencies.some(
(dep, index) => !Object.is(dep, prevDeps[index])
);
}
}
if (shouldRunEffect) {
scheduleTask(() => {
previousCleanup?.();
const cleanup = effect();
hookStates[hookIndex] = { cleanup, prevDeps: dependencies };
});
}
hooks.hookIndex++;
}
export function useEffect(effect, deps) {
applyEffectHook(effect, deps, setTimeout);
}
+export function useLayoutEffect(effect, deps) {
+ applyEffectHook(effect, deps, queueMicrotask);
+}
+export function useRef(initialValue) {
+ const { hooks } = currentVdom;
+ const { hookIndex, hookStates } = hooks;
+ if (!hookStates[hookIndex]) {
+ hookStates[hookIndex] = { current: initialValue };
+ }
+ return hookStates[hooks.hookIndex++];
+}
function createRoot(container) {
return {
render(rootVdom) {
currentRoot = this;
currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
update() {
compareVdom(container, currentRootVdom, currentRootVdom);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createReactMemoDOMElement(vdom) {
const { type, props } = vdom;
const renderVdom = type.render(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function createFunctionDOMElement(vdom) {
vdom.hooks = {
hookIndex: 0,
hookStates: [],
};
currentVdom = vdom;
const { type, props } = vdom;
const renderVdom = type(props);
vdom.oldRenderVdom = renderVdom;
return createDOMElement(renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateFunctionComponent(oldVdom, newVdom) {
const hooks = (newVdom.hooks = oldVdom.hooks);
hooks.hookIndex = 0;
currentVdom = newVdom;
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
useImperativeHandle
是 React 的一个高级 Hook,通常与 forwardRef
配合使用。它允许你在使用 ref 时,自定义暴露给父组件的实例值,而不是组件的默认实例。
这个 Hook 主要在你想给组件的外部用户更多的控制权,或当你想隐藏一些组件内部的细节时使用。
使用方法
useImperativeHandle(ref, createHandle, [deps]);
ref
: 通常是从 forwardRef
传递过来的。createHandle
: 一个函数,返回一个对象,这个对象的内容将被暴露给 ref。[deps]
: (可选)依赖数组,只有在这些依赖发生变化时,才会重新定义 ref 的实例值。src\index.js
import React from "./react";
import ReactDOM from "./react-dom/client";
const FancyInput = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return <input ref={inputRef} {...props} />;
});
function ParentComponent() {
const inputRef = React.useRef();
const handleButtonClick = () => {
inputRef.current.focus();
};
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={handleButtonClick}>Focus the input</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(
<ParentComponent />
);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
let currentRoot = null;
let currentRootVdom = null;
let currentVdom = null;
export function useReducer(reducer, initialState) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const hookState = hookStates[hookIndex];
if (isUndefined(hookState)) {
hookStates[hookIndex] = initialState;
}
function dispatch(action) {
hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
currentRoot?.update();
}
return [hookStates[hooks.hookIndex++], dispatch];
}
export function useState(initialState) {
return useReducer((oldState, newState) => {
return typeof newState === "function" ? newState(oldState) : newState;
}, initialState);
}
export function useMemo(factory, deps) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const prevHook = hookStates[hookIndex];
if (prevHook) {
const [prevMemo, prevDeps] = prevHook;
if (deps.every((dep, i) => dep === prevDeps[i])) {
hooks.hookIndex++;
return prevMemo;
}
}
const newMemo = factory();
hookStates[hookIndex] = [newMemo, deps];
hooks.hookIndex++;
return newMemo;
}
export function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
export function applyEffectHook(effect, dependencies, scheduleTask) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const previousHookState = hookStates[hookIndex];
let shouldRunEffect = true;
let previousCleanup;
if (previousHookState) {
const { cleanup, prevDeps } = previousHookState;
previousCleanup = cleanup;
if (dependencies) {
shouldRunEffect = dependencies.some(
(dep, index) => !Object.is(dep, prevDeps[index])
);
}
}
if (shouldRunEffect) {
scheduleTask(() => {
previousCleanup?.();
const cleanup = effect();
hookStates[hookIndex] = { cleanup, prevDeps: dependencies };
});
}
hooks.hookIndex++;
}
export function useEffect(effect, deps) {
applyEffectHook(effect, deps, setTimeout);
}
export function useLayoutEffect(effect, deps) {
applyEffectHook(effect, deps, queueMicrotask);
}
export function useRef(initialValue) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
if (!hookStates[hookIndex]) {
hookStates[hookIndex] = { current: initialValue };
}
return hookStates[hooks.hookIndex++];
}
+export function useImperativeHandle(ref, handler) {
+ ref.current = handler();
+}
function createRoot(container) {
return {
render(rootVdom) {
currentRoot = this;
currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
update() {
compareVdom(container, currentRootVdom, currentRootVdom);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
+ initializeHooks(vdom);
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
+ return finalizeHooks(vdom, renderVdom);
}
export function createReactMemoDOMElement(vdom) {
+ initializeHooks(vdom);
const { type, props } = vdom;
const renderVdom = type.render(props);
+ return finalizeHooks(vdom, renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
+function initializeHooks(vdom) {
+ vdom.hooks = {
+ hookIndex: 0,
+ hookStates: [],
+ };
+ currentVdom = vdom;
+}
+function finalizeHooks(vdom, renderVdom) {
+ vdom.oldRenderVdom = renderVdom;
+ return createDOMElement(renderVdom);
+}
function createFunctionDOMElement(vdom) {
+ initializeHooks(vdom);
const { type, props } = vdom;
const renderVdom = type(props);
+ return finalizeHooks(vdom, renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
+function updateHook(oldVdom, newVdom) {
+ const hooks = (newVdom.hooks = oldVdom.hooks);
+ hooks.hookIndex = 0;
+ currentVdom = newVdom;
+}
function updateFunctionComponent(oldVdom, newVdom) {
+ updateHook(oldVdom, newVdom);
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
+ updateHook(oldVdom, newVdom);
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
+ updateHook(oldVdom, newVdom);
let { type, props, ref } = newVdom;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\index.js
import React from "react";
import ReactDOM from "react-dom/client";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);
this.setState({ number: this.state.number + 1 });
console.log(this.state);
setTimeout(() => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);
this.setState({ number: this.state.number + 1 });
console.log(this.state);
});
};
render() {
return <button onClick={this.handleClick}>{this.state.number}</button>;
}
}
const classElement = <Counter />;
ReactDOM.createRoot(document.getElementById("root")).render(classElement);
src\react-dom\client.js
import setupEventDelegation from "./event";
import { isDefined, isUndefined, wrapToArray } from "../utils";
import { REACT_TEXT, FORWARD_REF, REACT_MEMO } from "../constant";
+export let currentRoot = null;
let currentRootVdom = null;
let currentVdom = null;
export function useReducer(reducer, initialState) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const hookState = hookStates[hookIndex];
if (isUndefined(hookState)) {
hookStates[hookIndex] = initialState;
}
function dispatch(action) {
hookStates[hookIndex] = reducer(hookStates[hookIndex], action);
currentRoot?.update();
}
return [hookStates[hooks.hookIndex++], dispatch];
}
export function useState(initialState) {
return useReducer((oldState, newState) => {
return typeof newState === "function" ? newState(oldState) : newState;
}, initialState);
}
export function useMemo(factory, deps) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const prevHook = hookStates[hookIndex];
if (prevHook) {
const [prevMemo, prevDeps] = prevHook;
if (deps.every((dep, i) => dep === prevDeps[i])) {
hooks.hookIndex++;
return prevMemo;
}
}
const newMemo = factory();
hookStates[hookIndex] = [newMemo, deps];
hooks.hookIndex++;
return newMemo;
}
export function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
export function applyEffectHook(effect, dependencies, scheduleTask) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
const previousHookState = hookStates[hookIndex];
let shouldRunEffect = true;
let previousCleanup;
if (previousHookState) {
const { cleanup, prevDeps } = previousHookState;
previousCleanup = cleanup;
if (dependencies) {
shouldRunEffect = dependencies.some(
(dep, index) => !Object.is(dep, prevDeps[index])
);
}
}
if (shouldRunEffect) {
scheduleTask(() => {
previousCleanup?.();
const cleanup = effect();
hookStates[hookIndex] = { cleanup, prevDeps: dependencies };
});
}
hooks.hookIndex++;
}
export function useEffect(effect, deps) {
applyEffectHook(effect, deps, setTimeout);
}
export function useLayoutEffect(effect, deps) {
applyEffectHook(effect, deps, queueMicrotask);
}
export function useRef(initialValue) {
const { hooks } = currentVdom;
const { hookIndex, hookStates } = hooks;
if (!hookStates[hookIndex]) {
hookStates[hookIndex] = { current: initialValue };
}
return hookStates[hooks.hookIndex++];
}
export function useImperativeHandle(ref, handler) {
ref.current = handler();
}
function createRoot(container) {
return {
render(rootVdom) {
currentRoot = this;
currentRootVdom = rootVdom;
mountVdom(rootVdom, container);
setupEventDelegation(container);
},
update() {
compareVdom(container, currentRootVdom, currentRootVdom);
},
};
}
export function mountVdom(vdom, container) {
const domElement = createDOMElement(vdom);
if (domElement === null) return;
container.appendChild(domElement);
domElement?.componentDidMount?.();
}
export function createReactForwardDOMElement(vdom) {
initializeHooks(vdom);
const { type, props, ref } = vdom;
const renderVdom = type.render(props, ref);
return finalizeHooks(vdom, renderVdom);
}
export function createReactMemoDOMElement(vdom) {
initializeHooks(vdom);
const { type, props } = vdom;
const renderVdom = type.render(props);
return finalizeHooks(vdom, renderVdom);
}
export function createDOMElement(vdom) {
if (isUndefined(vdom)) return null;
const { type } = vdom;
if (type.$$typeof === REACT_MEMO) {
return createReactMemoDOMElement(vdom);
} else if (type.$$typeof === FORWARD_REF) {
return createReactForwardDOMElement(vdom);
} else if (type === REACT_TEXT) {
return createTextDOMElement(vdom);
} else if (typeof type === "function") {
if (type.isReactComponent) {
return createClassDOMElement(vdom);
} else {
return createFunctionDOMElement(vdom);
}
} else {
return createNativeDOMElement(vdom);
}
}
function createTextDOMElement(vdom) {
const { props } = vdom;
const domElement = document.createTextNode(props);
vdom.domElement = domElement;
return domElement;
}
function initializeHooks(vdom) {
vdom.hooks = {
hookIndex: 0,
hookStates: [],
};
currentVdom = vdom;
}
function finalizeHooks(vdom, renderVdom) {
vdom.oldRenderVdom = renderVdom;
currentVdom = null;
return createDOMElement(renderVdom);
}
function createFunctionDOMElement(vdom) {
initializeHooks(vdom);
const { type, props } = vdom;
const renderVdom = type(props);
return finalizeHooks(vdom, renderVdom);
}
function createClassDOMElement(vdom) {
const { type, props, ref } = vdom;
const classInstance = new type(props);
classInstance.componentWillMount?.();
vdom.classInstance = classInstance;
if (ref) ref.current = classInstance;
const renderVdom = classInstance.render();
classInstance.oldRenderVdom = renderVdom;
const domElement = createDOMElement(renderVdom);
if (typeof classInstance.componentDidMount === "function") {
domElement.componentDidMount =
classInstance.componentDidMount.bind(classInstance);
}
return domElement;
}
function createNativeDOMElement(vdom) {
const { type, props, ref } = vdom;
const domElement = document.createElement(type);
if (ref) {
ref.current = domElement;
}
updateProps(domElement, {}, props);
mountChildren(vdom, domElement);
vdom.domElement = domElement;
return domElement;
}
function mountChildren(vdom, container) {
wrapToArray(vdom?.props?.children).forEach((child) =>
mountVdom(child, container)
);
}
function updateProps(domElement, oldProps = {}, newProps = {}) {
Object.keys(oldProps).forEach((name) => {
if (!newProps.hasOwnProperty(name) || name === "children") {
if (name === "style") {
Object.keys(oldProps.style).forEach((styleProp) => {
domElement.style[styleProp] = "";
});
} else if (name.startsWith("on")) {
delete domElement.reactEvents[name];
} else {
delete domElement[name];
}
}
});
Object.keys(newProps).forEach((name) => {
if (name === "children") {
return;
}
if (name === "style") {
Object.assign(domElement.style, newProps.style);
} else if (name.startsWith("on")) {
(domElement.reactEvents || (domElement.reactEvents = {}))[name] =
newProps[name];
} else {
domElement[name] = newProps[name];
}
});
}
export function getDOMElementByVdom(vdom) {
if (isUndefined(vdom)) return null;
let { type } = vdom;
if (typeof type === "function" || typeof type.render === "function") {
if (type.isReactComponent) {
return getDOMElementByVdom(vdom.classInstance.oldRenderVdom);
} else {
return getDOMElementByVdom(vdom.oldRenderVdom);
}
} else {
return vdom.domElement;
}
}
function updateReactTextComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
if (oldVdom.props !== newVdom.props) {
domElement.textContent = newVdom.props;
}
}
function updateClassComponent(oldVdom, newVdom) {
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
classInstance.componentWillReceiveProps?.(newVdom.props);
classInstance.emitUpdate(newVdom.props);
}
function updateNativeComponent(oldVdom, newVdom) {
let domElement = (newVdom.domElement = getDOMElementByVdom(oldVdom));
updateProps(domElement, oldVdom.props, newVdom.props);
updateChildren(domElement, oldVdom.props.children, newVdom.props.children);
}
function isSameVnode(oldVnode, newVnode) {
return (
oldVnode &&
newVnode &&
oldVnode.type === newVnode.type &&
oldVnode.key === newVnode.key
);
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = wrapToArray(oldVChildren);
newVChildren = wrapToArray(newVChildren);
let lastPlaceNode = null;
for (let index = 0; index < newVChildren.length; index++) {
const newChild = newVChildren[index];
if (!newChild) continue;
const oldChildIndex = oldVChildren.findIndex((oldChild) =>
isSameVnode(oldChild, newChild)
);
const oldChild = oldVChildren[oldChildIndex];
if (oldChild) {
updateVdom(oldChild, newChild);
const oldDOMElement = getDOMElementByVdom(oldChild);
if (isDefined(lastPlaceNode)) {
if (lastPlaceNode.nextSibling !== oldDOMElement) {
parentDOM.insertBefore(oldDOMElement, lastPlaceNode.nextSibling);
}
} else {
parentDOM.insertBefore(oldDOMElement, parentDOM.firstChild);
}
lastPlaceNode = oldDOMElement;
oldVChildren.splice(oldChildIndex, 1);
} else {
const newDOMELement = createDOMElement(newChild);
if (isDefined(lastPlaceNode)) {
parentDOM.insertBefore(newDOMELement, lastPlaceNode.nextSibling);
} else {
parentDOM.insertBefore(newDOMELement, parentDOM.firstChild);
}
lastPlaceNode = newDOMELement;
}
}
oldVChildren.forEach((oldChild) => getDOMElementByVdom(oldChild)?.remove());
}
function updateHook(oldVdom, newVdom) {
const hooks = (newVdom.hooks = oldVdom.hooks);
hooks.hookIndex = 0;
currentVdom = newVdom;
}
function updateFunctionComponent(oldVdom, newVdom) {
updateHook(oldVdom, newVdom);
let { type, props } = newVdom;
let newRenderVdom = type(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
newRenderVdom
);
newVdom.oldRenderVdom = newRenderVdom;
}
function updateReactMemoComponent(oldVdom, newVdom) {
updateHook(oldVdom, newVdom);
let { type, props } = newVdom;
const { render, compare } = type;
if (compare(props, oldVdom.props)) {
newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
return;
}
let renderVdom = render(props);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateReactForwardComponent(oldVdom, newVdom) {
// updateHook(oldVdom, newVdom);
let { type, props, ref } = newVdom;
debugger;
let renderVdom = type.render(props, ref);
compareVdom(
getDOMElementByVdom(oldVdom).parentNode,
oldVdom.oldRenderVdom,
renderVdom
);
newVdom.oldRenderVdom = renderVdom;
}
function updateVdom(oldVdom, newVdom) {
if (oldVdom.type.$$typeof === REACT_MEMO) {
return updateReactMemoComponent(oldVdom, newVdom);
} else if (oldVdom.type.$$typeof === FORWARD_REF) {
return updateReactForwardComponent(oldVdom, newVdom);
} else if (oldVdom.type === REACT_TEXT) {
return updateReactTextComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "string") {
return updateNativeComponent(oldVdom, newVdom);
} else if (typeof oldVdom.type === "function") {
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom);
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
function unMountVdom(vdom) {
if (!vdom) return;
let { props, ref } = vdom;
let domElement = getDOMElementByVdom(vdom);
vdom?.classInstance?.componentWillUnmount();
if (ref) {
ref.current = null;
}
wrapToArray(props.children).forEach(unMountVdom);
domElement?.remove();
}
export function compareVdom(parentDOM, oldVdom, newVdom, nextDOMElement) {
if (!oldVdom && !newVdom) {
return;
} else if (!!oldVdom && !newVdom) {
unMountVdom(oldVdom);
} else if (!oldVdom && !!newVdom) {
let newDOMElement = createDOMElement(newVdom);
if (nextDOMElement) parentDOM.insertBefore(newDOMElement, nextDOMElement);
else parentDOM.appendChild(newDOMElement);
newDOMElement?.componentDidMount?.();
} else if (!!oldVdom && !!newVdom && oldVdom.type !== newVdom.type) {
let newDOMElement = createDOMElement(newVdom);
unMountVdom(oldVdom);
newDOMElement?.componentDidMount?.();
} else {
updateVdom(oldVdom, newVdom);
}
}
const ReactDOM = {
createRoot,
};
export default ReactDOM;
src\react.js
import { wrapToVdom, shallowEqual } from "./utils";
import * as client from "./react-dom/client";
import { FORWARD_REF, REACT_MEMO } from "./constant";
let isBatchingUpdates = false;
export function setIsBatchingUpdates(value) {
isBatchingUpdates = value;
}
+let isScheduledUpdate = false;
+export function scheduleUpdate() {
+ if (isScheduledUpdate) return;
+ isScheduledUpdate = true;
+ queueMicrotask(() => {
+ client.currentRoot?.update();
+ isBatchingUpdates = false;
+ isScheduledUpdate = false;
+ });
+}
function createElement(type, config, children) {
delete config.__self;
delete config.__source;
let { ref, key, ...props } = config;
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
props.children = wrapToVdom(children);
}
return {
type,
props,
ref,
key,
};
}
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.pendingStates = [];
}
shouldComponentUpdate() {
return true;
}
setState(partialState) {
if (isBatchingUpdates) {
this.pendingStates.push(partialState);
+ scheduleUpdate();
} else {
const newState =
typeof partialState === "function"
? partialState(this.state)
: partialState;
this.state = {
...this.state,
...newState,
};
+ scheduleUpdate();
}
}
accumulateState() {
let state = this.pendingStates.reduce((state, update) => {
const newState = typeof update === "function" ? update(state) : update;
return { ...state, ...newState };
}, this.state);
this.pendingStates.length = 0;
return state;
}
updateIfNeeded() {
let nextState = this.accumulateState();
if (this.constructor.getDerivedStateFromProps) {
const derivedState = this.constructor.getDerivedStateFromProps(
this.nextProps,
nextState
);
if (derivedState !== null) {
nextState = { ...nextState, ...derivedState };
}
}
const shouldUpdate = this.shouldComponentUpdate?.(
this.nextProps,
nextState
);
this.state = nextState;
if (this.nextProps) this.props = this.nextProps;
if (shouldUpdate === false) return;
this.forceUpdate();
}
emitUpdate(nextProps) {
this.nextProps = nextProps;
if (this.nextProps || this.pendingStates.length > 0) {
this.updateIfNeeded();
}
}
forceUpdate() {
this.componentWillUpdate?.();
const renderVdom = this.render();
+ const oldDOMElement = client.getDOMElementByVdom(this.oldRenderVdom);
const parentDOM = oldDOMElement.parentNode;
const snapshot = this.getSnapshotBeforeUpdate?.(this.props, this.state);
+ client.compareVdom(parentDOM, this.oldRenderVdom, renderVdom);
this.oldRenderVdom = renderVdom;
this.componentDidUpdate?.(this.props, this.state, snapshot);
}
}
function createRef() {
return {
current: null,
};
}
function forwardRef(render) {
return {
$$typeof: FORWARD_REF,
render,
};
}
function createContext(defaultValue) {
const context = {
_currentValue: defaultValue,
Provider: function Provider(props) {
context._currentValue = props.value;
return props.children;
},
Consumer: function Consumer(props) {
return props.children(context._currentValue);
},
};
return context;
}
class PureComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState)
);
}
}
function memo(render, compare = shallowEqual) {
return {
$$typeof: REACT_MEMO,
render,
compare,
};
}
function useContext(context) {
return context._currentValue;
}
const React = {
createElement,
Component,
createRef,
forwardRef,
createContext,
PureComponent,
memo,
useContext,
...client,
};
export default React;
src\react-dom\event.js
+import { setIsBatchingUpdates, scheduleUpdate } from "../react";
const eventTypeMethods = {
click: {
capture: "onClickCapture",
bubble: "onClick",
},
};
function createSyntheticEvent(nativeEvent) {
let isPropagationStopped = false;
const handlers = {
get(target, key) {
if (target.hasOwnProperty(key)) return Reflect.get(target, key);
if (typeof nativeEvent[key] === "function") {
return nativeEvent[key].bind(nativeEvent);
} else {
return nativeEvent[key];
}
},
};
const syntheticEvent = new Proxy(
{
nativeEvent,
preventDefault() {
if (nativeEvent.preventDefault) {
nativeEvent.preventDefault();
} else {
nativeEvent.returnValue = false;
}
},
stopPropagation() {
if (nativeEvent.stopPropagation) {
nativeEvent.stopPropagation();
} else {
nativeEvent.cancelBubble = true;
}
isPropagationStopped = true;
},
isPropagationStopped() {
return isPropagationStopped;
},
},
handlers
);
return syntheticEvent;
}
export default function setupEventDelegation(container) {
if (container._reactEventDelegated) return;
["capture", "bubble"].forEach((phase) => {
Reflect.ownKeys(eventTypeMethods).forEach((type) => {
container.addEventListener(
type,
(nativeEvent) => {
const syntheticEvent = createSyntheticEvent(nativeEvent);
const path = syntheticEvent.composedPath();
const methodName = eventTypeMethods[type][phase];
const elements = phase === "capture" ? path.reverse() : path;
setIsBatchingUpdates(true);
for (let element of elements) {
if (syntheticEvent.isPropagationStopped()) {
break;
}
element.reactEvents?.[methodName]?.(syntheticEvent);
}
+ scheduleUpdate();
},
phase === "capture"
);
});
});
container._reactEventDelegated = true;
}