1.React 是什么? #

React是一个由 Facebook 开发和维护的开源 JavaScript 库,用于构建用户界面,特别是单页面应用程序(SPA)。以下是 React 的主要特点和概念:

  1. 虚拟 DOM:React 使用一个叫做虚拟 DOM 的内存中数据结构来跟踪实际 DOM 的变化。当组件的状态改变时,React 会创建一个新的虚拟 DOM 树并与之前的树进行比较,然后决定如何以最有效的方式更新实际的 DOM。
  2. 声明式编程:在 React 中,你只需描述 UI 应该如何根据不同的状态显示,而不是描述如何进行变化。这使得代码更容易理解和预测。
  3. JSX:React 引入了 JSX(JavaScript XML)语法,允许开发者在 JavaScript 代码中写入类似 HTML 的标记。这种结合使得 UI 逻辑更加直观。
  4. 组件化:React 以组件为中心,让开发者能够构建复杂的 UI 从小到大,通过组合不同的可重用组件。
  5. 单向数据流:React 组件通过 props 接收数据并显示它们,当数据发生变化时,组件会重新渲染。这种单向数据流使得状态管理更为清晰和可预测。
  6. 可与各种工具和库集成:尽管 React 专注于 UI,但它可以与各种状态管理工具(如 Redux 或 MobX)、路由库(如 React Router)以及其他前端工具和库一起使用。
  7. 跨平台:除了 Web 开发,Facebook 还推出了 React Native,它允许开发者使用相同的 React 概念来构建原生移动应用程序。

2.虚拟 DOM 的工作原理是什么? #

虚拟 DOM(Virtual DOM)是 React 中的核心概念之一。为了理解它,首先需要知道直接操作真实的 DOM 是代价高昂的,因为这可能引起浏览器的重排和重绘,导致性能瓶颈。虚拟 DOM 旨在最小化与真实 DOM 的交互次数,从而提高性能。 以下是关于 React 中的虚拟 DOM 的详细解释:

  1. 定义:虚拟 DOM 是对真实 DOM 的轻量级表示。它是一个 JavaScript 对象,映射了真实 DOM 的结构和属性。
  2. 工作机制
    • 当 UI 的某个部分发生变化时(例如,因为用户输入或新数据),React 会创建一个新的虚拟 DOM 树。
    • 接着,React 会将新的虚拟 DOM 树与上一个版本的虚拟 DOM 树进行比较,这一步被称为“差异化”(diffing)。
    • 通过这一比较过程,React 能够确定真实 DOM 需要进行哪些最小的修改以反映新的 UI 状态。
    • 最后,React 将这些变化批量应用到真实的 DOM,这一步被称为“重新渲染”(reconciliation)。
  3. 优势
    • 性能提高:通过批量更新真实 DOM,并只修改必要的部分,React 可以避免昂贵的 DOM 操作和不必要的重排/重绘。
    • 编程简化:开发者不必关心如何高效地更新 DOM。他们只需声明 UI 在各种状态下的外观,而 React 负责如何实现这一过程。
    • 跨平台:由于虚拟 DOM 只是 JavaScript 对象,因此 React 可以在不同环境中重复使用相同的逻辑,例如在 React Native 中。
  4. 局限性
    • 虽然虚拟 DOM 提高了性能,但它并不是解决所有问题的银弹。在某些情况下,对真实 DOM 的直接操作可能更快。
    • 差异化和重新渲染仍然需要计算资源。对于大型应用或频繁的更新,开发者可能需要使用一些性能优化技巧。 总之,虚拟 DOM 是 React 如何快速和高效地更新 UI 的核心机制。通过将复杂的 DOM 更新逻辑抽象为简单的 JavaScript 运算,React 提供了一个强大且高性能的方法来构建动态 web 应用。

2.1 创建虚拟 DOM #

2.1.1 创建项目 #

npm install create-a-react-app -g
cra study-react

2.1.2 src\index.js #

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)
);

2.1.3 react.js #

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;

