React 面试题: 不一定最全但绝对值得收藏!!(11 ~ 20)(万字总结)

引言

最近在准备面试, 所以整理了些常见的 React 相关的面试题!!!! 有需求的欢迎 👏🏻👏🏻 点赞、收藏, 同时欢迎 👏🏻👏🏻 大家在评论区留下面试中经常被问到的问题, 一起讨论讨论(我也可以悄摸记下准备准备)!! 最后文章写得仓促如果错误, 请多多见谅!!

补充: 慢慢的 React 相关题目整理越来越多, 并且目前 掘金 编辑器不知道为啥内容多了编辑、修改起来很卡, 所以就针对该系列内容做了拆分(每篇 10 题)!!!!

一、Redux

  • 页面上用户通过 dispatch 方法触发一个 Action: dispatch(Action)
  • Store 接收到 Action
  • Store 调用 Reducer 函数, 并将 Action 和当前状态作为参数传递给它
  • Reducer 函数根据 Action 类型执行相应的处理, 并返回新的状态
  • Store 更新状态, 并通知所有订阅状态的组件(视图)
  • 组件(视图)收到通知, 获取新状态, 重新渲染

1.1 createStore 实现原理

  1. 一个状态 state 用于存储状态
  2. 一个监听器列表, 当状态改变时会遍历该列表, 执行里面的所有方法
  3. subscribe: 注册监听器
  4. action: 有效载体, 必须包含 action.type, 以及额外数据
  5. dispatch: 执行 reducer(state, action)、遍历执行所有监听器(触发组件状态更新、从而引起页面重新渲染)
  6. reducer: 纯函数 (state, action) ==> 根据 action.type 处理计算 ==> 返回新状态

1.2 react-redux

  1. Provider: 创建 context, 添加全局 store
  2. connect: 高阶组件
  • 通过 context 获取 redux store
  • 添加监听器, 当通过 dispatch 更新状态时执行该监听器, 监听器将执行第一参数(回调函数 state => ({})) 将返回值作为高阶组件的 state
  • 将第二参数使用 dispathc 进行包裹返回新函数: (... arg) => dispatch(fun(... arg))
  • 最后将 state 和封装后的方法挂载到组件上

1.3 中间件

理解: 中间件其实就是要对 reduxstore.dispatch 方法做一些改造, 来定制一些功能

Redux-thunk: 实现原理

  1. 本来 dispatch 参数只能是 action 对象, redux-thunk 中间件对 dispatch 进行了封装, 允许 action 是一个函数
  2. dispatch 中如果发现 action 是函数则执行 action(dispatch, getState);(延迟 dispatch), 否则执行 dispatch(action)
js 复制代码
// 下面方式使用了 mapDispatchToProps
// 正常情况下, openModalAction 函数应该返回一个 action 对象
// redux-thunk 中间件对 dispatch 进行了封装, 所以允许 action 是一个函数
export const openModalAction = ({ code, data, ...rest }) => {
  return dispatch => {
    dispatch(openModal({ code, data, ...rest }));
  };
};

1.4 redux 优缺点

优点:

  1. 单一数据源: 所有状态都存在一个对象中, 使得开发、调试都会变得比较容易
  2. State 是只读的: 如果要修改状态只能通过触发 action 来修改, action 是一个普通对象, 可以很方便被日志打印、序列化、储存...... 因此状态的修改过程就会变得有迹可寻, 比较方便得跟踪数据的变化
  3. redux 使用纯函数(reducer)来修改状态, 同一个 action 返回的 state 相同, 这样的话让状态的修改过程变得可控, 测试起来也方便

缺点: 啰嗦, 存在 ActionReducer, 如果要添加一个新的状态需要写一堆模版代码, 但是现在市面上已经有很多成熟的方案(工具)可以帮我们简化这一步, 比如 Redux Toolkit

js 复制代码
export default createSlice({
  initialState,
  name: 'user',
  reducers: {
    updateUser: (state, { payload }) => ({ ...state, ...payload }),
  },
});

1.5 和 mobx 的区别

  1. 单一数据、数据分散
  2. 响应式编程、函数式编程
  3. 状态修改和页面响应被抽象化封装到内部, 不易监测、调试
  4. mobx 更适合业务不是很复杂、快速开发的项目

1.6 redux-thunk 和 redux-sage 区别

  1. redux-thunk 允许 action 是一个函数, 当 aciton 是一个函数时会进行执行并传入 dispatch, 对于 redux-thunk 的整个流程来说, 它是等异步任务执行完成之后, 我们再去调用 dispatch , 然后去 store 去调用 reduces

  2. redux-saga 则是 reduxaction 基础上, 重新开辟了一个 async action 的分支, 单独处理异步任务, 当我们 dispatchaction 类型不在 reducer 中时, redux-saga 的监听函数 takeEvery 就会监听到, 等异步任务有结果就执行 put 方法, 相当于 dispatch 再一次触发 dispatch

  3. saga 自己基本上完全弄了一套 asyc 的事件监听机制, 代码量大大增加, 从我自己的使用体验来看 redux-thunk 更简单, 和 redux 本身联系地更紧密, 尤其是整个生态都向函数式编程靠拢的今天, redux-thunk 的高阶函数看上去更加契合这个闭环

二、组件之间传参方法

2.1 父子间通信

这种父子通信方式也就是典型的单向数据流, 父组件通过 props 传递数据, 子组件不能直接修改 props, 而是必须通过调用父组件函数的方式告知父组件修改数据

  1. 父组件通过 props 传递数据给子组件

  2. 子组件通过调用父组件传来的 函数 传递数据给父组件(自定义事件)

  3. 非常规方法: 父组件通过 ref 获取子组件的实例对象

2.2 兄弟间通信

