Skip to content

React 基础

一、react 元素 - 虚拟 DOM

TIP

JSX 其实只是一种语法糖,最终会通过babeljs转译成 createElement 语法,最新版本转化为 Object(r.jsx)("h1",

1 特点

  • React 元素是构成 React 应用的最小单位
  • React 元素用来描述你在屏幕上看到的内容
  • React 元素事实上是普通的 JS 对象
  • ReactDOM 来确保浏览器中的 DOM 数据和 React 元素保持一致

2 jsx 转化 react 元素

在 React 中可以使用React.createElement 方法创建虚拟 DOM

  • 在 React 中可以使用 React.createElement 方法创建虚拟 DOM
  • 也可以认为 React.createElement 是创建虚拟 DOM 工厂
  • 它的返回值才是 React 元素,也是虚拟 DOM

简单版本

js
// jsx
let element = <h1>hello</h1>;

// react元素
let element = React.createElement("h1", null, "Hello");

console.log(JSON.stringify(element, null, 4));

/**
{
	"type": "h1",
	"props": {
		"children": "Hello"
	}
}
 */

复杂版本

js
// jsx
let element = (
  <h1 className="title" style={{ color: "red" }}>
    <span>hello</span>world
  </h1>
);

// react元素
/**
 * 参数1 标签的类型 h1 span div
 * 参数2 属性的JS对象
 * 参数3往后的都是儿子们
 */
const element = React.createElement(
  "div", //DOM的类型
  {
    //DOM的属性
    style: { color: "red" }, //行内样式
    className: "container", //类名
  },
  "hello",
  React.createElement(
    "span", //DOM的类型
    { style: { color: "blue" } }, //行内属性
    "world" //儿子
  )
);

console.log(JSON.stringify(element, null, 4));

输出结果 element 剔除私有属性之外,其实是一个对象,如下所示:

js
{
    "type": "div",
    "key": null,
    "ref": null,
    "props": {
        "style": {
            "color": "red"
        },
        "className": "container",
        "children": [
            "hello",
            {
                "type": "span",
                "key": null,
                "ref": null,
                "props": {
                    "style": {
                        "color": "blue"
                    },
                    "children": "world"
                },
                "_owner": null,
                "_store": {}
            }
        ]
    },
    "_owner": null,
    "_store": {}
}

3 实现 createElement

js
/**
 * @param {*} type 元素的类型
 * @param {*} config 配置的属性对象
 * @param {*} children 第一个儿子
 */
function createElement(type, config, children) {
  // 删除多余对象
  if (config) {
    delete config._owner;
    delete config._store;
  }
  const { ref, key, ...props } = config;

  // 如果参数数量大于3个,说明不止一个儿子
  if (arguments.length > 3) {
    // 以arguments作为this指针,调用数组的上的slice 方法,把从第3个参数开始的实参都放到数组里
    children = Array.prototype.slice.call(arguments, 2);
  } else {
    // 如果没有儿子,或者只有一个儿子,那么直接把children赋值给props.children就可以了
    // children 也可能是一个字符串或者数字,也可能是一个null,也可能是一个react元素
    props.children = children;
  }

  //  返回React元素,也就是虚拟DOM
  return {
    type, // 元素的类型
    props, // 元素的属性
    ref,
    key,
  };
}

const React = {
  createElement,
};

export default React;

4. 渲染函数

  • 本质就是根据传入的元素类型,然后用 JS 原生 DOM 操作,进行创建对应的元素、处理事件,最后挂载到真实根节点上
js
/**
 * 创建DOM容器
 * @param {*} container 容器
 * @returns
 */
function createRoot(container) {
  return {
    // 把虚拟DOM变成真实DOM并日插入容器container
    render(reactElement) {
      // 把虚拟DOM变成真实DOM
      const domElement = renderElement(reactElement);
      // 把此真实DOM添加到容器中
      container.appendChild(domElement);
    },
  };
}
/**
 * 把虚拟DOM变成真实DOM
 * @param {*} element element 虚拟DOM
 * @returns 真实DOM
 */
function renderElement(element) {
  // 如果此元素是一个数字或者是一个字符串,则创建一个文本节点并返回
  if (typeof element === "string" || typeof element === "number") {
    return document.createTextNode(element);
  }
  // 取出元素类型和属性对象
  const { type, props } = element;
  // 根据type创建真实的DOM节点
  const domElement = document.createElement(type);
  // 再处理属性,先出属性的数组
  Object.keys(props).forEach((name) => {
    // 如果属性是children则先跳过不处理, 后面会单独处理
    if (name === "children") {
      return;
    }
    // 如果是行内样式属性的话,则直接覆盖到真实DOM的style 上
    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") {
    // 格式化children 数组
    const children = Array.isArray(props.children)
      ? props.children
      : [props.children];
    // 递归:把每个儿子都从虚拟DOM变成真实DOM,并插入到父节点里面
    children.forEach((child) => domElement.appendChild(renderElement(child)));
  }
  return domElement;
}
const ReactDOMClient = {
  createRoot,
};
export default ReactDOMClient;

二、组件:函数组件和类组件

1 函数组件

  • 函数组件的 props 参数可以为任意值
  • 函数组件接收一个单一的 props 对象并返回了一个 React 元素

用法

js
import React from "react"; // react核心库
import ReactDOM from "react-dom/client"; // react的dom渲染库

/**
 * 定义一个函数组件-函数: 名字首字母必须要大写
 * 函数内部的jsx语法被转化为react.createElement元素
 * @param { } props 代表传递给此组件的属性对象
 */
function FunctionComponent(props) {
  return (
    <div>
      hello {props.name} {props.age}
    </div>
  );
}

/**
 * 定义一个函数组件-返回一个虚拟DOM
function FunctionComponent(props) {
  return React.createElement("h1", null, "Hello, ", props.name);
} 
*/

// JSX写法
// 调用组件,组件名称和属性会被当成参数传入到react.createElement中
const element = <FunctionComponent name="zhangsan" age={18} />;

// 普通写法
// 其实最终JSX在webpack打包编辑的时候会使用babel编译成普通JS
// babel负责把name="zhangsan" age={18}转成props对象并传递给props
const element2 = React.createElement(FunctionComponent, {
  name: "zhangsan",
  age: 18,
});

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element1);

实现代码

js
function renderElement(element) {
  // ...
  const { type, props } = element;
  // 如果元素/虚拟DOM的类型是一个函数的话
  if (typeof type === "function") {
    // 把属性对象传递给函数组件这个函数,返回一个React元素
    const functionElement = type(props);
    // 把两数组件返回的React素传递给renderElement,创建真实DOM
    return renderElement(functionElement);
  }
  // ...
}

2 类组件

js
import React from "react"; // react核心库
import ReactDOM from "react-dom/client"; // react的dom渲染库

/**
 * 定义一个类组件继承自父类React.Component
 * 定义的类组件必须编写一个名为render的函数,负责返回要渲染的虚拟DOM
 */
class ClassComponent extends React.Component {
  constructor(props) {
    super(props); // this.props = props;
    // super指的是父类的构造函数
    // 在内部会把收到的属性对象放在自己的实例上,以后可以通过this.props
  }
  render() {
    return (
      <div>
        hello {this.props.name} {this.props.age}
      </div>
    );
  }
}

// 调用组件,babel会把属性收集起来变成一个props属性对象,并传递给类组件的构造函数
const element = <ClassComponent name="zhangsan" age={18} />;

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element1);

实现代码

js
function renderElement(element) {
  // ...
  const { type, props } = element;
  // 如果元素/虚拟DOM的类型是一个函数的话
  if (typeof type === "function") {
    // 1. 这是一个类组件
    if (type.isReactComponent) {
      // 把属性对象传递给类组件的构造函数,返回类组件的实例
      const classInstance = new type(props);
      // 调用实例上的render方法,返回将要渲染的虚拟DOM
      const classElement = classInstance.render();
      // 把虚拟DOM传递给renderElement 返回真实DOM
      return renderElement(classElement);

      // 2. 这里处理函数组件
    } else {
      // 把属性对象传递给函数组件这个函数,返回一个React元素
      const functionElement = type(props);
      // 把两数组件返回的React素传递给renderElement,创建真实DOM
      return renderElement(functionElement);
    }
  }
  // ...
}

3 总结

  • React 元素可能是字符串(原生 DOM 类型),也可能一个函数(函数组件),也可能是一个类(类组件)
  • 在定义组件元素的时候,会把 JSX 所有的属性封装成一个 props 对象传递给组件
  • 组件的名称一定要首字母大写,React 是通过首字母来区分原生还是自定义组件
  • 组件要先定义,再使用
  • 组件要返回并且只能返回一个 React 根元素,否则会报错:JSX expressions must have one parent element

4 区别