2.1.3 package.json #

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"
  }
}

2.2 渲染虚拟 DOM #

2.2.1 src\index.js #

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);

2.2.2 client.js #

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;

3.为什么 React 要使用 JSX? #

React 使用 JSX 为了提供一种更加直观、声明式的方式来描述 UI,并且在编写 UI 代码时能够保持 JavaScript 的所有功能。 核心概念:

  1. 声明式语法: JSX 提供了一种看起来很像 HTML 的语法,让我们能够直观地描述 UI 的外观和结构。这使得代码更加可读和易于维护。
  2. 组件化: 通过 JSX, 我们可以直接在 JavaScript 中定义组件,这使得 UI 的复用、测试和关注点分离变得简单。
  3. 整合能力: 由于 JSX 本质上是 JavaScript,我们可以在其中插入任何有效的 JavaScript 表达式。这为 UI 的创建提供了巨大的灵活性。 方案对比:
  4. 纯 JavaScript: 在没有 JSX 的情况下,UI 可以使用纯 JavaScript 来构建。但这样会使代码变得冗长和难以阅读,尤其是对于复杂的 UI 结构。与此相反,JSX 提供了一种简洁、易读的方式来定义 UI。
  5. 模板语言: 传统的前端框架经常使用特定的模板语言来描述 UI。虽然这些模板可以是声明式的,但它们通常限制了你可以使用的逻辑。与之相反,JSX 允许你在描述 UI 时使用完整的 JavaScript 功能。
  6. 字符串拼接: 直接使用字符串拼接 HTML 是不安全的,因为它容易受到跨站脚本攻击(XSS)。而 JSX 在编译时会自动进行安全转义,从而防止这种攻击。 结论: React 采用 JSX 是为了结合 HTML 的直观性和 JavaScript 的强大功能,提供一种更加有效、安全和声明式的方式来构建用户界面。

3.1 src\index.js #

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);

4.类组件与函数组件有什么区别? #

什么是 React 组件? React 组件是独立、可重用的代码片段,用于描述 UI 的一部分。组件可以是简单的 UI 元素(如一个按钮)或包含其他组件的复杂容器。 共同点:

  1. 渲染 UI: 无论是类组件还是函数组件,它们的主要目标都是渲染 UI。
  2. 接收 props: 两者都可以接收 props,并依据 props 渲染 UI。
  3. 组件生命周期: 在引入 Hooks 之前,只有类组件能使用完整的生命周期方法。但现在,通过使用 useEffect Hook,函数组件也可以模拟大多数生命周期行为。 不同点:
  4. 编程思想:
    • 类组件: 面向对象编程 基于 ES6 的类语法,需要理解 JavaScript 的 this 绑定。
    • 函数组件: 函数式编程 基于函数,更简洁,并且更易于理解。
  5. 使用场景:
    • 类组件: 当需要使用 state 或生命周期方法时。
    • 函数组件: 对于简单的组件或当你想利用 Hooks。
  6. 特有功能:
    • 类组件: 具有完整的生命周期方法和错误边界处理。
    • 函数组件: 可以使用 Hooks。
  7. 逻辑复用:
    • 类组件: 需要使用高阶组件 (HOCs) 或 render props。
    • 函数组件: 可以使用自定义 Hooks。
  8. 性能优化:
    • 类组件: 使用 PureComponentshouldComponentUpdate 进行细粒度控制。
    • 函数组件: 可以利用 React.memouseMemo
  9. 未来发展趋势:
    • 类组件: 虽然 React 团队仍然支持,但鼓励新的开发使用函数组件和 Hooks。
    • 函数组件: 是 React 的未来方向,尤其是随着 Hooks 的引入。 结论: 类组件和函数组件都是 React 的核心部分,它们在渲染和传递 props 方面有很多相似之处。但随着 Hooks 的引入,函数组件变得更加强大和灵活,它们更简洁,易于读写,逻辑复用更直观,并且更符合 React 的未来发展方向。

4.1 函数组件 #

4.1.1 src\index.js #

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);

