React:组件通信、事件机制

1,954 阅读5分钟

一、组件、组件通信

1. class组件和函数组件的区别

1.1 特性

  • class 组件有生命周期;
  • class 组件内部有this,可以基于 this 执行多种操作;
  • 函数组件能够方便提取状态逻辑,容易测试。

1.2 函数组件捕获了渲染所使用的值

解释:React 中 props 是不可变的,但this 是一直可变的。因此 class 组件每次调用this.props都会去获取最新的this值,无法保证当前读取到的 props 就是最初捕获到的。而函数组件读取的 props 会在函数执行时就被捕获。也就是说函数组件中 延迟输出state 是调用时的state,而不是最新的state

class ProfilePage extends React.Component {
  showMessage = () => alert('Followed ' + this.props.user);
  handleClick = () => setTimeout(this.showMessage, 3000);
  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

React 框架中有一个经典的公式UI = f(data),也就是说 React 框架的本质就是输入数据、输出 UI,数据应该和 UI 绑定在一起。而 class 组件读取this.props的表现会打破 state 和 UI 之间的联系,不符合 React 的理念。

(补充忽略)React的经典公式UI = f(data),也就是说UI是当前应用状态的一个函数,那么事件处理程序也是渲染结果的一部分,它应该是一个拥有特定propsstate的特定渲染。然而对于类组件来说,调用一个回调函数读取一个 延迟的this.props 时,会打断这种关联,showMessage回调并没有与任何一个特定的渲染绑定在一起,所以它失去了正确的props。也就是说,从this中读取数据的这种行为,切断了这种联系。

  1. 怎么修复class组件的这个bug呢?可以提前读取this.props

  2. 如果想要读取最新的props和state,而不是捕获时的数据,可以采用class组件的this.props/this.state。

1.3 性能优化

  • class组件:主要靠shouldComponentUpdate避免不必要的渲染。
  • 函数组件:通过useMemouseCallback来进行缓存以提升性能。

2. 受控组件和非受控组件:针对表单元素

值是由 React 的 state 控制的表单元素称为受控组件。值不受 React 控制的称为非受控组件。

input等表单元素通常自己会维护一个state,然后根据用户输入进行更新。而 React 组件内部也会维护一个 state,并通过setState来更新。我们将两者进行结合,使 React 的 state 成为唯一的数据源。

  • 将 React 的state 赋值给表单元素的 value 属性。
  • onChange事件监听输入,输入发生变化时就调用setState更新state。
function App() {
    const [name, setName] = useState('');
    return (
        <input value={name} onChange={e => setName(e.target.value)}>
    );
}

3. ref 和 forwardRef

ref 是对 DOM 节点或 React 组件实例的引用。ref 属性可以让我们访问到它们,适用于管理焦点、滚动位置等场景。

import { useRef } from 'react';
function Form() {
    const inputRef = useRef(null);  // { current: null }
    function handleClick() {
        inputRef.current.focus();
    }
    return (
      <>
        <input ref={inputRef} /> { current:DOM节点的引用 }
        <button onClick={handleClick}>聚焦输入框</button>
      </>);
}

ref 和 state 的区别:

  • ref 发生变化并不会触发组件渲染,state 改变时会触发渲染。
  • ref 是可以任意修改的,而 state 只能通过 setState 修改。

React 不推荐使用 ref 将子组件实例暴露给父节点的方式。建议使用forwardRef转发的方式实现将子组件实例暴露给父组件

参数是一个render函数,接收父组件传递的 props 和 ref。返回React组件。
const MyInput = forwardRef(function MyInput(props, ref) {
    //...
})

4. 五种组件通信的方式

  1. 父组件 => 子组件:propsforwardRef

  2. 子组件 => 父组件:回调函数,父组件将一个函数作为 props 传递给子组件,子组件调用该函数就可以向父组件通信。

<Sub callback = { this.callback.bind(this) } />
props.callback(msg)
  1. 跨层级组件

    • 中间组件层层传递props
    • 使用context
  2. 兄弟组件:将公共的 state 向上移动到共同的父组件中。

  3. 不相关的复杂组件:事件总线、redux等状态管理库去进行全局状态管理。

二、react18 事件机制

不同浏览器对事件的兼容性不同,React 通过实现一个兼容全浏览器的事件系统来抹平浏览器之间的差异,方便对所有事件统一管理

  • 原生事件就是用document.addEventListener设置监听的事件,绑定在真实 dom 上。
  • 合成事件像onClick并不会绑定到真实 dom 上,而是绑定到root节点上。(v16绑定到document)
const rootNode = document.getElementById('root');
// v17 之后使用 rootNode.addEventListener() 来进行绑定
ReactDOM.render(<App />, rootNode);

合成事件是React模拟原生 DOM 事件所有能力的一个事件对象,能够兼容所有浏览器

原生事件和合成事件的区别

  • 事件命名方式不同:合成事件采用小驼峰式,原生事件是纯小写;
  • 事件处理函数写法不同:原生事件中事件处理函数为字符串,而合成事件中是传参一个函数作为事件处理函数。
  • 阻止默认事件的方式不同:原生事件可以return false阻止,React 需要调用e.preventDefault
<button onclick="handleClick()">原生事件</button>
<button onClick={handleClick}>合成事件</button>

事件机制原理(???)

  • 事件收集:React 对所有的事件进行了分类,并生成了一个React事件和它依赖的原生事件的集合。
  • 事件绑定:初始化时会将所有事件委托到根节点上,并且会区分冒泡阶段、捕获阶段。
  • 事件触发:触发时目标节点的 Fiber 节点会一直向上收集对应的事件,根据冒泡或者捕获阶段进行触发。

执行顺序

当点击 DOM 元素触发后,会先执行原生事件,再处理React合成事件,最后执行 root 节点上挂载的事件。

参考文章

  1. react 核心开发者 dan :类组件和函数组件的区别
  2. 类组件和函数组件的区别
  3. React合成事件一
  4. React合成事件二