状态提升: 在父组件中创建共同的状态、事件函数, 其中一个兄弟组件调用父组件传递过来的事件函数修改父组件中的状态, 然后父组件将状态传递给另一个兄弟组件

2.3 任意组件之间进行通信

  1. 使用 Context
js 复制代码
import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

function App({ children }) {
  const theme = useContext(ThemeContext);
  return (<div>{theme}</div>)
}

function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <App />
    </ThemeContext.Provider>
  )
}
  1. 使用 Redux 等状态管理工具

三、受控组件和非受控组件

3.1 受控组件

组件内部 state 或值完全受 prop 控制的组件

就像 antdInput 组件, 可以通过 props 传一个 value 使得 Input 变为受控组件, Input 组件内部状态(值)就由 props 控制

js 复制代码
import { Input } from 'antd';
<Input value="写死或者设置为状态值"/>

补充: getDerivedStateFromProps 的作用

  1. state 只受到 props 的影响
  2. 只有当 stateprop 不同时, 才去修改 state

3.2 非受控组件

组件内部 state或值不受 props 控制的组件, 由组件内部自己管理

就像 antdInput 组件, 如果不给组件传 value 值, 那么组件就是非受控组件, Input 组件内由自己管理 value, 这时如果要想拿到表单的 value 则只能通过 ref 等手段, 手动获取

注意的是: Input 组件内部, 使用了 input 标签将 value 和状态进行绑定, 那么对于 input 标签来说它是受控的, 所以受控组件只是相对

js 复制代码
import { Input } from 'antd';
<Input/>

3.3 什么时候使用受控组件、什么时候使用非受控

当组件内部值或状态和外部存在交互逻辑时, 则需要将其作为受控组件进行使用

  1. 当组件状态(值)只由自身交换控制, 不受外部影响时, 可使用非受控组件: 比如 Antd Input 组件, 如果输入框的内容只随着用户输入时改变, 那么就可以使用非受控组件

  2. 当组件状态(值)除了受自身交换控制、还受到外部影响时, 可使用受控组件: 比如 Antd Input 组件, 需要和其他控件产生联动对组件的值进行相应的格式化

  3. 当组件状态(值)和外部需要交换时, 可使用受控组件: 比如 Antd 单选框, 当选中时需要隐藏页面上内容时, 一般就会将单选框最为受控组件进行使用

3.4 参考

四、Ref 相关

4.1 作用

  1. 在函数组件中, 当我们希望组件能够 记住 或者说 存储 某些信息, 但呢又不希望该信息触发新的渲染时, 就可以使用 ref 来存储
  2. 用于访问真实 DOM 元素
  3. 当父组件需要获取子组件实例对象时, 也可通过 ref 来实现

4.2 获取真实 DOM: 三种创建方式

  1. 推荐使用 API: React.createRef()useRef
js 复制代码
// 类组件, 使用 createRef
this.ref = React.createRef();
<div ref={this.ref}></div>


// 函数组件, 使用 useRef
const ref = React.useRef();
<div ref={ref}></div>
  1. ref 回调函数方式
js 复制代码
// 类组件
bindRef = ele => {
  this.bodyRef = ele;
};

<div ref={this.bindRef}></div>


// 函数组件
const bindRef = useCallback((ele) => {
  
}, []);
<div ref={bindRef}></div>
  1. 字符串(仅限类组件中使用)
js 复制代码
// 会自动在 this 上绑定 bodyRef, 等于当前元素
<div ref="bodyRef"></div>

4.3 获取子组件实例

  1. 子组件为类组件, 直接绑定 ref, 就能够拿到整个子组件的实例对象
js 复制代码
class A extends Component {}

const App = () => {
  const ref = useRef()
  return (<A ref={ref}/>)
}
  1. 函数组件: forwardRef + useImperativeHandle
js 复制代码
import { forwardRef, useImperativeHandle } from 'react';

const A = (props, ref) => {
  useImperativeHandle(ref, () => {
    // 返回要绑定的实例对象
    return {};
  }, []);
}

const App = forwardRef(A);

4.4 转发 ref

  1. 可使用 React.forwardRef 进行转发
js 复制代码
// React.forwardRef 返回一个组件
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));
  1. 使用不同的属性名称将 ref 进行转发(常见于类组件, 毕竟 forwardRef 不能用于类组件)
js 复制代码
class A extends Component {
  render () {
    return (
      <div ref={this.props.innerRef}>
        1
      </div>
    );
  }
}

const bodyRef = useRef()
<A  innerRef={bodyRef} />

五、Fragment

React 中如果需要渲染多个元素, 需要使用元素进行包裹, 否则将会报错

  • 报错原因, 主要原因还是在 JSX 编译这块
js 复制代码
// 编译前
const dom = (
  <ChildA />
  <ChildB />
  <ChildC />
);

// 编译后, 这样很明显是有问题的
const dom = (
  React.createElement(......) 
  React.createElement(......) 
  React.createElement(......)
);
  • 上面错误代码解决办法就是, 使用 div 等标签进行包裹, 这样就能够通过编译
js 复制代码
// 编译前
const dom = (
  <div>
    <ChildA />
    <ChildB />
    <ChildC />
  </div>
);

function MyComponent() {
  return React.createElement(
    'div', 
    null,
    React.createElement(......),
    React.createElement(......),
    React.createElement(......),
  );
}
  • 上面处理会有个问题, 就是会添加了额外节点, Fragment 出现就为了解决上面的问题, 通过 Fragment 可以将子列表分组, 最终在渲染为真实 DOM 节点时会将其忽略(不会进行渲染)
js 复制代码
// 编译前
const dom = (
  <React.Fragment>
    <ChildA />
    <ChildB />
    <ChildC />
  </React.Fragment>
);