4.1.2 client.js #

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;

4.2 类组件 #

4.2.1 src\index.js #

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);

4.2.2 react.js #

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;

4.2.3 client.js #

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;

5.React 中的合成事件是什么? #

React 的合成事件 (SyntheticEvent) 是 React 团队为解决跨浏览器的事件一致性问题而设计的。它是一个浏览器的原生事件的跨浏览器包装器,具有与原生事件相同的接口,但提供了更多的功能和与所有浏览器一致的行为。

5.1 浏览器中的事件模型 #

浏览器中的事件模型主要围绕用户与网页互动时发生的各种事件(如点击、键盘输入、鼠标移动等)进行设计。这些事件可以在 DOM (Document Object Model) 元素上监听和处理。以下是浏览器事件模型的核心概念:

  1. 事件流: 事件流描述了页面中接收事件的顺序。事件流分为两个主要阶段:捕获阶段和冒泡阶段。
    • 捕获阶段:事件从 Document 开始,向下传递至目标元素的外层,但不包括目标元素本身。
    • 目标阶段:事件在目标元素上触发。
    • 冒泡阶段:事件从目标元素开始,向上传递回 Document
  2. 事件监听器: 使用 JavaScript,你可以在 DOM 元素上添加事件监听器来响应特定事件。例如,你可以监听元素的 click 事件:
    element.addEventListener("click", function () {
      console.log("Element was clicked!");
    });
    
    当指定事件发生时,回调函数会被调用。
  3. 事件对象: 当事件发生时,事件监听器的回调函数会接收一个事件对象作为参数。这个对象包含了关于事件的信息,例如触发事件的元素、事件类型、鼠标位置等。
    element.addEventListener("click", function (event) {
      console.log(event.target); // 返回触发事件的元素
    });
    
  4. 取消默认行为: 有些 DOM 事件有与之关联的默认行为。例如,点击 <a> 标签会导航到其 href 属性指定的 URL。如果你想阻止这个默认行为,可以使用事件对象的 preventDefault 方法:
    linkElement.addEventListener("click", function (event) {
      event.preventDefault();
      console.log("Link was clicked but default navigation was prevented");
    });
    
  5. 停止事件传播: 在事件流中,有时你可能想要阻止事件进一步传播(无论是捕获还是冒泡)。你可以使用事件对象的 stopPropagation 方法:
    element.addEventListener("click", function (event) {
      event.stopPropagation();
      console.log("Element was clicked and event propagation was stopped");
    });
    
  6. 事件委托: 由于事件具有冒泡特性,你可以在父元素上监听其子元素的事件,而不是直接在每个子元素上绑定事件。这种方法叫做事件委托,它是一种内存和性能优化手段。
  7. 移除事件监听器: 如果你不再需要监听某个事件,可以使用 removeEventListener 方法移除事件监听器。 总的来说,浏览器的事件模型提供了一种机制,允许开发者监听和响应在网页上发生的各种交互行为。

5.2 React 合成事件的一些关键 #

  1. 跨浏览器一致性:不同的浏览器可能会有不同的事件行为和属性。React 的合成事件系统提供了一个一致的 API,不考虑用户使用的浏览器。
  2. 性能:在大多数情况下,为每个元素都附加事件监听器是低效的。React 实际上使用事件委托,它在文档的根级别附加一个事件监听器,然后使用合成事件系统在需要时调用正确的处理程序。
  3. 自动池化:为了提高性能,React 会重用合成事件对象。当事件回调被调用后,所有的属性都会被清空,这样对象就可以被再次使用。这意味着你不能在事件回调之外的异步代码中访问事件。
    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 });
    }
    
  4. 与原生事件的关系:虽然合成事件模拟了浏览器的原生事件,但你仍然可以在需要时访问原生事件。每个 SyntheticEvent 对象都有一个 nativeEvent 属性,它指向原生事件。
  5. 支持所有事件:React 为所有的 DOM 事件提供了合成事件,包括键盘、表单、焦点、鼠标等等。 总的来说,React 的合成事件提供了一种高效、跨浏览器的方式来处理 DOM 事件。这使得 React 应用程序的事件处理既简单又一致。