React 组件是独立的、可重用的代码片段,用于描述 UI 的一部分,组件可双是简单的 UI,比经如说是一个按钮,也可以是一个复杂的容器,比如一个页面

相同点

  1. 渲染 UI:功能都是渲染 UI 的,目标都是渲染 UI
  2. 接收 props:都可以接收属性props并返回虚拟 DOM,并进行渲染
  3. 本质:从本质来说它们表现的结果是一样的
  4. 组件生命周期: 在引入 Hooks 之前,只有类组件能使用完整的生命周期方法。但现在,通过使用 useEffect Hook,函数组件也可以模拟大多数生命周期行为

不同点

  1. 编程思想

    • 类组件: 面向对象编程 基于 ES6 的类语法,需要理解 JavaScript 的 this 绑定。
    • 函数组件: 函数式编程 基于函数,更简洁,并且更易于理解,没有 this。
  2. 使用场景

    • 类组件: 当需要使用 state 或生命周期方法时。
    • 函数组件: 对于简单的组件或当你想利用 Hooks。
  3. 特有功能

    • 类组件: 具有完整的生命周期方法和错误边界处理。
    • 函数组件: 可以使用 Hooks。
  4. 逻辑复用

    • 类组件: 需要使用高阶组件 (HOCs) 或 render props。
    • 函数组件: 可以使用自定义 Hooks。
  5. 性能优化

    • 类组件: 使用 PureComponent 或 shouldComponentUpdate 进行细粒度控制。
    • 函数组件: 可以利用 React.memo 和 useMemo。
  6. 未来发展趋势

    • 类组件: 虽然 React 团队仍然支持,但鼓励新的开发使用函数组件和 Hooks。
    • 函数组件: 是 React 的未来方向,尤其是随着 Hooks 的引入。

结论: 类组件和函数组件都是 React 的核心部分,它们在渲染和传递 props 方面有很多相似之处。但随着 Hooks 的引入,函数组件变得更加强大和灵活,它们更简洁,易于读写,逻辑复用更直观,并且更符合 React 的未来发展方向。

三、事件处理

3.1 浏览器中的事件模型

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

浏览器事件模型的核心概念:

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

  2. 移除事件监听器: 如果你不再需要监听某个事件,可以使用 removeEventListener 方法移除事件监听器。

js
element.removeEventListener("click");
  1. addEventListener 的三个参数
  • 第一个参数 event:监听的事件名称
  • 第二个参数是函数:需要执行的事件
  • 第三个参数是 useCapture(变量):用来判断是捕获(true)还是冒泡(false),默认为 false 冒泡

总的来说,浏览器的事件模型提供了一种机制,允许开发者监听和响应在网页上发生的各种交互行为。

3.2 合成事件

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

React 使用的是自定义的合成事件,合成事件的优点

  1. 跨浏览器一致性:不同的浏览器可能会有不同的事件行为和属性。React 的合成事件系统提供了一个一致的 API,不考虑用户使用的浏览器。--- 进行浏览器兼容,实现更好的跨平台
  2. 性能考量:在大多数情况下,为每个元素都附加事件监听器是低效的。React 实际上使用事件委托,它在文档的根级别附加一个事件监听器,然后使用合成事件系统在需要时调用正确的处理程序。
  3. 自动池化:为了提高性能,React 会重用合成事件对象。当事件回调被调用后,所有的属性都会被清空,这样对象就可以被再次使用。这意味着你不能在事件回调之外的异步代码中访问事件。
js
function handleClick(event) {
  console.log(event); // => SyntheticBaseEvent
  console.log(event.type); // => "click"
  const eventType = event.type;
  setTimeout(function () {
    // 实测,异步这里还是可以拿到事件值,18优化了
    console.log(event.type); // => "click"
    console.log(eventType); // => "click"
  }, 0);
  // 不要这样做!
  this.setState({ clickEvent: event });
  // 你可以这样做:
  this.setState({ eventType: event.type });
}
  1. 与原生事件的关系:虽然合成事件模拟了浏览器的原生事件,但你仍然可以在需要时访问原生事件。每个 SyntheticEvent 对象都有一个 nativeEvent 属性,它指向原生事件。
  2. 支持所有事件:React 为所有的 DOM 事件提供了合成事件,包括键盘、表单、焦点、鼠标等等。

总的来说,React 的合成事件提供了一种高效、跨浏览器的方式来处理 DOM 事件。这使得 React 应用程序的事件处理既简单又一致。

3.3 版本更新

  • 16 以前利用事件池(数组存储事件),17 已经移除事件池
  • 在 React17 之前合成事件是委托给了文档对象
  • 在 React17 之后合成事件是委托给了 root,也就是根节点 <div id="root"></div>,目的是为了解决一个应用中可以使用多个跟节点的问题

3.4 react 事件基础

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
  • 你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault
js
import React from "react";
import ReactDOM from "react-dom/client";

class Counter extends React.Component {
  // 事件处理函数
  handleClick(e) {
    // 阻止默认事件
    e.preventDefault();
    alert("The link was clicked.");
  }
  render() {
    return (
      <a href="http://www.baidu.com" onClick={this.handleClick}>
        Click me
      </a>
    );
  }
}

const element = <Counter />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element);

3.5 this 指向

  • 事件函数写成箭头函数
  • 事件函数为普通函数,调用用匿名函数
  • bind 改变 this,在构造函数、render 中、调用方法中都可以 bind
  • 如果要传参数,只能使用匿名函数或者箭头函数,本质都一样的
js
import React from "react";
import ReactDOM from "react-dom/client";

class Counter extends React.Component {
  constructor(props) {
    super(props);

    // this.handleClick = this.handleClick.bind(this);
  }
  handleClick1 = () => {
    console.log("this is:", this);
  };
  handleClick2() {
    console.log("this is:", this);
  }
  handleClick3 = (amount) => {
    console.log("this is:", this, amount);
  };
  render() {
    // this.handleClick = this.handleClick.bind(this);
    return (
      <div>
        {/* 1 事件函数写成箭头函数(公共属性),this用于指向类的实例 */}
        <button onClick={this.handleClick1}>+</button>
        {/* 2 事件函数为普通函数,调用用匿名函数 */}
        <button onClick={() => this.handleClick2()}>-</button>
        {/* 3 bind改变this指向 */}
        <button onClick={this.handleClick2.bind(this)}>bind-</button>

        {/* 第一种:通过 bind 绑定 this 传参 */}
        <button onClick={this.handleClick3.bind(this, 3)}>bind传参</button>
        {/* 第二种:通过箭头函数绑定 this 传参 */}
        <button onClick={() => this.handleClick3(3)}>箭头传参</button>
      </div>
    );
  }
}

const element = <Counter />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element);

3.6 与原生事件的区别

特性原生事件React 合成事件
命名方式名称全部小写 onclick、onblur名称采用小驼峰 onClick、onBlur
函数语法字符串函数
事件绑定addEventListener 绑定JSX 属性直接绑定
事件对象原生 Event 对象SyntheticEvent
阻止默认行为方式return false 或 e.preventDefault()使用 e.preventDefault() 方法
事件冒泡处理默认冒泡,通过 e.stopPropagation() 阻止默认冒泡,通过 e.stopPropagation() 阻止
js
/* 1 事件名称命名方式不同 */
// 原生事件 - 全小写
<button onclick="handleClick()">原生事件</button>
// React 合成事件 - 小驼峰
const button = <button onClick={handleClick}>react合成事件</button>


/* 2 事件处理函数写法不同 */
// 原生事件 - 字符串
<button onclick="handleClick()">原生事件</button>
// React 合成事件 - 函数
const button = <button onClick={handleClick}>react合成事件</button>


/* 2 阻止默认行为方式不同 */
// 原生事件 - return false
<a href="" onclick="console.log('阻止原生事件'); return false">原生事件</a>
// React 合成事件 - e.preventDefault();
const handleClick = e => {
  e.preventDefault();
  console.log('阻止原生事件');
}
const clickElement = <a href="" onClick={handleClick}>react合成事件</a>

3.7 事件原理

  • 在 dom 节点上挂一个对象,存储各种监听函数,最后将整个事件委托到 document 节点上
  • 在点击事件的时候,会从事件源开始依次向上模拟冒泡,执行事件函数
js
import { updateQueue } from "./Component";

// 调用
function updateProps(dom, oldProps, newProps) {
  for (let key in newProps) {
    if (key === "children") {
    }
    if (key === "style") {
    } else if (key.startsWith("on")) {
      //onClick
      //dom[key.toLocaleLowerCase()]=newProps[key];//dom.onclick=handleClick
      addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
    }
  }
}

/**
 * 实现的事件委托,把所有的事件都绑定到document上
 * @param {*} dom dom 真实DOM
 * @param {*} eventType 事件类型onclick
 * @param {*} handler 合成事件监听函数
 */