// 编译后
function MyComponent() {
  return React.createElement(
    React.Fragment, 
    null,
    React.createElement(......),
    React.createElement(......),
    React.createElement(......),
  );
}
  • Fragment 简写形式 <></>
js 复制代码
const dom = (
  <>
    <ChildA />
    <ChildB />
    <ChildC />
  </>
);
  • Fragment 对应 ReactElement 元素类型(type) 为 Symbol('react.fragment')
js 复制代码
console.log(
  <>
    <div> 1</div>
    <div> 2</div>
  </>,
);

{
  type: Symbol('react.fragment'),
  ...
}

补充: 有一点是需要注意的, <> 并不接受任何属性, 包括 keyref 属性

六、React 元素中 $$typeof 的作用

用于标识 React 元素, 该属性值为 Symbol, 主要为了防止 XOO 攻击
补充: XSS 攻击通常指的是通过利用网页开发时留下的漏洞, 通过巧妙的方法注入恶意指令代码到网页, 使用户加载并执行攻击者恶意制造的网页程序。

  1. 已知 JSX 语法将被编译为 React.createElement 后返回一个对象(React 元素)
js 复制代码
{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'), // 标识 React 元素
}
  1. 由于服务器可以存储任意的 JSON 数据, 如果在没有 $$typeof 情况下, 就很容易被伪造(手动创建 React 元素, 在页面进行注入)
js 复制代码
// 假设后端返回了这样一串数据(React 元素)
const message = {  
  type: 'div',  
  props: {    
    dangerouslySetInnerHTML: {    
      __html: '/* 把你想的搁着 */'    
    },  
  },
};

// 前端这么现实数据
<p>{message}</p>
  1. 由于 JSON 不支持 Symbol 类型数据, 所以只要在 React 元素中添加 Symbol 类型数据 $$typeof, React 在处理元素时只需通过 $$typeof 就能够识别出 非法元素(伪造元素)

  2. 如果浏览器不支持 Symbols 怎么办?

  • 那这种保护方案就无效了 但是 React 仍然会加上 $$typeof 字段以保证一致性
  • 但这样只会设置一个数字 ------ 0xeac7
  • 而之所以设置 0xeac7, 只是因为 0xeac7 看起来有点像 React
  1. 参考: 为什么React元素有一个$$typeof属性?

七、Hooks

React 16.8 的新增特性, 它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

17.1 和类组件对比有什么优点

优点:

  1. 更简洁: 相比于传统的 class 组件, 使用 Hooks 可以将组件的逻辑拆分成更小, 这使得组件代码更加简洁、易读、好维护

  2. 易上手: 使用 Hooks 你可以在函数组件中使用状态和其他 React 特性, 无需编写 class 从而避免了繁琐的 class 组件的声明和继承、同时也无需考虑 this 指向等问题

  3. 逻辑复用: 自定义 Hooks 允许将组件之间的状态逻辑进行抽离, 作为一个独立的可组合和可共享单元, 从而减少了重复代码的出现

  4. 更好的可测试性: 通过 Hooks 可以将组件渲染、和业务逻辑分离进行分离, 使得组件的测试变得更加容易。可以针对每个 Hook 编写单独的测试,确保其正确性, 同时保持组件测试的简洁性。

  5. 灵活性: Hooks 的设计允许你在组件内部使用多个不同的 Hook, 这使得你可以在一个函数组件中使用各种各样的特性, 而不必担心组件层次的嵌套和复杂性

  6. 有助于代码拆分: 使用 Hooks 可以更容易地拆分组件, 将组件的不同部分拆分成更小的逻辑单元,有助于更好地组织和管理代码。

  7. 类组件在业务不断扩展的情况下, 容易变得臃肿难以维护, 往往相关的业务被拆分到多个生命周期里, 或者一个生命周期中存在多个不相关的业务, 而 Hook 的出现, 可以将业务拆分为更小的函数, 对业务逻辑进行更为细腻的控制, 使得组件更容易理解、维护

  8. 可以把状态分开管理, 逻辑上更清晰了, 更方便维护了

  9. 补充: 类组件中如果需要复用状态逻辑, 只能通过高阶组件来实现, 没有 hooks 简洁, 而且还多了一层组件嵌套

缺点:

  1. 陡峭的学习曲线: 对于那些熟悉传统 class 组件的开发者来说, 学习 Hooks 可能需要一些时间。Hooks 改变了组件的编写方式, 并且需要理解如何正确地使用 useStateuseEffectuseContext 等钩子函数

  2. 使用规则: Hooks 有一些使用规则, 例如在条件语句中不可使用, 或者只能在函数组件的最顶层使用。违反这些规则可能导致 bug 和意想不到的行为。

  3. 性能问题: 尽管 Hooks 通常可以优化组件逻辑, 但不正确地使用它们可能导致性能问题。比如, 在 useEffect 中没有正确处理依赖项数组可能会导致不必要的重复执行。

怎么避免 hooks 的常见问题:

  1. 不要在 useEffect 里面写太多的依赖项, 划分这些依赖项成多个单一功能的 useEffect 其实这点是遵循了软件设计的 单一职责模式
  2. 拆分组件, 细化组件的粒度, 复杂业务场景中使用 hooks 应尽可能地细分组件, 使得组件的功能尽可能单一, 这样的 hooks 组件更好维护
  3. 能通过事件触发数据更新, 就尽量通过事件方式去实现, 尽量避免在 useEffect 中依赖 A 状态然后去修改 B 状态

7.2 常用的几个 Hooks

  • useState: 用于定义组件状态, 需要注意的是该方法在更新状态时会进行浅比较, 如果待更新状态值和当前状态值一致, 则不会进行更新, 不会引起组件的重新渲染