5.3 合成事件工作流程 #

  1. 事件绑定: 当你在 React 组件中为某个元素(例如按钮)添加事件处理程序时,React 并不会真正地将事件监听器绑定到该 DOM 元素上。相反,它在文档的根级别只绑定一次事件监听器。
  2. 事件发生: 当用户与 UI 互动时(例如点击一个按钮),触发的事件冒泡到文档的根级别。
  3. 事件委托: 由于 React 在根级别有事件监听器,当事件冒泡到这一级时,React 会捕获到这个事件。
  4. 合成事件创建: 在事件到达 React 的根监听器时,React 会创建一个 SyntheticEvent 对象。这个对象模仿了浏览器的原生事件,但它是跨浏览器的,确保所有浏览器都有相同的事件属性和方法。
  5. 事件处理: 通过原始事件的目标元素和事件类型,React 确定了要调用哪个事件处理程序。然后,它使用之前创建的合成事件对象调用该处理程序。
  6. 事件池化: 出于性能考虑,当事件处理程序被调用并返回后,React 会"池化"合成事件对象,意味着它会重用这些对象来减少垃圾回收的负担。此时,所有的属性都会从事件对象上清除。这就是为什么在异步操作中,你不能访问合成事件的属性的原因。
  7. 非受控组件和原生事件: 有些情况下,你可能想绕过 React 的事件系统。在这种情况下,你可以直接使用原生事件监听器和不通过 React 的方式处理事件。但大多数情况下,React 的合成事件系统都能满足需求,并提供更一致、高效的事件处理方式。 通过这种方法,React 能够确保跨浏览器的一致性,并提供性能上的优化,因为它只在文档的根级别绑定一个事件监听器,而不是为每个元素都绑定监听器。

5.4 事件代理 #

5.4.1 src\index.js #

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 父节点在冒泡阶段执行
*/

5.4.2 client.js #

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;

5.4.3 event.js #

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;
}

5.4.4 constant.js #

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;
}

5.4.5 react.js #

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;

5.4.6 utils.js #

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;
}

5.5 合成事件 #

5.5.1 src\index.js #

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的冒泡
 */

5.5.2 event.js #

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;
}

6.setState 是同步的还是异步的? #

在React中,状态更新(通过setState调用)可以是同步的也可以是异步的,这取决于被调用的上下文。 当setState被直接调用,例如作为事件处理器的一部分,例如绑定到按钮点击事件的函数中,React会将这些更新批处理(或“批量更新”),因为isBatchingUpdates标志被设置为true。这意味着React会积累所有的setState调用,并在事件处理结束后统一处理,以避免不必要的重渲染和性能问题。

但是,如果setState在一个异步函数中被调用,如在setTimeoutsetInterval或者addEventListener的回调中,React就不会批处理这些更新,因为在这些情况下isBatchingUpdates标志通常是false。这意味着每次setState调用都会立即导致组件的重新渲染。

因此,当回答面试题时,您可以说明:setState通常在React事件处理器中是异步批处理的,以提高性能和减少不必要的渲染。但是,在诸如setTimeoutsetInterval或DOM事件监听器中,setState可能会表现为同步的,除非使用了特定的技巧或者使用了React 17及以上版本的新特性。这个细微的差别是非常重要的,因为它影响到组件的渲染和性能优化。

6.1 setState #

6.1 src\index.js #

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);

6.2 react.js #

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;

6.3 client.js #

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;

6.2 批量更新 #

6.2.1 src\index.js #

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"));

6.2.2 react.js #

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;

6.2.3 event.js #

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;
}

7.React 中的 ref 是什么? #

在 React 中,ref(参照)是一个重要的属性,它允许我们直接访问 DOM 元素或组件实例。虽然 React 鼓励使用 props 和 state 来控制渲染和组件交互,但有时我们仍然需要直接操作 DOM,或者访问 React 组件实例上的某些方法。这时,ref就显得特别有用。