export function addEvent(dom, eventType, handler) {
  // 先给dom绑定一个store属性,然后在里面绑定各种事件回调函数handler
  let store = dom.store || (dom.store = {});
  store[eventType] = handler; // store.onclick = handler;
  //如果有很多个元素都绑定 click事件,往document持的时候只挂一次
  // 事件委托,不管你给哪个DOM元素上绑事件,最后都统一代理到document上去了,而且如果有多次绑定,也只挂一次
  if (!document[eventType]) {
    document[eventType] = dispatchEvent;
  }
  // 等价于:document.addEventListener('click',dispatchEvent);
}

function dispatchEvent(event) {
  // target事件源=button,那个DOM元素 类型type=click
  let { target, type } = event;
  let eventType = `on${type}`; // onclick

  // 把队列设置为批量更新模式
  updateQueue.isBatchingUpdate = true;

  let syntheticEvent = createSyntheticEvent(event);
  //模拟事件冒泡的过程
  while (target) {
    let { store } = target;
    let handler = store && store[eventType];
    handler && handler.call(target, syntheticEvent);
    target = target.parentNode;
  }

  // 事件处理完毕之后,改为false,然后批量更新状态
  updateQueue.isBatchingUpdate = false;
  updateQueue.batchUpdate();
}

//在源码里此处做了一些浏览器兼容性的适配
function createSyntheticEvent(event) {
  let syntheticEvent = {};
  for (let key in event) {
    syntheticEvent[key] = event[key];
  }
  return syntheticEvent;
}

3.8 合成事件工作流程

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

3.9 React 16 版本的事件顺序 - 可以忽略不看了

js
import React from "react";
import ReactDOM from "react-dom";

class Counter extends React.Component {
  parentRef = React.createRef();
  childRef = React.createRef();

  componentDidMount() {
    this.parentRef.current.addEventListener(
      "click",
      () => {
        console.log("父元素原生捕获");
      },
      true
    );
    this.parentRef.current.addEventListener("click", () => {
      console.log("父元素原生冒泡");
    });

    this.childRef.current.addEventListener(
      "click",
      () => {
        console.log("子元素原生捕获");
      },
      true
    );
    this.childRef.current.addEventListener("click", () => {
      console.log("子元素原生冒泡");
    });

    document.addEventListener(
      "click",
      () => {
        console.log("document原生捕获");
      },
      true
    );
    document.addEventListener("click", () => {
      console.log("document原生冒泡");
    });
  }

  parentCapture = () => {
    console.log("父元素React事件捕获");
  };
  parentBubble = () => {
    console.log("父元素React事件冒泡");
  };

  childCapture = () => {
    console.log("子元素React事件捕获");
  };
  childBubble = () => {
    console.log("子元素React事件冒泡");
  };

  render() {
    return (
      <div
        ref={this.parentRef}
        onClick={this.parentBubble}
        onClickCapture={this.parentCapture}
      >
        <p
          ref={this.childRef}
          onClick={this.childBubble}
          onClickCapture={this.childCapture}
        >
          事件执行顺序
        </p>
      </div>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));

/**
document原生捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
 */

react-event

实现原理

js
<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>

<body>
	<div id="parent">
		<p id="child">事件执行</p>
	</div>

	<script>
		function dispatchEvent(event) {
			let paths = [];
			let current = event.target;
			while (current) { // 从事件源,一直查找到最顶层的事件
				paths.push(current);
				current = current.parentNode;
			}
			console.log(paths); //  [p#child, div#parent, body, html, document]

			//模拟捕获和冒泡,其实在这个时候,原生的捕获阶段已经结束 了
			// 先倒序执行,模拟捕获
			for (let i = paths.length - 1; i >= 0; i--) {
				let handler = paths[i].onClickCapture;
				handler && handler();
			}
			// 然后正序执行,模拟冒泡
			for (let i = 0; i < paths.length; i++) {
				let handler = paths[i].onClick;
				handler && handler();
			}
		}

		//注册React事件的事件委托
		document.addEventListener('click', dispatchEvent);

		let parent = document.getElementById('parent');
		let child = document.getElementById('child');
		parent.addEventListener("click", () => {
			console.log("父元素原生捕获");
		}, true);
		parent.addEventListener("click", () => {
			console.log("父元素原生冒泡");
		});
		child.addEventListener("click", () => {
			console.log("子元素原生捕获");
		}, true);
		child.addEventListener("click", () => {
			console.log("子元素原生冒泡");
		});

		document.addEventListener('click', () => {
			console.log("document原生捕获");
		}, true);
		//React会执行一个 document.addEventListener('click',dispatchEvent);
		//这个注册是在React注册这后注册的,所以后执行
		document.addEventListener('click', () => {
			console.log("document原生冒泡");
		});

		parent.onClickCapture = function () { console.log('父元素React事件捕获') }
		parent.onClick = function () { console.log('父元素React事件冒泡') }

		child.onClickCapture = function () { console.log('子元素React事件捕获') }
		child.onClick = function () { console.log('子元素React事件冒泡') }

		/**
		document原生捕获
		父元素原生捕获
		子元素原生捕获
		子元素原生冒泡
		父元素原生冒泡
		父元素React事件捕获
		子元素React事件捕获
		子元素React事件冒泡
		父元素React事件冒泡
		document原生冒泡
		*/
	</script>
</body>

</html>

3.10 React 17+版本的事件顺序