js 复制代码
const [state, setState] = useState(0);
setState(0); // 不会引起组件重新渲染
  • useEffect: 让函数型组件拥有处理 副作⽤ 的能⼒, 每次依赖项改变, 都会触发回调函数的执行, 通过它可模拟类似 类组件 中的部分⽣命周期
  • useLayoutEffect: 与 useEffect 相同, 但它会在所有的 DOM 变更之后同步调用
  • useInsertionEffect: 在任何 DOM 突变之前触发, 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题
  • useMemo: 可以监测某个值的变化, 根据变化值计算新值, useMemo 会缓存计算结果, 如果监测值没有发⽣变化, 即使组件重新渲染, 也不会重新计算
  • useRef: 获取 DOM 元素对象、记录非状态数据、获取子组件实例对象
  • useCallback: 可让您在重新渲染之间缓存函数定义, 使组件重新渲染时得到相同的函数实例
  • useImperativeHandle 用于绑定 ref
  • useReducer: 使用简易版 Redux
  • useContext: 使用 context
  • useDebugValue: 可以在 React DevTools 中向自定义 Hook 添加一个标签, 方便追踪数据
  • useId: 生成唯一 ID, 是 hook 所以只能在组件的顶层或您自己的 Hook 中调用它, 您不能在循环或条件内调用它、不应该用于生成列表中的键
  • useDeferredValue: 用于推迟更新部分 UI
  • useSyncExternalStore: 使用外部 store
  • useTransition: 允许在不阻塞 UI 的情况下更新状态

7.3 useEffect、useLayoutEffect、useInsertionEffect 之间的区别

  1. useInsertionEffect: 应该是 DOM 变更之前执行

  2. useLayoutEffect: DOM 已经按照 VDOM 更新了, 此时 DOM 已经在内存中更新了, 但是还没有更新到屏幕上

  3. useEffect: 则是浏览器完成渲染之后执行

  4. 所以三者执行顺序: useInsertionEffect(DOM 变更前)useLayoutEffect(DOM 变更后)useEffect

  5. useLayoutEffectuseEffect 基本相同, 但它会在所有的 DOM 变更之后 同步 调用, 一般可以使用它来读取 DOM 布局并同步触发重渲染, 为了避免阻塞视觉更新, 我们需要尽可能使用标准的 useEffect

  6. useEffectuseLayoutEffect 都可用于模拟 componentDidUpdate componentDidMount

  7. 当父子组件都用到 useEffect 时, 子组件中的会比父组件中的先触发

  8. 参考: 面试官:useLayoutEffect和useEffect的区别

7.4 React.memo

  1. 在类组件的时代时代, 为了性能优化我们经常会选择使用 PureComponent, 组件每次默认会对 props 进行一次 浅比较, 只有当 props 发生变更, 才会触发 render
js 复制代码
class MyComponent extends PureComponent {
  render () {}
}
  1. 当然在类组件中, 我们除了使用 PureComponent 还可以在 shouldComponentUpdate 生命周期中, 对 props 进行比较, 进行更深层次的控制;

补充:

  • shouldComponentUpdate 当收到新的 propsstate 时, 在渲染之前都会被调用
  • 这里的比较可以是浅比较、也可以是深比较, 主要看代码实现
  • shouldComponentUpdate 返回为 true 的时候, 当前组件进行 render, 如果返回的是 false 则不进行 render
js 复制代码
class MyComponent extends Component {
  shouldComponentUpdate(){
    if (需要 Render) {
      // 会进行渲染
      return true
    }

    // 不会进行渲染
    return false
  }
  render () {}
}
  • 在函数组件中, 我们是无法使用上面两种方式来限制 render 的, 但是 React 贴心的提供了 React.memo 这个 HOC(高阶组件), 它的作用和 PureComponent 很相似, 只是它是专门为函数组件设计的

React.memo 使用说明

  • 默认情况下会对组件 props 进行 浅比较, 只有 props 变更才会触发 render
  • 允许传入第二参数, 该参数是个函数, 该函数接收 2 个参数, 两个参数分别是新旧 props,
  • 注意: 与 shouldComponentUpdate 不同的是, arePropsEqual 返回 true 时, 不会触发 render, 如果返回 false 则会, 和 shouldComponentUpdate 刚好与其相反
js 复制代码
// 组件
function MyComponent(props) {}

// 比较方法
function areEqual(prevProps, nextProps) {
  if (需要 Render) {
    // 会进行渲染
    return false
  }

  // 不会进行渲染
  return true
}

export default React.memo(MyComponent, areEqual);

作用: 性能优化, 如果本组件中的数据没有发⽣变化, 阻⽌组件更新, 类似类组件中的 PureComponentshouldComponentUpdate

7.5 使用时需要注意什么

  1. 遵守 Hooks 使用规则: Hooks 只能在函数组件的顶层使用, 或者在自定义 hooks 中使用, 不能在循环、条件或嵌套函数中使用 hooks

  2. 依赖数组: 在使用 useEffectuseCallbackhooks 时, 务必提供依赖数组作为第二个参数。忽略或者错误的依赖数组可能导致意外行为, 比如过度重新渲染或内存泄漏

  3. 避免无限循环: 在使用 useEffect 时要小心无限循环, 确保依赖数组中有正确的依赖项, 并且 effect 的逻辑不会触发不必要的重新渲染

  4. 状态不可变性: 避免直接修改状态对象, 也不要试图通过 pushpopsplice 等直接更改数组

  5. 单一职责 组件useEffects

  6. 尽量避免通过 useEffect 来处理 actions: useEffect 监听某个状态 A, 内部又去修改 A, 这样就容易造成死循环

  7. 如果某个数据的变更不需要触发 render, 或者该数据没有在 jsx 中被使用, 那么就不要使用 useState 改用 useRef 进行记录