7.1 直接访问 DOM 元素 #

7.1.1 src\index.js #

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);

7.1.1 src\index.js #

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);

7.1.2 react.js #

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;

7.1.3 client.js #

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;

7.2 为 class 组件添加 Ref #

7.2.1 src\index.js #

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);

7.2.2 client.js #

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;

7.3 React.forwardRef #

React.forwardRef 是 React 的一个功能,它允许你将 ref 从父组件传递到子组件。这在你需要在子组件内部访问 DOM 节点或者某个 React 元素时非常有用。React.forwardRef 是通过接收一个渲染函数来实现的,该函数接受 propsref 为参数,然后返回一个 React 元素。

7.3.1 src\index.js #

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);

7.3.2 react.js #

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;

7.3.3 constant.js #

src\constant.js

export const REACT_TEXT = Symbol.for("react.text");
+export const FORWARD_REF = Symbol.for("react.forward_ref");

7.3.4 client.js #

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;

8.React 中的生命周期? #

React 的组件生命周期可以被大致分为三个主要阶段:Mounting(挂载),Updating(更新)和 Unmounting(卸载)。

  1. Mounting(挂载): 这些方法会在组件创建并插入到 DOM 中时被调用。
    • constructor: 组件的构造函数,最先被调用,用于初始化本地状态和绑定事件处理函数。
    • static getDerivedStateFromProps: 当 props 发生变化时,在 render 方法之前被调用。它应返回一个对象来更新状态或返回 null 表示不更新任何内容。
    • render: 唯一必需的方法。读取this.propsthis.state并返回以下类型之一:React 元素,字符串和数字,fragments,Portals,布尔值或 null。
    • componentDidMount: 在组件输出被渲染到 DOM 之后立即调用。这是一个很好的地方去发起网络请求或设置订阅。
  2. Updating(更新): 当组件的 props 或 state 改变时,会进入更新生命周期。
    • static getDerivedStateFromProps: 同上。
    • shouldComponentUpdate: 根据组件的 props 和 state 的变化,返回一个布尔值来决定 React 是否应继续渲染。默认返回true。这是一个优化性能的点,可以避免不必要的渲染。
    • render: 同上。
    • getSnapshotBeforeUpdate: 在最近的渲染输出提交到 DOM 之前被调用。它使组件能在可能的 DOM 改变之前从 DOM 捕获一些信息。
    • componentDidUpdate: 在更新发生后立即被调用。可以在此处进行 DOM 查询并触发网络请求。
  3. Unmounting(卸载): 当组件从 DOM 中移除时会调用。

    • componentWillUnmount: 在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,如无效的定时器,或取消网络请求,或清理任何在componentDidMount中创建的订阅。 此外,React 还引入了 Error Boundaries,用于捕获子组件树的 JS 错误,渲染备用 UI,而不是使整个组件树崩溃:
  4. static getDerivedStateFromError: 此生命周期被调用后,你可以渲染一个备用 UI。

  5. componentDidCatch: 此生命周期在报告错误日志时非常有用。 注意:
  6. React 16.3 开始引入了新的生命周期方法,并且弃用了componentWillMount, componentWillReceivePropscomponentWillUpdate。推荐使用新的生命周期方法。

8.1 src\index.js #

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
 */

8.2 react.js #

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;

8.3 client.js #

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;

9.React 中的子组件生命周期 #

9.1 src\index.js #

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);

9.2 react.js #

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;

9.3 client.js #

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;

10.DOM-DIFF #

10.1 key #

在 React 中,key 是一个特殊的属性(attribute)你应该在创建列表元素时包括它们。key 帮助 React 识别哪些项目已经改变,被添加,或者被移除。Keys 应当给予数组内的每一个元素一个独一无二的标识符。