  • 更改事件委托: 首先第一个修改点就是更改了事件委托绑定节点,在 16 版本中,React 都会把事件绑定到页面的 document 元素上,这在多个 React 版本共存的情况下就会导致另外一个 React 版本上绑定的事件没有被阻止触发,所以在 17 版本中会把事件绑定到 render 函数的节点上
  • 去除事件池: 17 版本中移除了事件对象池,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用 event.persist() 才能正确的使用该事件,或者正确读取需要的属性
js
import React from "react";
import ReactDOM from "react-dom/client";

class ClassComponent extends React.Component {
  parentBubble() {
    console.log("React中父节点冒泡");
  }
  childBubble(event) {
    console.log("React子节点冒泡");
    // event.stopPropagation()
  }
  parentCapture(event) {
    console.log("React父节点捕获");
    //阻止事件传播
    //event.stopPropagation();
  }
  childCapture() {
    console.log("React子节点捕获");
  }
  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根节点捕获`);
    },
    true
  );
  document.getElementById("root").addEventListener(
    "click",
    () => {
      console.log(`  Native根节点冒泡`);
    },
    false
  );
  document.getElementById("parent").addEventListener(
    "click",
    () => {
      console.log(`  Native父节点捕获`);
    },
    true
  );
  document.getElementById("child").addEventListener(
    "click",
    () => {
      console.log(`  Native子节点捕获`);
    },
    true
  );
  document.getElementById("parent").addEventListener("click", () => {
    console.log(`  Native父节点冒泡`);
  });
  document.getElementById("child").addEventListener("click", () => {
    console.log(`  Native子节点冒泡`);
  });
}, 1000);

输出结果:

text
React父节点捕获
React子节点捕获
   Native根节点捕获
   Native父节点捕获
   Native子节点捕获
   Native子节点冒泡
   Native父节点冒泡
React子节点冒泡
React中父节点冒泡
   Native根节点冒泡

图解:

实现原理-事件代理

根据 props 属性上的 onClick 来判断自定义事件,然后覆盖容器组件上的事件函数

js
function renderElement(element) {
  // ...
  // 取出元素类型和属性对象
  const { type, props } = element;
  // 根据type创建真实的DOM节点
  const domElement = document.createElement(type);
  // 再处理属性,先出属性的数组
  Object.keys(props).forEach((name) => {
    if (name.startsWith("on")) {
      // 我们在domElement添加一个自定义属性reactEvents,用来存放React事件回调
      if (
        domElement.reactEvents === undefined ||
        domElement.reactEvents === null
      ) {
        domElement.reactEvents = {};
      }
      // domElement.reactEvents[onClick]=就是对应监听函数
      // domElement.reactEvents[onClickCapture]=就是对应监听函数
      domElement.reactEvents[name] = props[name];
    }
    // ...
  });
  // ...
}

给根组件绑定事件

js
function createRoot(container) {
  return {
    // 把虚拟DOM变成真实DOM并日插入容器container
    render(reactElement) {
      // 把虚拟DOM变成真实DOM
      const domElement = renderElement(reactElement);
      // 把此真实DOM添加到容器中
      container.appendChild(domElement);
      //设置事件代理
      setupEventDelegation(container);
    },
  };
}

事件代理

js
// src\react-dom\event.js

//定义一个事件类型的方法字典
const eventTypeMethods = {
  //key是原生的事件名 值是一个对象,对象有key和值,key是事件阶段,值是对应的绑定元素上的方法
  click: {
    capture: "onClickCapture",
    bubble: "onClick",
  },
  // ......
};

//事件传播的二个阶段
const phases = ["capture", "bubble"];

/**
 * 设置事件委托,把所有的事件都绑到容器container上
 * @param {*} container root根节点
 */
function setupEventDelegation(container) {
  //遍历所有的事件
  Reflect.ownKeys(eventTypeMethods).forEach((eventType) => {
    //遍历二个阶段capture bubble
    phases.forEach((phase) => {
      //给容器添加监听函数 eventType 事件的名称click nativeEvent 原生的事件对象
      //在React17以前此处绑定为document
      //在React17之后此处绑定为根节点根容器中 div#root
      container.addEventListener(
        eventType,
        (nativeEvent) => {
          // 要模拟事件传播的顺序,事件传递路径上所有的DOM元素上绑定的React事件取出来按顺序执行
          // composedPath() 是 Event 接口的一个方法,当对象数组调用该侦听器时返回事件路径
          // 返回一个 EventTarget对象数组,表示将在其上调用事件侦听器的对象。
          // 从子往父,从内往外的顺序
          const composedPath = nativeEvent.composedPath();

          //因为模拟冒泡和模拟捕获顺序是相反的
          //因为数组的顺序是从子向父,从内到外,其实是冒泡的顺序,如果是在捕获阶段执行需要倒序
          const domElements =
            phase === "capture" ? composedPath.reverse() : composedPath;
          //domElement.reactEvents[onClick]=就是对应监听函数
          //拼出来方法名 onClick onClickCapture
          const methodName = eventTypeMethods[eventType][phase];
          //遍历所有的DOM元素,执行它身上绑定的React事件监听函数
          for (let domElement of domElements) {
            //如果此DOM节点上绑定有回调函数,则执行它
            domElement.reactEvents?.[methodName]?.(nativeEvent);
          }
        },
        phase === "capture"
      );
    });
  });
}
export default setupEventDelegation;

实现原理-合成代理

将上述 nativeEvent 原生事件对象 包装为自定义的合成事件对象

js
function setupEventDelegation(container) {
  //遍历所有的事件
  Reflect.ownKeys(eventTypeMethods).forEach((eventType) => {
    //遍历二个阶段capture bubble
    phases.forEach((phase) => {
      container.addEventListener(
        eventType,
        (nativeEvent) => {
          // 根据原生事件创建和合成事件
          const syntheticEvent = createSyntheticEvent(nativeEvent);
          const composedPath = syntheticEvent.composedPath();
          const domElements =
            phase === "capture" ? composedPath.reverse() : composedPath;
          const methodName = eventTypeMethods[eventType][phase];
          //遍历所有的DOM元素,执行它身上绑定的React事件监听函数
          for (let domElement of domElements) {
            //如果某个方法执行的时候,已经调用了event.stopPropagation();,则表示阻止传播了,跳出循环
            if (syntheticEvent.isPropagationStopped()) {
              break;
            }
            //currentTarget是会变的,而target是不变的
            syntheticEvent.currentTarget = domElement;
            //如果此DOM节点上绑定有回调函数,则执行它
            domElement.reactEvents?.[methodName]?.(syntheticEvent);
          }
        },
        phase === "capture"
      );
    });
  });
}

合成事件对象:将原生的事件对象进行重新重写代理

js
/**
 * 根据原生事件对象创建合成事件对象
 * @param {*} nativeEvent
 */
function createSyntheticEvent(nativeEvent) {
  // 声明一个变量,表示当前的事件是否已经阻止传播了
  let isPropagationStopped = false;
  // 声明一个变量,表示当前的事件是否已经阻止默认行为了
  let isDefaultPrevented = false;
  const handler = {
    get(target, key) {
      // 如果此属性是target的自己定义的属性,则返回重写后的方法和属性
      if (target.hasOwnProperty(key)) {
        // 则直接返回被代理的对象的属性
        return Reflect.get(target, key);
      } else {
        // 先取出属性上的值
        const value = Reflect.get(nativeEvent, key);
        // 如果是函数的话绑死一下this,保证你在调用这些函数的时候它的this指向原生的事件对象
        return typeof value === "function" ? value.bind(nativeEvent) : value;
      }
    },
  };
  const target = {
    nativeEvent, //指向的是原生事件对象
    preventDefault() {
      if (nativeEvent.preventDefault) {
        nativeEvent.preventDefault(); //标准浏览器
      } else {
        nativeEvent.returnValue = false; //IE
      }
      isDefaultPrevented = true;
    },
    stopPropagation() {
      if (nativeEvent.stopPropagation) {
        nativeEvent.stopPropagation();
      } else {
        nativeEvent.cancelBubble = true;
      }
      isPropagationStopped = true;
    },
    stopImmediatePropagation() {
      if (nativeEvent.stopImmediatePropagation) {
        nativeEvent.stopImmediatePropagation();
      } else {
        nativeEvent.cancelBubble = true;
      }
      isPropagationStopped = true;
    },
    isDefaultPrevented() {
      return isDefaultPrevented;
    },
    isPropagationStopped() {
      return isPropagationStopped;
    },
  };
  //可以根据原生事件创建一个代理对象
  const syntheticEvent = new Proxy(target, handler);
  return syntheticEvent;
}

四、状态

  • 组件的数据来源有两个地方,分别是属性对象和状态对象
  • 属性是父组件传递过来的(默认属性,属性校验),不能修改
  • 状态是内部产生的,可以改,状态只能用在类组件里
  • 唯一能给 this.state 赋值的地方就是构造函数,只能初始值
  • 其它地方要想改变状态只能调用 setState()方法
  • 每当你调用 setState 方法的时候就会引起组件的刷新,组件会重新调用一次 render 方法,得到新虚拟 DOM,进行 DOM 更新
  • 属性和状态的变化都会影响视图更新

0 setState 是同步的还是异步的?

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

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

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

1 使用

js
import React from "react"; // react核心库
import ReactDOM from "react-dom"; // react的dom渲染库

// 定时器,每秒更新+1
class Clock extends React.Component {
  constructor(props) {
    super(props);
    // 初始化状态
    this.state = {
      date: new Date(),
    };
    setInterval(this.tick, 1000);
  }

  tick = () => {
    // Do not mutate state directly. Use setState()
    // 通过setState()修改状态
    this.setState({ date: new Date() });
  };

  render() {
    return (
      <div>
        <h1>Hello</h1>
        <h2>当前的时间:{this.state.date.toLocaleTimeString()}</h2>
      </div>
    );
  }
}

let element = <Clock />;

ReactDOM.render(element, document.getElementById("root"));

2 异步更新

  • 状态是封闭的,只有组件自己能访问和修改
  • 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用,进行批量执⾏, 多 key 一次执⾏,相同 key 合并
  • 可能是异步的,在事件处理函数中或生命周期函数中批量更新是异步的,其它地方都是直接同步更新的
js
import React from "react"; // react核心库
import ReactDOM from "react-dom"; // react的dom渲染库

class Counter extends React.Component {
  state = { number: 0 };

  /**
   * 在事件处理函数中,
   *   setState 的调用会批量执行
   *   setState 不会修改this.state,需要等实际处理函数结束之后,再进行更新
   */
  handleClick = () => {
    // setState第一个参数为对象的情况
    this.setState({ number: this.state.number + 1 });
    console.log(this.state.number); // 0
    this.setState({ number: this.state.number + 1 });
    console.log(this.state.number); // 0
  };
  // 点击第一次   打印两个0  最后状态变为1
  // 点击第二次   打印两个1  最后状态变为2

  render() {
    return (
      <div>
        <p>number:{this.state.number}</p>
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));

setState 第一个参数为函数的情况

js
state = { number: 0 };

handleClick = () => {
  // 函数进行修改,第一个函数的状态返回值为第二个函数的参数
  this.setState((state) => ({ number: state.number + 1 }));
  console.log(this.state.number); // 0
  this.setState((state) => ({ number: state.number + 1 }));
  console.log(this.state.number); // 0
};
// 点击第一次   打印两个0  最后状态变为2
// 点击第二次   打印两个2  最后状态变为4

// 原理如下:
let queue = [];
queue.push((state) => ({ number: state.number + 1 }));
queue.push((state) => ({ number: state.number + 1 }));

let state = { number: 0 };
let result = queue.reduce((newState, action) => {
  return action(newState);
}, state);
console.log(result); // 2

setState 的第二个参数可以获取立刻更新的值

js
state = { number: 0 };

handleClick = () => {
  // setState第二个参数为回调函数,里面可以获取每次更新的最新值
  this.setState({ number: this.state.number + 1 }, () => {
    console.log("callback1", this.state.number); // 1
  });
  console.log(this.state.number); // 0
  this.setState({ number: this.state.number + 1 }, () => {
    console.log("callback2", this.state.number); // 1
  });

  // 点击第一次,因为批量执行,最后再回调函数中打印的是1
  // 点击第二次,因为批量执行,最后再回调函数中打印的是2
};
  • React 能管控的地方,就是批量的、异步的,比如事件处理函数中、生命周期中
  • React 不能管控的地方,就是非批量的、同步的,比如 setInterval setTimeout 原生 DOM 事件等
  • 如果要同步获取最新状态值,三种⽅方式:
    1. 传递回调函数给 setState
    2. 写在定时器中
    3. 写在 JS 的原生事件中
js
state = { number: 0 };

handleClick = () => {
  //在其它react不能管控的地方,就是同步执行的
  Promise.resolve().then(() => {
    this.setState({ number: this.state.number + 1 }, () => {
      console.log("callback3", this.state.number); // 1
    });
    console.log(this.state.number); // 1
    this.setState({ number: this.state.number + 1 }, () => {
      console.log("callback4", this.state.number); // 2
    });
    console.log(this.state.number); // 2
  });
  setTimeout(() => {
    this.setState((state) => ({ number: state.number + 1 }));
    console.log(this.state.number); // 3
    this.setState((state) => ({ number: state.number + 1 }));
    console.log(this.state.number); // 4
  });
};

原生事件中如果要组件刷新,需要调用 forceUpdate

js
/**
 * 组件强制更新方法
 * 1.获取老的虚拟DOM =》 React元素
 * 2.根据最新的属生和状态计算新的虚拟DOM
 * 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
 */
forceUpdate(){
	console.log('updateComponent');
	let oldRenderVdom = this.oldRenderVdom;//老的虚拟DOM
	//根据老的虚拟DOM查到老的真实DOM
	let oldDOM = findDOM(oldRenderVdom);
	let newRenderVdom = this.render();//计算新的虚拟DOM
	compareTwoVdom(oldDOM.parentNode,oldRenderVdom,newRenderVdom);//比较差异,把更新同步到真实DOM上
	this.oldRenderVdom=newRenderVdom;
}

3 批量更新器

js
// 更新器
class Updater {
  constructor() {
    // 初始状态
    this.state = { name: "test", number: 0 };
    this.queue = [];
  }
  // 更新状态方法
  setState(newState) {
    this.queue.push(newState);
  }
  // 批量更新
  flush() {
    for (let i = 0; i < this.queue.length; i++) {
      let update = this.queue[i];
      if (typeof update === "function") {
        this.state = { ...this.state, ...update(this.state) };
      } else {
        this.state = { ...this.state, ...update };
      }
    }
  }
}
let updater = new Updater();
updater.setState({ number: 1 });
updater.setState((previousState) => ({ number: previousState.number + 1 }));
updater.setState({ number: 2 });
updater.setState({ number: 3 });

updater.flush();
console.log(updater.state); // { name: 'test', number: 3 }

4 同步更新器

js
// 更新器
class Updater {
  constructor() {
    // 初始状态
    this.state = { name: "test", number: 0 };
    this.queue = [];
  }
  // 更新状态方法
  setState(update) {
    if (typeof update === "function") {
      this.state = { ...this.state, ...update(this.state) };
    } else {
      this.state = { ...this.state, ...update };
    }
  }
}
let updater = new Updater();
updater.setState({ number: 1 });
console.log(updater.state); // { name: 'test', number: 1 }
updater.setState((previousState) => ({ number: previousState.number + 1 }));
console.log(updater.state); // { name: 'test', number: 2 }
updater.setState({ number: 3 });
console.log(updater.state); // { name: 'test', number: 3 }
updater.setState({ number: 4 });
console.log(updater.state); // { name: 'test', number: 4 }

5 批量实现代码

js
// /定义一个布尔值的变量,用来控制当前是否处于批量更新模式
let isBatchingUpdates = false;
// 定义一个元素是不能重复的集合,有待更新的组件称为dirtyComponent
const dirtyComponents = new Set();
// 定义一个可以给isBatchingUpdates赋值的函数
export function setIsBatchingUpdates(value) {
  isBatchingUpdates = value;
}
// 更新脏组件
export function flushDirtyComponents() {
  dirtyComponents.forEach((component) => component.forceUpdate());
  dirtyComponents.clear(); //清空集合
  isBatchingUpdates = false; //更新完成要关闭批理更新
}

class Component {
  // ...
  setState(partialState) {
    //如果当前处于批量更新模式,则把此当前的实例添加到脏组件集合中
    if (isBatchingUpdates) {
      dirtyComponents.add(this);
    } else {
      //如果当前处于非批量更新模式,则直接更新
      //如果partialState是一个函数的话,传入老状态,计算新状态
      const newState =
        typeof partialState === "function"
          ? partialState(this.state)
          : partialState;
      //合并老状态和新状态为最终的状态
      this.state = {
        ...this.state,
        ...newState,
      };
      //计算完新的状态后要更新组件
      scheduleUpdate();
    }
  }
  //根据this.pendingStates计算新状态
  accumulateState = () => {
    //取出老状态,然后依次执行更新队列中的新状态,计算出最终的新状态
    let state = this.pendingStates.reduce((state, partialState) => {
      const newState =
        typeof partialState === "function" ? partialState(state) : partialState;
      //合并老状态和新状态为最终的状态
      return {
        ...state,
        ...newState,
      };
    }, this.state);
    //清空更新队列
    this.pendingStates.length = 0;
    //返回新状态
    return state;
  };
  forceUpdate() {
    this.state = this.accumulateState();
  }
}

事件调用控制变量状态

js
export default function setupEventDelegation(container) {
  ["capture", "bubble"].forEach((phase) => {
    Reflect.ownKeys(eventTypeMethods).forEach((type) => {
      container.addEventListener(
        type,
        (nativeEvent) => {
          // ...
          +setIsBatchingUpdates(true);
          for (let element of elements) {
            if (syntheticEvent.isPropagationStopped()) {
              break;
            }
            element.reactEvents?.[methodName]?.(syntheticEvent);
          }
          +flushDirtyComponents();
        },
        phase === "capture"
      );
    });
  });
}

五、Ref

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

1 原生使用 Ref

  • 可以使用 ref 去存储 DOM 节点的引用
  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  • ref 的本质就是一个对象,默认 key 给 current,值为 null
js
import React from "react";
import ReactDOM from "react-dom";

class Counter extends React.Component {
  constructor(props) {
    super(props);
    // 创建Ref
    this.numberA = React.createRef(); // {current:null}
    this.numberB = React.createRef();
    this.result = React.createRef();
  }
  handleClick = (event) => {
    let numberA = this.numberA.current.value;
    let numberB = this.numberB.current.value;
    this.result.current.value = parseFloat(numberA) + parseFloat(numberB);
  };

  render() {
    return (
      <>
        {/* 原生DOM使用Ref */}
        <input ref={this.numberA} />
        <input ref={this.numberB} />
        <button onClick={this.handleClick}>+</button>
        <input ref={this.result} />
      </>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));

2 类组件使用 Ref

  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
js
import React from "react";
import ReactDOM from "react-dom";

class TextInput extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  getTextInputFocus = () => {
    this.inputRef.current.focus();
  };
  render() {
    return <input ref={this.inputRef} />;
  }
}
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.textInputRef = React.createRef();
  }
  getFormFocus = () => {
    // this.textInputRef.current就会指向TextInput类组件的实例
    this.textInputRef.current.getTextInputFocus();
  };

  render() {
    return (
      <>
        {/* 类组件使用Ref */}
        <TextInput ref={this.textInputRef} />
        <button onClick={this.getFormFocus}>获得焦点</button>
      </>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));

实现原理

js
function createRef() {
  return { current: null };
}

function createElement(type, config, children) {
  let ref; //是用来获取虚拟DOM实例的
  if (config) {
    ref = config.ref;
    delete config.ref;
  }
  return {
    type,
    props,
    ref,
    key,
  };
}

function createDOM(vdom) {
  let { type, props, ref } = vdom;
  let dom; //获取 真实DOM元素
  // TODO 。。。其他逻辑
  //让虚拟DOM的dom属生指向它的真实DOM
  vdom.dom = dom;
  if (ref) ref.current = dom; //让ref.current属性指向真实DOM的实例
  return dom;
}

3 函数组件使用 Ref

  • 不能在函数组件上使用 ref 属性,因为他们没有实例
  • 但是可以通过 Ref 转发,将函数组件进行包裹,将其 Ref 传递下去
js
import React from "react";
import ReactDOM from "react-dom";

function TextInput(props, ref) {
  return <input ref={ref} />;
}
// forwardRef 包裹函数组件
const ForwardedTextInput = React.forwardRef(TextInput);

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.textInputRef = React.createRef();
  }
  getFormFocus = () => {
    // this.textInputRef.current就会指向TextInput类组件的实例
    this.textInputRef.current.focus();
  };

  render() {
    return (
      <>
        {/* 函数组件使用Ref */}
        <ForwardedTextInput ref={this.textInputRef} />
        <button onClick={this.getFormFocus}>获得焦点</button>
      </>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));

实现原理: 给 forwardRef 返回的对象上加上 ref 标识,然后在虚拟 DOM 转为真实 DOM 的时候,解析并判断类型,最后执行函数组件的 render 方法,传入 ref,最后挂载到父组件的 ref.current 属性上

js
const REACT_FORWARD_REF_TYPE = Symbol("react.forward_ref");
// 1 加上标识,返回一个对象
function forwardRef(render) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render, // 原来那个函数件
  };
}

// 2 在虚拟DOM转成真实DOM的时候,判断类型,然后进行特殊处理
function createDOM(vdom) {
  let { type, props, ref } = vdom;
  let dom; //获取 真实DOM元素
  //如果type.$$typeof属性是REACT_FORWARD_REF_TYPE值
  if (type && type.$$typeof === REACT_FORWARD_REF_TYPE) {
    return mountForwardComponent(vdom);
  }
  // TODO 。。。其他逻辑
  //让虚拟DOM的dom属生指向它的真实DOM
  vdom.dom = dom;
  if (ref) ref.current = dom; //让ref.current属性指向真实DOM的实例
  return dom;
}

function mountForwardComponent(vdom) {
  let { type, props, ref } = vdom;
  // 执行函数组件的render方法
  let renderVdom = type.render(props, ref);
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

六、生命周期

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

  1. Mounting(挂载): 这些方法会在组件创建并插入到 DOM 中时被调用。
    • constructor: 组件的构造函数,最先被调用,用于初始化本地状态和绑定事件处理函数。
    • static getDerivedStateFromProps: 当 props 发生变化时,在 render 方法之前被调用。它应返回一个对象来更新状态或返回 null 表示不更新任何内容。
    • render: 唯一必需的方法。读取 this.props 和 this.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: 此生命周期在报告错误日志时非常有用。 注意:

React 16.3 开始引入了新的生命周期方法,并且弃用了 componentWillMount, componentWillReceiveProps 和 componentWillUpdate。推荐使用新的生命周期方法。

1 旧版生命周期

sh
初始化 挂载 更新 卸载

react

  • 状态更新生命周期变化
js
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	static defaultProps = {
		name: '计算器'
	}

	constructor(props) {
		super(props);
		this.state = {number: 0};
		console.log("Counter 1.constructor 初始化属性和状态");
	}

	handleClick = () => {
		this.setState({number: this.state.number + 1});
	}

	componentWillMount() {
		console.log("Counter 2.componentWillMount 组件将要挂载");
	}

	componentDidMount() {
		console.log("Counter 4.componentWillMount 组件挂载完成");
	}

	// setState会引起状态的变化,父组件更新的时候,会让子组件的属性发生变化
	// 当属性或者状态发生改变的话,会走此方法来决定 是否要渲染更新
	shouldComponentUpdate(nextProps, nextState) {
		console.log('Counter 5.shouldComponentUpdate 决定组件是否需要更新?');
		// 偶数为true 需要更新,奇数为false 不需要更新
		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>
		)
	}
}

ReactDOM.render(<Counter />, document.getElementById('root'))

// 初始化挂载
Counter 1.constructor 初始化属性和状态
Counter 2.componentWillMount 组件将要挂载
Counter 3.render 挂载
Counter 4.componentWillMount 组件挂载完成

// 点击加号,返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,返回true
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
Counter 7.componentDidUpdate 组件更新完成
  • 状态和属性一起变化的生命周期
js
import React from "react";
import ReactDOM from "react-dom";

class ChildCounter extends React.Component {
  static defaultProps = {
    // 1.设置默认属性
    name: "ChildCounter",
  };

  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 决定组件是否需要更新?");
    // 3的倍数就更新,否则就不更新
    return nextProps.count % 3 === 0;
  }

  componentWillUpdate() {
    console.log("ChildCounter 7.componentWillUpdate  组件将要更新");
  }

  componentDidUpdate() {
    console.log("ChildCounter 8.componentDidUpdate 组件更新完成");
  }

  componentWillUnmount() {
    console.log("ChildCounter 9.componentWillUnmount 组件将要卸载");
  }
}

class Counter extends React.Component {
  static defaultProps = {
    name: "Counter", // 设值默认属性
  };

  constructor(props) {
    super(props);
    this.state = { number: 0 }; // 设值默认状态
    console.log("Counter 1.constructor 初始化属性和状态");
  }

  handleClick = () => {
    this.setState({ number: this.state.number + 1 });
  };

  componentWillMount() {
    console.log("Counter 2.componentWillMount 组件将要挂载");
  }

  componentDidMount() {
    console.log("Counter 4.componentWillMount 组件挂载完成");
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("Counter 5.shouldComponentUpdate 决定组件是否需要更新?");
    // 偶数为true 需要更新,奇数为false 不需要更新
    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>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));
js
// 初始化挂载
Counter 1.constructor 初始化属性和状态
Counter 2.componentWillMount 组件将要挂载
Counter 3.render 挂载
	ChildCounter 1.componentWillMount 组件将要挂载
	ChildCounter 2.render
	ChildCounter 3.componentDidMount 组件挂载完成
Counter 4.componentWillMount 组件挂载完成

// 点击加号,state = 1,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,state = 2,偶数返回true
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性
	ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 7.componentDidUpdate 组件更新完成

// 点击加号,state = 3,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,state = 4,偶数返回true,子组件卸载
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 9.componentWillUnmount 组件将要卸载
Counter 7.componentDidUpdate 组件更新完成

// 点击加号,state = 5,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?

// 点击加号,state = 6,偶数返回true,子组件重新渲染
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 1.componentWillMount 组件将要挂载
	ChildCounter 2.render
	ChildCounter 3.componentDidMount 组件挂载完成
Counter 7.componentDidUpdate 组件更新完成

// ......  一直点击加号到12
// 点击加号,state = 12,偶数返回true,子组件更新
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
	ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性
	ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?
	ChildCounter 7.componentWillUpdate  组件将要更新
	ChildCounter 2.render
	ChildCounter 8.componentDidUpdate 组件更新完成
Counter 7.componentDidUpdate 组件更新完成

2 新版生命周期

sh
创建时 更新时 卸载时

react

  • V17 可能会废弃的三个⽣命周期函数⽤ getDerivedStateFromProps 替代,⽬前使⽤的话加上 UNSAFE_:
    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate
  • 引⼊两个新的⽣命周期函数:
    • static getDerivedStateFromProps
    • getSnapshotBeforeUpdate
  • 变更缘由: 原来(React v16.0 前)的⽣命周期在 React v16 推出的 Fiber 之后就不合适了,因为如果要开启 async rendering,在 render 函数之前的所有函数,都有可能被执⾏多次
js
import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
	static defaultProps = {// 1.设置默认属性
		name: 'ChildCounter'
	}

	constructor(props) {
		super(props);
		this.state = { number: 0 }; // 设值默认状态
		console.log("Counter 1.constructor 初始化属性和状态");
		this.tmpRef = React.createRef();
	}

	/**
	 * 替换旧的componentWillReceiveProps
	 * 从组件的新属性中映射出一个状态
	 * 设计为静态(static)方法的原因:
	 *   1 WillReceiveProps里面调用setState 可能会让父组件刷新,父组件一刷新,子组件也会刷新,会出现死循环
	 *   2 因为static中没有this,就不能调用this.setState方法,就可以避免死循环
	 */
	static getDerivedStateFromProps(nextProps, prevState) {
		console.log("Counter 2.getDerivedStateFromProps", prevState);
		const { number } = prevState;
		if (number % 2 === 0) {
			return { number: number * 2 };
		} else if (number % 3 === 0) {
			return { number: number * 3 }; // 如果返回的分状态,会跟自己的state进行合并
		}
		return null; // 如果返回null,表示不修改状态
	}

	shouldComponentUpdate(nextProps, nextState) {
		console.log('Counter 4.shouldComponentUpdate 决定组件是否需要更新?', nextState);
		// 3的倍数就更新,否则就不更新
		return nextState.number % 4 === 0;
	}

	handleClick = () => {
		this.setState({ number: this.state.number + 1 });
	}

	render() {
		console.log("Counter 3.render");
		return (
			<div ref={this.tmpRef}>
				<p>{this.state.number}</p>
				<button onClick={this.handleClick}>+</button>
			</div>
		)
	}

	componentDidMount() {
		console.log("Counter 4.componentWillMount 挂载完成");
	}

	// 在DOM更新前执行,可以用来获取更新前的一些DOM信息
	getSnapshotBeforeUpdate() {
		console.log('Counter 6.getSnapshotBeforeUpdate 获取DOM更新前的信息');
		return this.tmpRef.current.scrollHeight;
	}
	componentDidUpdate(pervProps, pervState, scrollHeight) {
		//当前向上卷去的高度加上增加的内容高度
		console.log('Counter 7.componentDidUpdate 更新完成');
		console.log('打印高度:', this.tmpRef.current.scrollHeight, scrollHeight);
	}

	componentWillUnmount() {//清除定时器
		console.log('Counter 8.componentWillUnmount 卸载');
	}
}
ReactDOM.render(<Counter />, document.getElementById('root'))

// 初始化
Counter 1.constructor 初始化属性和状态
Counter 2.getDerivedStateFromProps {number: 0}
Counter 3.render
Counter 4.componentWillMount 挂载完成

// 点击加号,state = 1
Counter 2.getDerivedStateFromProps {number: 1}
Counter 4.shouldComponentUpdate 决定组件是否需要更新? {number: 1}

// 点击加号,state = 2
Counter 2.getDerivedStateFromProps {number: 2}
Counter 4.shouldComponentUpdate 决定组件是否需要更新? {number: 4}
Counter 3.render
Counter 6.getSnapshotBeforeUpdate 获取DOM更新前的信息
Counter 7.componentDidUpdate 更新完成
打印高度:60 60

七、Context

1 组件通信

  • Props 属性传递可⽤于⽗⼦组件相互通信
  • Context 跨层级组件之间通信
  • Redux ⽆明显关系的组件间通信

2 Context

  • React 中使⽤ Context 实现祖代组件向后代组件跨层级传值。Vue 中的 provide & inject 来源于 Context,在 Context 模式下有两个⻆⾊:
    • Provider:外层提供数据的组件
    • Consumer:内层获取数据的组件
  • 使用 Context: 创建 Context => 获取 Provider 和 Consumer => Provider 提供值 => Consumer 消费值
  • React Context

3 使用案例

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

使用 Context 的步骤

  • 使用 React.createContext 创建一个新的上下文。
  • 使用 Context.Provider 组件为子组件提供上下文值。
  • 使用 Context.Consumer 组件或 useContext hook 在任何子组件中访问上下文值。
js
import React from "react";
import ReactDOM from "react-dom/client";

//1.使用React.createContext创建一个新的上下文
const BorderContext = React.createContext("orange");
//2.通过Context.Provider组件为下级组件提供上下文的值
//3.在子组件可以获取上下文的值
const { Provider: BorderProvider, Consumer: BorderConsumer } = BorderContext;

function BorderBox(props) {
  return (
    <BorderConsumer>
      {(contextValue) => (
        <div
          style={{
            margin: "5px",
            padding: "5px",
            border: `5px solid ${contextValue.color}`,
          }}
        >
          {props.children}
        </div>
      )}
    </BorderConsumer>
  );
}
//在类组件如何获取上层组件Provider传递过来的值
//可以给类组件添加一个动态属性contextType 值就是一个Context对象
class Header extends React.Component {
  render() {
    return (
      <BorderBox>
        Header
        <BorderBox>标题</BorderBox>
      </BorderBox>
    );
  }
}

class Main extends React.Component {
  render() {
    return (
      <BorderBox>
        Main
        <Content />
      </BorderBox>
    );
  }
}
function ThemedButton(props) {
  return (
    <BorderConsumer>
      {(contextValue) => (
        <button onClick={() => contextValue.changeColor(props.color)}>
          {props.label}
        </button>
      )}
    </BorderConsumer>
  );
}
function Content() {
  return (
    <BorderBox>
      Content
      <ThemedButton label="变红" color="red" />
      <ThemedButton label="变绿" color="green" />
    </BorderBox>
  );
}
class Footer extends React.Component {
  static contextType = BorderContext;
  //如果类上有contextType静态属性的话,那么实例的context属性就会指向context的当前值
  render() {
    return (
      <div style={{ border: `5px solid ${this.context.color}` }}>Footer</div>
    );
  }
}

class App extends React.Component {
  state = { color: "black", fontColor: "blue" };
  changeColor = (color) => {
    this.setState({ color });
  };
  render() {
    const contextValue = {
      color: this.state.color,
      changeColor: this.changeColor,
    };
    return (
      <BorderProvider value={contextValue}>
        <BorderBox>
          Page
          <Header />
          <Main />
          <Footer />
        </BorderBox>
      </BorderProvider>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

4 实现代码

js
function createContext(defaultValue) {
  const context = {
    _currentValue: defaultValue, //这个表示当前的值
    Provider: (props) => {
      //把属性中接收过来的value属性保存在context的_currentValue属性上
      context._currentValue = props.value;
      return props.children; //真正就是它的子元素
    },
    Consumer: (props) => {
      // Consumer也是一个函数组件,
      //它渲染的是子组件函数返回的结果 。函数的参数是context的当前值
      return props.children(context._currentValue);
    },
  };
  return context;
}

八、React 如何避免不必要的渲染?

  • 在 React 里面每次更新都是从根节点开始的,这一点和 Vue 是不同的
  • 因为每次都是根节点开始对比 DOMDIFF 的,所以渲染的工作量很大。
  • 所以我们需要尽量减少渲染

1 PureComponent - 类组件

  • 对于类组件来说,可以使用一个 React.PureComponent
  • PureComponent 是 React 提供的一个组件基类,它的核心特性是只有当它的 属性(props) 或 状态(state) 发生浅层变化时,它才会重新渲染。浅层比较会检查对象顶层的属性,而不是深度检查。如果对象的顶层属性没有变化,那么 PureComponent 就不会重新渲染,这可以提高性能。
  • 使用 PureComponent 最适合于那些组件的 props 和 state 结构较简单,或者可以确保结构不会经常发生深度变化的场景。

1.1 使用案例

js
import React from "react";
import ReactDOM from "react-dom/client";
class RegularComp extends React.Component {
  render() {
    // 会更新
    console.log(`Rendering RegularComp`);
    return <div>{this.props.value}</div>;
  }
}
class PureComp extends React.PureComponent {
  render() {
    // 不会更新
    console.log(`Rendering PureComponent`);
    return <div>{this.props.value}</div>;
  }
}
class App extends React.Component {
  state = { data: { value: 1 } };
  render() {
    return (
      <div>
        <button onClick={() => this.setState({ data: this.state.data })}>
          +
        </button>
        <RegularComp value={this.state.data.value} />
        <PureComp value={this.state.data.value} />
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

截图:todo

1.2 实现代码

  • 继承 Component 类组件,然后重写方法 shouldComponentUpdate,对属性和状态都做浅比较
js
class PureComponent extends Component {
  // 重写shouldComponentUpdate方法
  shouldComponentUpdate(nextProps, nextState) {
    // 如果属性不相等,或者状态不相等,就返回true进行更新
    return (
      !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
    );
  }
}

// 两个值的浅比较方法
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) {
    // 如果obj2中没有这个属性,或者有此属性但是值不相等
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }
  return true;
}

总结

  • PureComponent 表示一个纯组件,可以用来优化 React 程序,减少 render 函数执行的次数,从而提高组件的性能
  • PureComponent 会重写并自动执行 shouldComponentUpdate 方法,对属性和状态都做浅比较
  • PureComponent 的优点:当组件更新时,如果组件的 props 或者 state 都没有改变,render 函数就不会触发。省去虚拟 DOM 的生成和对比过程,达到提升性能的目的

2 React.memo - 函数组件

  • React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件
  • React.memo 默认是浅比较,可以传入第二个参数进行自定义深比较
  • React.memo 是一个高阶组件,它可以用于优化那些仅仅依赖于其 props 变化的组件的重新渲染行为。换句话说,如果组件的 props 在连续的渲染之间没有发生变化,那么使用 React.memo 可以避免组件的不必要的重新渲染。
  • 如果你有一个功能组件并且你想避免该组件因父组件的重新渲染而不必要地重新渲染,你可以通过 React.memo 包裹

2.1 工作原理

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

2.2 使用案例

js
import React from "react";
import ReactDOM from "react-dom/client";

function Counter(props) {
  console.log("Rendering Counter");
  return <h1>{props.count}</h1>;
}
// memo是一个高阶组件,高阶组件就是一个函数,接收一个老组件,返回一个新组件
// 如果当一个组件被 React.memo包裹的时候,React会记住该组件上一次渲染的结果
// 当这个组件再次重新渲染的时候,React会进行浅比较当前的属性对象和上一次的属性对象
// 如果属性没有发生变化,React会重用上一次的渲染结果,而不是重新渲染组件
// 如果属性发生了变化,则组件会重新渲染
const MemoCounter = React.memo(Counter);

class App extends React.Component {
  state = { count: 0 };
  incrementCount = () => {
    this.setState((prevState) => ({ count: prevState.count + 0 }));
  };
  render() {
    return (
      <div>
        <MemoCounter count={this.state.count} />
        <button onClick={this.incrementCount}>+</button>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

2.3 实现原理

  • React.memo 返回的是一个对象
js
// src\react.js

// react中定义具体的虚拟dom对象,在react-dom中domdiff的时候判断类型比做出具体渲染
export const REACT_MEMO = Symbol("react.memo");

function memo(type, compare = shallowEqual) {
  return {
    $$typeof: REACT_MEMO,
    type, //原来那个真正的函数组件
    compare,
  };
}
js
// 老版本

// memo封装了PureComponent
function memo(OldComponent) {
  return class extends React.PureComponent {
    render() {
      return <OldComponent {...this.props} />;
    }
  };
}
js
// 新版本
// src\react-dom\client.js

// 1 创建虚拟dom的时候进行处理
export function createDOMElement(vdom) {
  // ...
  if (type.$$typeof === REACT_MEMO) {
    //memo是包装后的函数组件,里面也有hook,所以也需要初始化hooks
    return createReactMemoDOMElement(vdom);
  }
  // ...
}

function createReactMemoDOMElement(vdom) {
  const { type, props } = vdom;
  const renderVdom = type.render(props);

  // 在获取到函数组件的返回的虚拟DOM之后,记录一下
  // 让当前的函数组件的虚拟DOM的oldRenderVdom属性指向它返回虚拟DOM
  vdom.oldRenderVdom = renderVdom;
  // 把函数组件返回的React元素传递给createDOMElement,创建真实DOM
  return createDOMElement(renderVdom);
}

// 2 更新虚拟dom的时候进行处理
function updateVdom(oldVdom, newVdom) {
  const { type } = oldVdom;
  if (type.$$typeof === REACT_MEMO) {
    return updateReactMemoComponent(oldVdom, newVdom);
  }
  // ...
}

function updateReactMemoComponent(oldVdom, newVdom) {
  let { type, props } = newVdom;
  const { render, compare } = type;
  // 比较新的虚拟DOM和老的虚拟DOM
  if (compare(props, oldVdom.props)) {
    // 如果相等说明属性没有变化
    // 复用一次的渲染结果
    newVdom.oldRenderVdom = oldVdom.oldRenderVdom;
    return;
  }
  // 如果属性不相等,则重新执行渲染函数得到新的虚拟DOM
  let renderVdom = render(props);
  // 进行DOM-DIFF更新
  compareVdom(
    getDOMElementByVdom(oldVdom)?.parentNode,
    oldVdom.oldRenderVdom,
    renderVdom
  );
  // 缓存本次渲染的虚拟DOM,以便进行下次的对比
  newVdom.oldRenderVdom = renderVdom;
}

九、组件高级

1 组件通信

  • Props 属性传递可⽤于⽗⼦组件相互通信
  • Context 跨层级组件之间通信
  • Redux ⽆明显关系的组件间通信

2 高阶组件 HOC

  • 高阶组件就是一个工厂函数,传给它一个组件,它返回一个新的组件
  • 高阶组件的作用其实就是为了组件之间的代码复用,主要用途为 属性代理 和反向继承
  • 高级组件来自于高阶函数

属性代理:对原有属性进行扩展

js
import React from "react";
import ReactDOM from "react-dom";

const withLoading = (OldComponent) => {
  return class extends React.Component {
    show = () => {
      let loading = document.createElement("div");
      loading.innerHTML = `<p id="loading" style="position:absolute;top:100px;left:50%;z-index:10;">loading</p>`;
      document.body.appendChild(loading);
    };
    hide = () => {
      document.getElementById("loading").remove();
    };
    render() {
      // 属性代理:对原有属性进行扩展
      return <OldComponent {...this.props} show={this.show} hide={this.hide} />;
    }
  };
};

class Panel extends React.Component {
  render() {
    return (
      <div>
        {this.props.title}
        <button onClick={this.props.show}>显示</button>
        <button onClick={this.props.hide}>隐藏</button>
      </div>
    );
  }
}
// 写法一:直接包装使用
let LoadingPanel = withLoading(Panel);
ReactDOM.render(
  <LoadingPanel title="这是标题" />,
  document.getElementById("root")
);
  • 装饰器写法
js
// 1. 安装
npm i react-app-rewired customize-cra @babel/plugin-proposal-decorators -D

// 2. 修改package.json
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
}

// 3. 编写config-overrides.js
const { override, addBabelPlugin } = require('customize-cra');

module.exports = override(
	addBabelPlugin(
		[
			"@babel/plugin-proposal-decorators", { "legacy": true }
		]
	)
);

// 4. 编写jsconfig.json
{
    "compilerOptions": {
         "experimentalDecorators": true
    }
}

// 5. 修改高阶组件代码,使用@进行扩展
const withLoading = (OldComponent) => {}

@withLoading
class Panel extends React.Component {
	....
}
ReactDOM.render(<Panel title="这是标题" />, document.getElementById('root'));

反向继承:拦截生命周期、state、渲染过程

js
// 一个场景:对第三方组件进行扩展

import React from 'react';
import ReactDOM from 'react-dom';

// 第三方组件,不能修改里面的代码,只能进行扩展
class AntDesignButton extends React.Component {
	state = { name: '张三' }
	componentWillMount() {
		console.log('AntDesignButton componentWillMount');
	}
	componentDidMount() {
		console.log('AntDesignButton componentDidMount');
	}
	render() {
		console.log('AntDesignButton render');
		return <button name={this.state.name} title={this.props.title} />
	}
}

// 高阶组件:反向继承
const wrapper = OldComponent => {
	return class extends OldComponent {
		state = { number: 0 }
		componentWillMount() {
			console.log('wrapper componentWillMount');
			super.componentWillMount();
		}
		componentDidMount() {
			console.log('wrapper componentDidMount');
			super.componentDidMount();
		}
		handleClick = () => {
			this.setState({ number: this.state.number + 1 });
		}
		render() {
			console.log('wrapper render');
			// 获取父组件的虚拟dom
			let renderElement = super.render();
			// 扩展新属性
			let newProps = {
				...renderElement.props,
				onClick: this.handleClick
			}
			// 老元素     新属性    新儿子
			return React.cloneElement(renderElement, newProps, this.state.number);
		}
	}
}

let WrappedButton = wrapper(AntDesignButton);
ReactDOM.render(<WrappedButton title="这是标题" />, document.getElementById('root'));

// 初始状态,先子后父
	wrapper componentWillMount
AntDesignButton componentWillMount
	wrapper render
AntDesignButton render
	wrapper componentDidMount
AntDesignButton componentDidMount

// 点击按钮
	wrapper render
AntDesignButton render

React.cloneElement 原理

js
/**
 * 根据一个老的元素,克隆出一个新的元素
 * @param {*} oldElement 老元素
 * @param {*} newProps 新属性
 * @param {*} children 新的儿子们
 */
function cloneElement(oldElement, newProps, children) {
  if (arguments.length > 3) {
    children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
  } else {
    children = wrapToVdom(children);
  }
  // 新属性覆盖老属性  新儿子覆盖老儿子
  let props = { ...oldElement.props, ...newProps, children };
  return { ...oldElement, props };
}

// 不管原来是什么样的元素,都转成对象的形式,方便后续的DOM-DIFF
export function wrapToVdom(element) {
  if (typeof element === "string" || typeof element === "number") {
    //返回的也是React元素,也是虚拟DOM
    return { type: REACT_TEXT, props: { content: element } }; //虚拟DOM.props.content就是此文件的内容
  } else {
    return element;
  }
}

3 Render Props

  • render-props
  • render prop 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
  • 具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑
  • render prop 是一个用于告知组件需要渲染什么内容的函数 prop,这也是逻辑复用的一种方式
js
// 移动鼠标获取坐标的案例

import React from "react";
import ReactDOM from "react-dom";

// 组件
class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };
  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {/* 
					<h1>移动鼠标</h1>
					<p>当前的鼠标位置是x={props.x}, y={props.y}</p>
				 */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

ReactDOM.render(
  // render属性返回一个函数,props传入到子组件,子组件调用props.render函数传入参数
  <MouseTracker
    render={(props) => (
      <div>
        <h1>移动鼠标</h1>
        <p>
          当前的鼠标位置是x={props.x}, y={props.y}
        </p>
      </div>
    )}
  />,
  document.getElementById("root")
);

Released under the MIT License.