7.6 为什么 hooks 不能写在循环或者条件判断语句里?

Hooks 只能在函数组件的顶层使用, 或者在自定义 hooks 中使用, 不能在循环、条件或嵌套函数中使用 hooks

js 复制代码
export default () => {
  const [name, setName] = useState('1');

  if (!name) {
    return null;
  }

  const [age, setAge] = useState();

  const handler = useCallback(() => {
    setName(null);
  }, []);

  return (
    <div onClick={handler}>
      点击我
    </div>
  );
};

原因: React 需要利用 调用顺序 来正确更新相应的状态, 以及调用相应的钩子函数, 一旦在循环或条件分支语句中调用 Hooks, 就容易导致调用顺序的不一致性, 从而产生难以预料到的后果

这里拿 useState 来举例:

  1. hooks 为了在函数组件中引入状态, 维护了一个有序表
  2. 首次执行时会将每个 useState 的初始值, 依次 存到有序表里
  3. 每次更新也都会按照 索引 修改指定位置的值
  4. 每次 render 会将对应 索引 的值作为状态返回
  5. 那么试想下, 如果我们将 useState 写在判断条件下, 可能会导致 useState 不执行, 那么这个有序列表就会出现混乱
js 复制代码
export default () => {
  const [name, setName] = useState('1');

  if (!name) {
    return null;
  }

  const [age, setAge] = useState();

  const handler = useCallback(() => {
    setName(null);
  }, []);

  return (
    <div onClick={handler}>
      点击我会报错
    </div>
  );
};

总结: hooks 是将 state 原子化, 使用类似索引的方式来记录状态值, 当连续创建状态 A B, 就会有索引 0 对应着 A, 索引 1 对应这 B, 如果使用在循环、条件、嵌套函数内使用 Hook 就很容易造成索引错乱

7.7 如何打破了 React Hook 必须按顺序、不能在条件语句中调用的枷锁?

总结一下: 之前是通过顺序来查找, 现在通过唯一 key 来查找

实现则需要去修改源码, 参考: 我打破了 React Hook 必须按顺序、不能在条件语句中调用的枷锁

7.8 为什么 useState 返回的是一个数组?

  1. useState 要返回两个值, 一个是当前状态, 另一个则是修改状态的方法, 那么这里它就有两种方式可以返回这两个值: 数组、对象

  2. 那么问题就回到, 数组和对象解构赋值的区别了:

  • 数组的元素是按次序排列的, 数组解构时变量的取值由数组元素的位置决定, 变量名可以任意命名, 如下:
js 复制代码
const [name, setName] = useState()
const [age, setAge] = useState()
  • 对象的属性没有次序, 解构时变量名必须与属性同名才能取到正确的值, 假设 useState 返回的是一个对象, 那么就得这么使用:
js 复制代码
const { state: name, setState: setName } = useState()
const { state: age, setState: setAge} = useState()
  • 上面例子可以得出结果, useState 返回数组相比于对象会更灵活、解构起来也会更简洁、方便
  1. 当然最终 useState 返回的是啥, 还是由具体实现决定, 如果 useState 返回的是对象, 也不是不行

7.9 简单实现 hooks

js 复制代码
// 一、实现useState
const { render } = require("react-dom");
let memoriedStates = [];
let lastIndex = 0;
function useState(initialState) {
  memoriedStates[lastIndex] = memoriedStates[lastIndex] || initialState;
  function setState(newState) {
    memoriedStates[lastIndex] = newState;
    // 状态更新完毕,调用render函数。重新更新视图
    render();
  }
  // 返回最新状态和更新函数,注意 index 要前进
  return [memoriedStates[lastIndex++], setState];
}

// 二、实现useEffect
let lastDendencies; // 存放依赖项的数组
function useEffect(callback, dependencies) {
  if (lastDendencies) {
    // 判断传入的依赖项是不是都没有变化,只要有以一项改变,就需要执行callback
    const isChange = dependencies && dependencies.some((dep, index) => dep !== lastDendencies[index]);
    if (isChange) {
      // 一开始没有值,需要更新一次(相当于componentDidMount)
      typeof callback === 'function' && callback();
      // 更新依赖项
      lastDendencies = dependencies;
    }
  } else {
    // 一开始没有值,需要更新一次(相当于componentDidMount)
    typeof callback === 'function' && callback();
    // 更新依赖项
    lastDendencies = dependencies;
  }
}

// 三、实现useCallback
let lastCallback; // 最新的回调函数
let lastCallbackDependencies = []; // 回调函数的依赖项
function useCallback(callback, dependencies = []) {
  if (lastCallback) {
    const isChange = dependencies && dependencies.some((dep, index) = dep !== lastCallbackDependencies[index]);
    if (isChange) {
      // 只要有一个依赖项改变了,就更新回调(重新创建)
      lastCallback = callback;
      lastCallbackDependencies = dependencies;
    }
  } else {
    lastCallback = callback;
    lastCallbackDependencies = dependencies;
  }
  // 最后需要返回最新的函数
  return lastCallback;
}

// 四、实现useRef
let lastRef;
function useRef(initialValue = null){
  
  lastRef = lastRef != undefined ? lastRef : initialValue;
  // 本质上就是返回一个对象,对象种有一个current属性,值为初始化传入的值,如果没有传入初始值,则默认为null
  return {
    current: lastRef
  }
}

// 五、实现useContext
function useContext(context){
  // 很简单,就是返回context的_currentValue值
  return context._currentValue;
}