这里是一些关于 key 的要点:

  1. 性能优化

    • 当组件的状态发生变化时,React 将执行其重新渲染的过程。如果你的组件结构包含了子组件集合,React 需要决定哪些子组件需要重新渲染。
    • 使用 key,React 可以追踪每个子元素的身份,在数据改变时有效地重新渲染和更新列表。
  2. 稳定的身份

    • Keys 应当是稳定的、可预测的、且在同一列表中唯一的,以便 React 可以追踪组件。
    • 通常,我们使用数据中的 id 作为 key。如果你没有稳定的 id,你可能会使用项目索引作为最后的手段,但这不推荐因为它可能导致性能问题和组件状态的问题。
  3. 组件重排序

    • 如果列表项目的顺序可能会改变,不稳定的 keys(如数组索引)会导致性能下降和可能的状态错误。
    • 如果一个元素的 key 在不同的渲染之间改变,React 会重新创建组件而不是更新它。
  4. 与组件状态的关系

    • 如果你错误地使用了 key,尤其是在使用局部状态或与其它生命周期相关的组件时,你可能会遇到一些意想不到的行为。例如,如果 key 不是稳定的,组件的状态可能会在重新渲染时丢失。

为了给你更好的理解,这里有一个简单的例子。假设我们有一个任务列表,我们想要渲染出来:

const todoItems = todos.map((todo) => <li key={todo.id}>{todo.text}</li>);

在这个例子中,每个 todoid 唯一地标识了一个列表元素。当 todos 数组更新时,React 将使用 id 来匹配老的 todos 与新的 todos,从而决定哪些元素需要更新、添加或删除。

记住,只有当创建动态子元素列表时,key 才是必须的。如果你渲染一个静态列表,或者一个对象的子集,并且不关心是否会重新排序或更新,你可能不需要使用 key

10.2 src\index.js #

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);

10.3 react.js #

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;

10.4 client.js #

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;

11.React 中新的生命周期 #

11.1 getDerivedStateFromProps #

getDerivedStateFromProps 是 React 的生命周期方法,专为 class 组件设计。它的主要目的是让组件在接收新的 props(属性)时有机会产生新的 state(状态)。这是一个静态方法,这意味着它不能访问组件实例(即不能使用this关键字)。

以下是 getDerivedStateFromProps 的一些关键特点:

  1. 调用时机:该方法在组件实例化后和每次组件接收新的 props 时都会被调用。

  2. 参数:它接受两个参数:nextPropsprevState

    • nextProps:组件将要接收的新属性。
    • prevState:组件当前的状态。
  3. 返回值:它应该返回一个对象来更新状态,或者返回null来表示不需要更新任何状态。

  4. 无副作用:此方法只应用于返回一个 state 更新的对象或 null,而不应该有任何副作用(如发送 HTTP 请求或调用 setTimeout 等)。

  5. 静态方法:由于它是一个静态方法,所以不能访问 this

  6. 使用场景:当组件的状态依赖于 props 的更改时,可以考虑使用此方法。但是,大多数情况下,最好使用其他方法,如 componentDidUpdate,或者完全避免将 props 映射到 state。

11.1.1 src\index.js #

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);

11.1.2 react.js #

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;

11.2 getSnapshotBeforeUpdate #

getSnapshotBeforeUpdate是一个生命周期方法,它允许您在 DOM 的实际更改之前捕获一些信息(例如滚动位置)。然后,该信息可以传递给componentDidUpdate方法,这样您就可以在更新之后使用这些信息。

一般用途:当您的组件渲染的内容可能会改变滚动位置(例如,一个正在增长的列表),并且您想在 React 更新 DOM 后保持当前的滚动位置时,这个生命周期方法非常有用。

11.2.1 index.js #

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);

11.2.2 react.js #

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;

12.说一下 React 中的 Context? #

React 的Context是一种传递数据的方法,允许数据能够被传递到组件树中的任何层级,而不必通过每一个层级的组件明确地传递。它被设计为解决当有许多层嵌套时,只是为了把数据传递给较低层级的组件而手动传递props的问题。

