Skip to content

React常见问题 ​

生命周期相关 ​

挂载阶段 ​

挂载阶段也可以理解为初始化阶段,也就是把我们的组件插入到 DOM 中。

  • constructor
  • getDerivedStateFromProps
  • UNSAFE_componentWillMount
  • render
  • (React Updates DOM and refs)
  • componentDidMount

constructor ​

组件的构造函数,第一个被执行。显式定义构造函数时,需要在第一行执行 super(props),否则不能再构造函数中拿到 this。

在构造函数中,我们一般会做两件事:

  • 初始化 state
  • 对自定义方法进行 this 绑定

getDerivedStateFromProps ​

是一个静态函数,所以不能在这里使用 this,也表明了 React 官方不希望调用方滥用这个生命周期函数。每当父组件引发当前组件的渲染过程时,getDerivedStateFromProps 都会被调用,这样我们有机会根据新的 props 和当前的 state 来调整一个新的 state。

这个函数会在收到新的 props,调用了 setState 或 forceUpdate 时被调用。

render ​

React 最核心的方法,class 组件中必须实现的方法。

当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回一下类型之一:

  • 原生的 DOM,如 div
  • React 组件
  • 数组或 Fragment
  • Portals(传送门)
  • 字符串或数字,被渲染成文本节点
  • 布尔值或 null,不会渲染任何东西

componentDidMount ​

在组件挂载之后立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。这个方法比较适合添加订阅的地方,如果添加了订阅,请记得在卸载的时候取消订阅。

你可以在 componentDidMount 里面直接调用 setState,它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前,如此保证了即使 render 了两次,用户也不会看到中间状态。

更新阶段 ​

更新阶段是指当组件的 props 发生了改变,或者组件内部调用了 setState 或者发生了 forceUpdate,这个阶段的过程包括:

  • UNSAFE_componentWillReceiveProps
  • getDerivedStateFromProps
  • sholdComponentUpdate
  • UNSAFE_componentWIllUpdate
  • render
  • getSnapshotBeforeUpdate
  • (React Updates DOM and refs)
  • componentDidUpdate

shouldComponentUpdate ​

它有两个参数,根据此函数的返回值来判断是否重新进行渲染,首次渲染或者是当我们调用了 forceUpdate 时并不会触发此方法,此方法仅用于性能优化。

但是官方提倡我们使用内置的 PureComponent 而不是自己编写 shouldComponentUpdate。

getSnapshotBeforeUpdate ​

这个生命周期函数发生在 render 之后,在更新之前,给了一个机会去获取 DOM 信息,计算得到并返回一个 snapshot,这个 snapshot 会作为 componentDidUpdate 第三个参数传入。

componentDidUpdate ​

这个函数会在更新后被立即调用,首次渲染不会执行此方法。在这个函数中我们可以操作 DOM,可以发起请求,还可以 setState,但注意一定要用条件语句,否则会导致无限循环。

卸载阶段 ​

componentWillUnmount ​

这个生命周期函数会在组件卸载销毁之前被调用,我们可以在这里执行一些清除操作。不要在这里调用 setState,因为组件不会重新渲染。

shouldComponentUpdate 的作用 ​

shouldComponentUpdate 这个方法用来判断是否需要调用 render 方法重新描绘 DOM。因为 DOM 的描绘性能开销很大,如果可以在这个生命周期阶段做出更优化的 DOM diff 算法,可以极大地提升性能。

渲染相关 ​

ReactDiff ​

由于 diff 操作本身会带来性能上的损耗,在 React 文档中提到过,即使最先进的算法中,将前后两棵树完全比对的算法复杂度为O(n3),其中 n 为树中元素的数量。如果 React 使用了该算法,那么仅仅一千个元素的页面所需要执行的计算量就是十亿的量级,这无疑是无法接受的。为了降低算法的复杂度,React 的 diff 会预设三个限制:

  1. 只对同级元素进行 diff 比对。如果一个元素节点在前后两次更新中跨越了层级,那么 React 不会尝试复用它
  2. 两个不同类型的元素会产生出不同的树。如果元素由 div 变成 p,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点
  3. 开发者可以通过 key 来暗示哪些子元素在不同的渲染下能保持稳定

React 中的 key 的作用 ​

  1. 和 type 值一起来标记同级节点,当前元素的唯一性。
  2. fiber 架构的唯一性
html
<!-- 更新前 -->
<div>
  <p key="ka">ka</p>
  <h3 key="song">song</he>
</div>

<!-- 更新后 -->
<div>
  <h3 key="song">song</h3>
  <p key="ka">ka</p>
</div>

如果没有 key,React 会认为 div 的第一个子节点由 p 变成 h3,第二个子节点由 h3 变成 p,则会销毁这两个节点并重新构造。但是当我们用 key 指明了节点前后对应关系后,React 知道 key === "ka" 的 p 更新后还在,所以可以复用该节点,只需要交换顺序。key 是 React 用来追踪哪些列表元素被修改、被添加或者被移除的辅助标志。

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React diff 算法中,React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重新渲染。同时,React 还需要借助 key 来判断元素与本地状态的关联关系。