// 六、实现useReducer
let lastState;
function useReducer(reducer, initialState){
  lastState = lastState !== undefined ? lastState : initialState;
  // dispatch一个action,内部就是自动调用reducer来计算新的值返回
  function dispatch(action){
    lastState = reducer(lastState, action);
    // 更新完毕后,需要重新渲染视图
    render();
  }
  // 最后返回一个的状态值和派发action的方法
  return [lastState, dispatch];
}

7.10 useCallback 和 useMemo 的区别?

  1. 可以 useMemo 来实现 useCallback 吗?

可以, useMemo 只要返回一个函数即可

拓展知识: useCallback 是「useMemo 的返回值为函数」时的特殊情况, 是 React 提供的便捷方式。在 React Server Hooks 代码 中, useCallback 就是基于 useMemo 实现的, 尽管 React Client Hooks 没有使用同一份代码, 但 useCallback 的代码逻辑和 useMemo 的代码逻辑仍是一样的

八、性能优化

  1. 跳过不必要的组件更新
  • PureComponentReact.memoshouldComponentUpdate
  • useMemouseCallback 来生成稳定值
  • 状态下放, 缩小状态影响范围: 如果一个状态只在某部分子树中使用, 那么可以将这部分子树提取为组件, 并将该状态移动到该组件内部
  • 列表项使用 key 属性:
  • useMemo 返回虚拟 DOM: 利用 useMemo 可以缓存计算结果的特点, 如果 useMemo 返回的是组件的虚拟 DOM, 则将在 useMemo 依赖不变时, 跳过组件的 Render 阶段
  • 对于 props 可以跳过 回调函数改变 触发的 Render: 对于一些回调函数(事件)的变更, 其实并不需要触发 render, 实现方式参考: 跳过回调函数改变触发的 Render 过程
  • 自定义 Hooks 按需更新: 假设我们自定义的 Hook 暴露的状态, 有多个属性值, 但是调用则只使用了若干个, 那么其他属性的变更, 不应该引起 render, 实现方案参考: demo
  • 动画库直接修改 DOM 属性: 当一个动画启动后, 每次动画属性改变不会引起组件重新 Render 而是直接修改了 dom 上相关属性值, 比如拖拽动作可以通过操作原生 DOM 而不是通过状态来记录位置, 从而触发组件的 render
  1. 组件按需挂载:
  • 懒加载: 通过 Webpack 的动态导入和 React.lazy 方法来实现
  • 懒渲染: 懒渲染指当组件进入或即将进入可视区域时才渲染组件, 常见的组件 Modal/Drawer
  • 虚拟列表
  1. 批量更新:
  • 类组件, setState 自带批量更新操作
  • 函数组件, 尽量将相关的状态进行合并, 然后进行批量更新
  1. 按优先级更新, 及时响应用户: 举个例子当页面弹出一个 Modal, 当用户点击 确定 按钮后, 代码将执行两个操作, 1、关闭 Modal; 2、 处理 Modal 传回的数据并展示给用户; 同时假设第二个操作需要执行 500ms 时, 那么用户会明显感觉到从点击按钮到 Modal 被关闭之间的延迟, 如下代码 如果 setNumbers 这一步处理时间耗时, 那么就会出现明显的卡顿
js 复制代码
const slowHandle = () => {
  setShowInput(false)
  // 计算耗时 500s
  setNumbers([...numbers, +inputValue].sort((a, b) => a - b))
}

解决办法: 通过 setTimeout 将耗时任务放到下一个宏任务中去执行

js 复制代码
const fastHandle = () => {
  // 优先响应用户行为
  setShowInput(false)
  // 将耗时任务移动到下一个宏任务执行
  setTimeout(() => {
    setNumbers([...numbers, +inputValue].sort((a, b) => a - b))
  })
}
  1. 缓存优化:
  • React 组件中常用 useMemo 缓存上次计算的结果, 一般用在计算非常耗时的场景中, 如: 遍历大列表做统计信息, 当然 useMemo 只能缓存上一次结果, 如果需要缓存所以结果则需要自定义一个缓存表, 进行处理
  • 当然对于接口数据缓存来说, 如果实时性比较高的, 那么我们可以先取缓存时间, 然后通过 requestIdleCallback 在系统闲暇时重新发起请求获取数据, 这样在请求比较耗时情况下, 可以优化用户的体验
  1. 通过 debouncethrottle 优化频繁触发的回调函数

总结:

  • 如果是因为存在不必要更新的组件进入了 Render 过程, 则选择跳过不必要的组件更新进行优化
  • 如果是因为页面挂载了太多不可见的组件, 则选择 懒加载懒渲染虚拟列表 进行优化。
  • 如果是因为多次设置状态, 引起了多次状态更新, 则选择批量更新或 debounce(防抖)throttle(节流) 优化频繁触发的回调进行优化
  • 如果组件 Render 逻辑的确非常耗时, 我们需要先定位到耗时代码(这里我们可以选择使用 React 官方提供的性能分析插件、或者使用 chrome 自带的性能分析插件), 并判断能否通过缓存优化它, 如果可以则选择缓存优化, 否则选择按优先级更新, 及时响应用户, 将组件逻辑进行拆解, 以便更快响应用户

补充:

  1. 在组件中为 window 注册的全局事件、定时器等, 在组件卸载前要清理掉. 防止组件卸载后继续执行影响应用性能
  2. 使用 Fragment 避免额外标记
  3. 不要使用内联函数定义
  4. 避免使用内联样式属性
  5. 为组件创建错误边界

8.1 参考

九、React 18 更新内容有哪些?

主基调: 并发性是 React 18 的主要优势之一

9.1 彻底放弃 IE

  1. 17 还修复了 IE 兼容问题
  2. 18 就彻底放弃了对 IE 的支持