使用Context的步骤:

  1. 使用React.createContext创建一个新的上下文。
  2. 使用Context.Provider组件为子组件提供上下文值。
  3. 使用Context.Consumer组件或useContext hook 在任何子组件中访问上下文值。

12.1 src\index.js #

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 />);

12.2 react.js #

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;

13.说一下 React 如何避免不必要的渲染? #

13.1 React.PureComponent #

PureComponent 是 React 提供的一个组件基类,它的核心特性是只有当它的 props 或 state 发生浅层变化时,它才会重新渲染。浅层比较会检查对象顶层的属性,而不是深度检查。如果对象的顶层属性没有变化,那么 PureComponent 就不会重新渲染,这可以提高性能。

使用 PureComponent 最适合于那些组件的 props 和 state 结构较简单,或者可以确保结构不会经常发生深度变化的场景。

13.1.1 src\index.js #

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 />);

13.1.2 react.js #

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;

13.1.3 utils.js #

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;
+}

13.2 React.memo #

React.memo是一个高阶组件,它可以用于优化那些仅仅依赖于其 props 变化的组件的重新渲染行为。换句话说,如果组件的 props 在连续的渲染之间没有发生变化,那么使用React.memo可以避免组件的不必要的重新渲染。

如果你有一个功能组件并且你想避免该组件因父组件的重新渲染而不必要地重新渲染,你可以通过React.memo包裹该组件:

工作原理

  1. 当一个组件被React.memo包裹时,React 会记住该组件上一次渲染的结果。
  2. 当这个组件的父组件再次渲染时,React 会使用浅比较(shallow comparison)来比较当前的 props 和上一次的 props。
  3. 如果 props 没有发生变化,React 会重用上一次的渲染结果,而不是重新渲染组件。
  4. 如果 props 发生了变化,组件将会被重新渲染。

13.2.1 src\index.js #

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 />);

13.2.2 react.js #

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;

13.2.3 client.js #

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;

13.2.4 constant.js #

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");

14.useReducer #

useReducer 是 React Hooks 中的一个非常有用的 Hook,它是一个更复杂的版本的 useState。当你的 state 逻辑变得更加复杂或需要之前的状态来计算下一个状态时,useReducer 是非常有用的。

基本使用

useReducer 接受一个 reducer 函数和一个初始状态作为参数,返回当前的 state 和一个与该 reducer 函数关联的 dispatch 方法。

基本的 useReducer 使用方法如下:

const [state, dispatch] = useReducer(reducer, initialState);

为什么使用 useReducer

  1. 更加可预测: 由于它是基于 reducer 的,因此 useReducer 允许你的 state 逻辑以更可预测的方式运行,这在处理更复杂的 state 逻辑时特别有用。

  2. 更好的组织: 当有多种状态更新逻辑时,使用 useReducer 可以帮助你更好地组织代码。

  3. 中间件和增强器: 像 Redux 这样的库允许你使用中间件来增强 reducer 的功能。虽然 React 的 useReducer 没有这个功能,但你可以模仿类似的行为。

  4. 更好地处理副作用: 当与 useEffect 结合使用时,useReducer 可以更好地处理和 orchestrate 副作用。

14.1 src\index.js #

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 />);

15.2 client.js #

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;

15.3 react.js #

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;

15.useState #

15.1 src\index.js #

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 />);

15.2 client.js #

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;

16. useCallback+useMemo #

首先,我要解释 useMemouseCallback 的原理。

  1. useMemo:返回一个 memoized 值。只有当依赖项改变时,它才会重新计算这个值。
  2. useCallback:返回一个 memoized 的 callback。它会返回一个不变的函数,直到依赖项改变。

16.1 src\index.js #

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 />);

16.2 client.js #

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;

17.React.useContext #

React.useContext 是 React 的一个 Hook,它允许你无需明确地传递 props,就能让组件订阅 context 的变化。

为了理解 useContext,首先你需要知道 React 的 Context API。Context 提供了一种在组件之间共享此类值的方式,而不必明确地通过组件树的每个层级传递 props。

Context 主要由两个核心组件组成:ProviderConsumer

