跳到主要内容

React基础

事件

特点:

  1. event 是 SyntheticEvent,模拟出来 DOM 事件所有能力;
  2. event.nativeEvent 是原生事件对象;
  3. 所有的事件,都被挂在到 document 上(react 17+ 是绑定到 root 节点上,这样有利于多个 React 版本共存,例如微前端)

原理

  1. react 首先在 root 上挂载不同的事件,比如 click、mouseup、...

    • root.addEventListener('click', (e) => {...});
  2. 当页面上的 button 点击时,就会冒泡到 root 上,这时候就会触发 react 在 root 上的绑定事件,并可以根据 e.target 拿到 DOM,然后再根据真实 DOM 获取到虚拟 DOM 和虚拟 DOM 上我们自定义的事件,然后触发这个事件,并把 React 自己合成的事件对象传过来

为什么 React 能根据真实 DOM 获取到虚拟 DOM?原因是在构建 DOM 树的时候,在真实 DOM 上挂载了虚拟 DOM 的引用(__reactFiber)。

React 合成事件

setState

在 React <= v17 的版本中,setState 可能是异步的,可能会被合并。

const handleClick = () => {
setState({
count: count + 1,
});

console.log(count);
}

如果 count 初始值是 0,当点击的时候会加1,但是打印结果是 0,说明 setState 是异步的。

const handleClick = () => {
setTimeout(() => {
setState({
count: count + 1,
});

console.log(count);
}, 0);
}

const bodyClickHandler = () => {
document.bdoy.addEventListener('click', () => {
setState({
count: count + 1,
});

console.log(count);
});
}

把 setState 操作放在 setTimeout 中发现就会变成同步的。在自定义的 DOM 事件中,setState 也是同步的。

可能会被合并

// 传入值会被合并,执行结果只一次 +1
setState(count + 1);
setState(count + 1);
setState(count + 1);

// 传入函数不会被合并,执行结果是 +3(函数没法合并,只能一个个的去执行)
setState((prevState) => {
return prevState + 1
});
setState((prevState) => {
return prevState + 1
});
setState((prevState) => {
return prevState + 1
});

React18 setState 的变化

React <=17 的版本表现为:

  • React 组合事件:异步更新 + 合并 state;
  • DOM 事件,setTimeout:同步更新,不合并 state;

React18 的表现为:

Automatic Batching,自动批处理。

  • React 组合事件:异步更新 + 合并 state;
  • DOM 事件,setTimeout:异步更新 + 合并 state;

即两者保持一致了。如果不想合并更新,可以使用 flushSync

flushSync(() => {
setState((prevState) => {
return prevState + 1
});
});
console.log(count); // 可以获取到最新的值

flushSync 允许你强制 React 在提供的回调函数内同步刷新任何更新,这将确保 DOM 立即更新。

Portals 使用场景

portal 只改变 DOM 节点的所处位置。在其他方面,渲染至 portal 的 JSX 的行为表现与作为 React 组件的子节点一致。该子节点可以访问由父节点树提供的 context 对象、事件将从子节点依循 React 树冒泡到父节点。

  • 父组件设置了 BFC,overflow:hidden
  • 父组件 z-index 值太小;
  • fixed 需要放在 body 第一层级;
import { createPortal } from 'react-dom';

// ...

<div>
<p>这个子节点被放置在父节点 div 中。</p>
{createPortal(
<p>这个子节点被放置在 document body 中。</p>,
document.body
)}
</div>

context

场景:

  • 公共信息(语言、主题)如何传递给每个组件;
  • 用 props 太繁琐;
  • 用状态管理库有点小题大作;

弊端:

  • 性能问题: 当Context中的值发生变化时,所有依赖于该Context的组件都会重新渲染。这可能导致不必要的性能开销,特别是在大型组件树中。使用React.memo、useMemo等优化手段可以减轻一些性能问题,但需谨慎使用。

  • 难以调试: 使用Context的组件可能会在组件树的深层次嵌套中,这可能使得状态变化的追踪和调试变得更加复杂。DevTools 提供了一些工具来帮助调试上下文,但在复杂场景下,仍然可能会遇到挑战;

性能优化

React 默认情况下父组件有更新,子组件则无条件也更新。

  • shouldComponentUpdate(nextProps, nextState): boolean;返回 true 可以渲染,false 不重复渲染;
  • PureComponent,在 SCU 中实现了浅比较;
  • memo(FunctionComponent, areEqual),用于优化函数式组件,prevProps 和 nextProps 对比发现一致,应返回 true,表示不更新;
  • immutable.js/immer,彻底拥抱“不可变值”,基于共享数据(不是深拷贝)技术,速度好;

纯组件

纯组件(Pure Component)这概念衍生自纯函数。纯函数指的是返回结果只依赖于传入的参数,且对函数作用域外没有副作用的函数。这种函数在相同参数下,返回结果是不变的。纯函数的返回值能被安全地缓存起来,在下次调用时,跳过函数执行,直接读取缓存。

与纯函数类似,如果一个组件在 props 和 state 相同的情况下,每次 render 的结果都是相同的,那这个组件就是纯组件。

关于组件公共逻辑的抽离

  • 高阶组件(HOC),一个函数,接收一个组件,然后返回新的组件;
  • render props;<Component render={(props) => <h1>props.title</h1>} />
  • react hook;

redux

dispatch(action) ---> 调用 reducer 生成新的 newState ---> subscribe 触发通知;

中间件

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
import thunk from 'redux-thunk';

const logger = createLogger();

const store = createStore(
reducer,
// 应用中间件,会按顺序执行
applyMiddleware(thunk, logger)
);

redux 原理

中间件主要是增强 dispatch 函数的,比如 logger 中间件的实现:

let next = store.dispatch;

store.dispatch = function(action) {
console.log('dispatching', action);

next(action);

console.log('next state', store.getState());
}

mobx

mobx 原理