9.2 自动批处理

  1. 我们都知道在 React 18 之前:
  • 在合成事件、生命周期中如果多次修改 state, 会进行批处理, 然后只会触发一次 render
  • 在定时器、promise.then、原生事件处理函数中不会进行批处理
  • 这里之所以会有两种不同情况, 主要原因是早期对于 批处理 是通过一个状态作为批处理依据, 具体可查阅上文 7.2 React 的更新机制: 异步 OR 同步 部分内
js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount((c) => c + 1); 
    setFlag((f) => !f); 
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={flag ? { color: "blue" } : { color: "black" }}>{count}</h1>
    </div>
  );
}
  1. React 18 之后所有的更新都将自动批处理:
  • 主要原因是不再通过 状态 来作为批处理依据, 而是基于 fiber 增加调度的流程来实现的, 以更新的「优先级」为依据来进行批处理
  • 他通过对于 Root 上是否存在当前任务的调度信息, 以及任务的更新优先级是否发生变化, 以此来决定是否要开启一个新的 updateScheduled
  • 而只要复用同一个 Scheduled 一个 Scheduled 只会出发一次 render 也就完成了自动批处理的实现
  • 参考: React18精读一: Automatic Batching 自动批处理
  1. 如何退出批处理: flushSync 批处理是一个破坏性改动,, 如果你想退出批量更新, 可以使用 react-dom 中提供的 flushSync 函数
js 复制代码
import React, { useState } from 'react';
import { flushSync } from 'react-dom';

const App: React.FC = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <div
      onClick={() => {
        flushSync(() => {
          setCount1(count => count + 1);
        });
        // 第一次更新
        flushSync(() => {
          setCount2(count => count + 1);
        });
        // 第二次更新
      }}
    >
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </div>
  );
};

export default App;

9.3 Render API

修改了将组件挂载到 root 节点的一个 api

  1. 旧版本
js 复制代码
import React from 'react';
import ReactDOM from 'react-dom';
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root);
  1. 18 版本: 支持并发模式渲染
js 复制代码
// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';

const root = document.getElementById('root')!;

ReactDOM.createRoot(root).render(<App />);

9.4 删除: 卸载组件时的更新状态的警告

参考: react-18/discussions/82

  1. 背景: 18 之前我们如果在组件卸载后, 尝试修改状态就会在控制台抛出异常
js 复制代码
useEffect(() => {
  function handleChange() {
    setState(store.getState())
  }
  store.subscribe(handleChange)

  return () => store.unsubscribe(handleChange)
  // 这里如果没有解除订阅, 那么控制台将会抛出如下错误
  // Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
}, [])
  1. 目的: 如上代码如果忘记调用 unsubscribe 解除订阅, 就会出现内存泄露!! 所以为了避免出现这种问题, 所以就设置了上面的警告信息!!

  2. 为什么要去除: 主要原因还是警告具有误导性, 如下代码是一个比较常见的场景, 在执行 post('/someapi') 期间如果组件卸载, 后面调用 setPending, 在 18 之前这里将会抛出错误, 但实际上这里并没有太大的毛病, 也并没有存在内存泄露问题!!!

js 复制代码
async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}
  1. 18 之前人们为了抑制这个错误, 经常写如下代码:
js 复制代码
let isMountedRef = useRef(false)
useEffect(() => {
  isMountedRef.current = true
  return () => {
    isMountedRef.current = false
  }
}, [])

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  if (!isMountedRef.current) {
    setPending(false)
  }
}
  1. 看起来上面解决方法实际比原来的问题更加糟糕, 所以最后还是删了吧.... 后面看看有没其他手段来规避内存泄露

9.5 关于 React 组件的返回值