React Fiber ​

  • 对于⼤大型项⽬目,组件树会很⼤大,这个时候递归遍历的成本就会很⾼高,会造成主线程被持续占⽤用,结果就是主线程上的布局、动画等周期性任务就⽆无法⽴立即得到处理理,造成视觉上的卡顿,影响⽤用户体验。
  • 增量渲染(把渲染任务拆分成块,匀到多帧)
  • 更新时能够暂停,终⽌,复⽤渲染任务
  • 给不同类型的更新赋予优先级
  • 并发⽅面新的基础能⼒

调用 setState 之后发生了什么 ​

状态合并

在代码中调用 setState 函数之后,React 会将传入的参数与之前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。

调和

经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会计算出新的树和老的树之间的差异,然后根据差异对界面进行最小化重新渲染。通过 diff 算法,React 能够精确制导哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

setState 是同步还是异步 ​

合成事件是批量异步执行的

原生事件是同步执行的

开放性问题 ​

虚拟 dom ​

为什么要使用虚拟 dom 对象 ​

DOM 操作很慢,轻微的操作都可能导致页面重新排版,非常耗性能。相对于 DOM 对象,js 对象 处理起来更快,而且更简单。通过 diff 算法对比新旧 VDom 之间的差异,可以批量的、最小化的执行 dom 操作,从而提高性能。

在获取原生的 dom 节点后,dom 节点的属性极多,diff 后的成本较高。在使用虚拟 dom 后,只需要对我们用到的属性做 dif f即可。

dom 的使用 ​

React中 用 JSX 语法描述视图,通过 babel-loader 转译后它们变为 React.createElement(...) 形 式,该函数将生成 VDom 来描述真实 dom。将来如果状态变化,VDom 将作出相应变化,再通过 diff 算法,对比新老 VDom 区别从而做出最终 dom 操作。

jsx ​

React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。使用 JSX 编写模板简单快速。JSX编译为 JavaScript 代码后进行了优化,执行更快。

虚拟 dom 的缺点 ​

在首次渲染的情况下,需要多一层计算,然后渲染页面。会比组装原生 dom 节点然后直接 innerHtml 的性能差一下

常见组件优化技术 ​

使用 shouldComponentUpdate ​

通过对比新老参数,来达到优化效果

js
shouldComponentUpdate(nextProps, nextState) {
  const { xxx } = this.props
  const { xxx: oldXxx } = nextProps
  if(xxx == oldXxx) return false
  return true
}

pureComponentUpdate ​

内部执行了一个shouldComponentUpdate,进行了一个前后值的浅比较。但是只能用作类组件。

React.Component 和 React.PureComponent很相似,两则的区别在于,PureComponent类帮我们以浅比较的方式对比props和state,实现了shouldComponentUpdate()函数,在某些情况下,使用PureComponent可以减少render函数的执行,提升性能。

PureComponent默认实现的shouldComponentUpdate()方法使用的是浅比较: 即值的比较或引用的比较, 不会进行深层次的对比,所以当props或state的值是引用类型时,即使对象的值改变了,但是对象的引用没变

PureComponent也会影响子组件

React.memo ​

在函数组件中,原理是进行props比较

js
const MemoComponent = memo(
  props => {
    return <div>123</div>
  },
  (prevProps, nextProps) => {
    return preveProps.xxx === nextProps.xxx
  }
)

useMemo ​

原理也是进行第二个依赖参数的比较,类似于vue中的computed

js
const xxx = useMemo(() => {
  return props * 2
}, [props])

useCallback ​

函数版本的useMemo,依赖项的对比。

js
const xxFn = useCallback(() => {

}, [propx])

重点API ​

React 中 ref 的作用 ​

ref 是 React 提供的一种可以安全访问 DOM 元素或者某个组件实例的方式。

在类组件中使用 createRef(),在函数组件中使用 useRef或者 forwardRef。

使用类型

  • createRef
  • forwardRef
  • useRef

cloneElement ​

js
const ProcessInput = () => React.cloneElement(<input />, {
  placeholder: 'xxxx'
})

原理

js
const cloneElement = (element, config, ...children) => {
  const props = Object.assign({}, element.props)

  let defaultProps = {}
  if (element.type && element.type.defaultProps) {
    defaultProps = element.type.defaultProps
  }

  for (let propName in config) {
    if(propName !== "key" && propName !== "ref") {
      let val = config[propName] || defaultProps[propName]
      val && (props[propName] = val )
    }
  }

  props.children = children.map(child => 
    typeof child == 'object' ? children : createTextNode(child)
  )
    
  return {
    key: element.key || config.key || '',
    type: element.type,
    props,
  }
}

Released under the MIT License.