然而,在函数组件中,你不必使用 Consumer。你可以简单地使用 useContext Hook

17.1 src\index.js #

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 />);

17.2 react.js #

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;

18.useEffect #

React.useEffect 是 React 的一个 Hook,允许你在函数组件中执行有副作用的操作。它与类组件中的生命周期方法类似,如 componentDidMountcomponentDidUpdatecomponentWillUnmount,但更加强大和灵活。

使用方法:

useEffect(() => {
  // 副作用操作
  return () => {
    // 清除副作用,类似于 componentWillUnmount
  };
}, [依赖项]);

参数解释

  1. 函数体:第一个参数是你的副作用函数。这里面的代码在默认情况下在每次渲染后都会执行。
  2. 依赖数组:这是一个可选参数。当提供这个数组时,useEffect 只会在这些依赖发生变化时执行。

注意事项

  1. 不要在循环、条件或嵌套函数中调用 useEffect:它应该总是在你的 React 函数的顶层被调用。
  2. 清除副作用useEffect 可以返回一个函数,这个函数会在组件卸载前或重新执行副作用前被调用。这是清除副作用(如事件监听器、取消网络请求、清除定时器)的好地方。

18.1 src\index.js #

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 />);

18.2 client.js #

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;

19.useLayoutEffect+useRef #

useLayoutEffect 是 React 的一个 Hook,它和 useEffect 有相同的函数签名,但它有一些关键的差异。让我们深入了解这两者之间的差异以及 useLayoutEffect 的工作原理:

  1. 执行时机

    • useEffect: 是在浏览器绘制后异步执行的,这意味着执行这个 effect 可能会导致延迟。
    • useLayoutEffect: 是在浏览器执行绘制之前同步执行的。由于这个原因,如果你在 useLayoutEffect 中执行一些会导致额外绘制的代码(例如修改 DOM),它不会导致额外的浏览器绘制。
  2. 用途

    • 当你需要在 React 更新 DOM 之后,但在浏览器绘制之前,读取或修改 DOM,你应该使用 useLayoutEffect。这通常用于读取例如布局或尺寸等与视觉相关的属性。
    • 如果你不关心绘制,而只是需要在某些事情发生后执行一些代码,使用 useEffect 更为合适。

useRef 是 React 提供的一个 Hook,它返回一个可变的 ref 对象,这个对象的 .current 属性可以被修改,并且它不会导致组件重新渲染。

下面是 useRef 的一些核心点:

  1. 创建 Ref: 当你调用 useRef(),它会返回一个像这样的对象: { current: initialValue }。你可以为它提供一个初始值,例如 useRef(0),但这并不是必需的。

  2. 持久性: 不像 useState,每次组件重新渲染时都会返回一个新的 state,useRef 会在整个组件生命周期中保持其对象不变。

  3. 使用场景:

    • 访问 DOM 元素: 可以使用 useRef 创建一个 ref,然后将它附加到 JSX 元素上,从而在组件内部访问该 DOM 元素。
    • 保留不触发渲染的可变数据: 如果你有一个值,你不希望它的变化导致组件重新渲染,可以将它存储在 ref 中。
    • 跟踪上一次的 props 或 state: 你可以使用 useRef 来跟踪前一个渲染周期中的值。

19.1 src\index.js #

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 />);

19.2 client.js #

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;

20.useImperativeHandle #

useImperativeHandle 是 React 的一个高级 Hook,通常与 forwardRef 配合使用。它允许你在使用 ref 时,自定义暴露给父组件的实例值,而不是组件的默认实例。

这个 Hook 主要在你想给组件的外部用户更多的控制权,或当你想隐藏一些组件内部的细节时使用。

使用方法

useImperativeHandle(ref, createHandle, [deps]);

20.1 src\index.js #

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 />
);

20.2 client.js #

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;

20.updateClassComponent #

20.1 src\index.js #

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);

20.2 client.js #

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;

20.3 react.js #

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;

20.4 event.js #

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;
}