参考: react-18/discussions/75

  1. React 17 中, 如果需要返回一个空组件, 只允许返回 null, 如果返回了 undefined 控制台则会在运行时抛出一个错误

  2. React 18 中, 既能返回 null, 也能返回 undefined (但是 React 18dts 文件还是会检查, 只允许返回 null, 这里我们可以忽略这个类型错误

  3. 之前为什么要这么设计: 在编码过程中忘记 return, 是比较容易犯的一个错误, 为了帮助用户发现这个问题, 所以就有了这个警告

  4. 那现在为什么又允许了:

  • Suspense 中允许为 fallbackundefined, 所以为了保持一致性, 顾允许返回 undefined
  • 考虑到现在类型系统和 Eslint 都已经非常成熟、健壮, 通过它们就可以很好避免这类低级错误了

9.6 严格模式下第二次渲染期间抑制日志

  1. 背景:
  • 为了防止组件内有什么意外的副作用, 而引起 BUG, 所以严格模式下 React 在开发模式中会刻意执行两次渲染, 尽可能把问题提前暴露出来, 来提前预防
  • React 为了让日志更容易阅读, 通过修改 console 中的方法, 取消了其中一次渲染的控制台日志
  1. 问题: 开发人员在调试过程中会存在很多困惑

  2. 展望未来: React 将不再默认在第二次渲染期间抑制日志, 如果安装了 React DevTools > 4.18.0, 第二次渲染期间的日志现在将以柔和的颜色显示在控制台中

9.7 Suspense

参考: react-18/discussions/72

  1. 更新前: 如果 Suspense 组件没有提供 fallback 属性, React 就会跳过它, 继续讲错误向往传递, 知道被最近的 Suspense 捕获到
js 复制代码
<Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
  <Suspense>  // <--- 这个边界被跳过,没有 fallback 属性
    <Page />
  </Suspense>
</Suspense>
  1. 更新后: 如果 Suspense 组件没有提供 fallback 属性, 错误不会往外层传递, 而是展示为空
js 复制代码
<Suspense fallback={<Loading />}> // <--- 不使用
  <Suspense> // <--- 这个边界被使用, 将 fallback 渲染为 null
    <Page />
  </Suspense>
</Suspense>
  1. 为什么要做调整: Suspense 的错误如果一直往外透传, 那么这样会导致混乱、难以调试的情况发生

9.8 Concurrent Mode(并发模式)

并不是一个功能, 而是一个底层设计

  1. 使用新版本的 createRoot(root).render 来挂载节点将启用 并发模式, 但是并没开启 并发更新, 要相启用相应的 并发更新 需要使用相应的 api
  1. 开启并发更新: useTransition, 如下代码 useTransition 返回两个数组参数
  • isPending 表示是否正在等待中
  • useTransition 接收一个回调函数, 函数中的状态修改将被标记为 非紧急渲染 任务, 这样的话在大量的任务下也能保持 UI 能够快速的响应, 从而来显著改善用户交互
js 复制代码
import React, { useState, useEffect, useTransition } from 'react';

const App = () => {
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition();

  useEffect(() => {
    // 使用了并发特性,开启并发更新
    startTransition(() => {
      setList(new Array(10000).fill(null));
    });
  }, []);
  
  return (
    <>
      {list.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};
  1. 开启并发更新: useDeferredValue, 返回一个延迟响应的值, 可以让一个 state 延迟生效, 只有当前没有紧急更新时, 该值才会变为最新值! 同 useTransition 一样都是标记了一次非紧急更新
js 复制代码
import React, { useState, useEffect, useDeferredValue } from 'react';

const App = () => {
  const [list, setList] = useState([]);

  // 使用了并发特性,开启并发更新
  const deferredList = useDeferredValue(list);

  useEffect(() => {
    setList(new Array(10000).fill(null));
  }, []);

  return (
    <>
      {deferredList.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};
  1. useDeferredValueuseTransition 的区别
  • 相同: 从功能、作用以及内部的实现上来讲, 他们是一样的都是标记成了延迟更新任务
  • 不同: useTransition 是把更新任务变成了延迟更新任务, 而 useDeferredValue 是产生一个新的值, 这个值作为延时状态。(一个用来包装方法, 一个用来包装值)
  1. 简单总结: 所有的东西都是基于 fiber 架构实现的, fiber 为状态更新提供了可中断的能力
  • 并发更新的意义就是 交替执行 不同的任务(任务可以划分优先级, 高优先级的先执行), 当预留的时间不够用时, React 将线程控制权交还给浏览器, 等待下一帧时间到来, 然后继续被中断的工作
  • 并发模式是实现并发更新的基本前提, 同时时间切片是实现并发更新的具体手段

9.9 几个新的 API

  1. useId:
  • 参考: 为了生成唯一id, React18专门引入了新Hook: useId
  • 主要适用于 SSR(服务端渲染), 在应用的服务端或客户端之间生成唯一且稳定的 id
  • 背后的原理: 通过该组件在组件树中的层级结构来生成 id 这样就能够保证服务端或客户端之间的 id 是稳定的
  1. useSyncExternalStore:
    • 参考: React 18 撕裂介绍
    • 参考: React 的并发悖论
    • 视图撕裂: 对于开启并发更新的 React, 更新流程可能中断, 相同组件可能是在中断前后不同的宏任务中 render, 传递给他们的 stateprops 可能并不相同, 这就导致同一次更新, 同一个状态前后 UI 不一致的情况
    • ReactAPI 已经原生的解决的并发特性下的撕裂(tear)问题, 但是对于 redux 等外部框架它在控制状态时可能并非直接使用的 ReactAPI(useState), 而是自己在外部维护了一个 store 对象, 它脱离了 React 的管理, 也就无法依靠 React 自动解决撕裂问题。因此, React 对外提供了这样一个 API, 帮助这类框架开发者(有外部 store 需求的)解决撕裂问题
    • 对于如何解决外部框架的并发特性下的撕裂(tear)问题, React 目前并没有好的一个方案, 目前 useSyncExternalStore 的作用其实是状态管理库触发的更新都以同步的方式执行, 这样就不会有同步时机的问题了
  2. useInsertionEffect: 这个 Hooks 只建议 css-in-js 库来使用, 这个 Hooks 执行时机在 DOM 生成之后, useLayoutEffect 之前, 它的工作原理大致和 useLayoutEffect 相同, 只是此时无法访问 DOM 节点的引用, 一般用于提前注入 <style> 脚本

十、使用 React 需要注意的事项有哪些?

  1. state 不可直接进行修改改

  2. 不要在循环、条件或嵌套函数中调用 Hook, 必须始终在 React 函数的顶层使用 Hook

  3. 列表渲染需要设置唯一且稳定的 key

  4. 忘记以大写字母作为组件的名称开头

  5. 最好保持组件的代码量较少, 一个组件对应一个功能, 这样不仅可以节省我们的时间, 也有助于我们调试代码

  6. 类组件中注意 this 指向: 在 JSX 中给 DOM 绑定事件时, 回调函数默认情况下无法访问当前组件, 即回调函数中 this 不可用, 一般情况下我们可以通过 bind() 来改变函数的上下文来使其可用, 当然这里其实还可以使用箭头函数声明函数

  7. 过度使用 Redux: 尽管 Redux 很有用, 但您无需使用它来管理应用程序中的每个状态

本小节到处结束...

相关推荐
星就前端叭40 分钟前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_7482345243 分钟前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成43 分钟前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
噢,我明白了1 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__1 小时前
APIs-day2
javascript·css·css3
jwensh2 小时前
【Jenkins】Declarative和Scripted两种脚本模式有什么具体的区别
运维·前端·jenkins
关你西红柿子2 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
益达是我2 小时前
【Chrome】浏览器提示警告Chrome is moving towards a new experience
前端·chrome
济南小草根2 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.2 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析