React面试之生命周期与状态管理

2022-11-30 11:38:33 浏览数 (2)

React 生命周期

在 V16 版本中引入了 Fiber 机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的 2 个 API 来解决问题。

在之前的版本中,如果你拥有一个很复杂的复合组件,然后改动了最上层组件的 state,那么调用栈可能会很长。调用栈过长,再加上中间进行了复杂的操作,就可能导致长时间阻塞主线程,带来不好的用户体验。Fiber 就是为了解决该问题而生。

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。

对于如何区别优先级,React 有自己的一套逻辑。对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的情况下,React 会每 16 ms(以内) 暂停一下更新,返回来继续渲染动画。

对于异步渲染,现在渲染有两个阶段:reconciliation 和 commit 。前者过程是可以被打断的,后者则不能有任何的暂停,会一直更新界面直到完成。

Reconciliation 阶段 Reconciliation 阶段主要会涉及以下一些生命周期函数:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 阶段 Commit 阶段涉及到生命周期函数有:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

因为 reconciliation 阶段是可以被打断的,所以 reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。所以对于 reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。

getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用。例如:

代码语言:javascript复制
class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

V16 生命周期函数建议用法

以下实例是React V16生命周期的建议用法。

代码语言:javascript复制
class ExampleComponent extends React.Component {
  // 用于初始化 state
  constructor() {}
  // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
  // 因为该函数是静态函数,所以取不到 `this`
  // 如果需要对比 `prevProps` 需要单独在 `state` 中维护
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判断是否需要更新组件,多用于组件性能优化
  shouldComponentUpdate(nextProps, nextState) {}
  // 组件挂载后调用
  // 可以在该函数中进行请求或者订阅
  componentDidMount() {}
  // 用于获得最新的 DOM 数据
  getSnapshotBeforeUpdate() {}
  // 组件即将销毁
  // 可以在此处移除订阅,定时器等等
  componentWillUnmount() {}
  // 组件销毁后调用
  componentDidUnMount() {}
  // 组件更新后调用
  componentDidUpdate() {}
  // 渲染组件函数
  render() {}
  // 以下函数不建议使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}

如何理解setState

setState 在 React 中是经常使用的一个 API,但是它存在一些问题,可能会导致犯错,核心原因就是因为这个 API 是异步的。

首先 setState 的调用并不会马上引起 state 的改变,并且如果你一次调用了多个 setState ,那么结果可能并不如你期待的一样。

代码语言:javascript复制
handle() {
  // 初始化 `count` 为 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count   1 })
  this.setState({ count: this.state.count   1 })
  this.setState({ count: this.state.count   1 })
  console.log(this.state.count) // -> 0
}
  1. 两次的打印都为 0,因为 setState 是个异步 API,只有同步代码运行完毕才会执行。setState异步的原因我认为在于,setState 可能会导致 DOM的重绘,如果调用一次就马上去进行重绘,那么调用多次就会造成不必要的性能损失。设计成异步的话,就可以将多次调用放入一个队列中,在恰当的时候统一进行更新过程。
  2. 虽然调用了三次 setState ,但是 count 的值还是为 1。因为多次调用会合并为一次,只有当更新结束后 state 才会改变,三次调用等同于如下代码
代码语言:javascript复制
Object.assign(  
  {},
  { count: this.state.count   1 },
  { count: this.state.count   1 },
  { count: this.state.count   1 },
)

当然你也可以通过以下方式来实现调用三次 setState 使得 count 为 3。

代码语言:javascript复制
handle() {
  this.setState((prevState) => ({ count: prevState.count   1 }))
  this.setState((prevState) => ({ count: prevState.count   1 }))
  this.setState((prevState) => ({ count: prevState.count   1 }))
}

如果你想在每次调用 setState 后获得正确的 state ,可以通过如下代码实现。

代码语言:javascript复制
handle() {
    this.setState((prevState) => ({ count: prevState.count   1 }), () => {
        console.log(this.state)
    })
}

Redux 源码简析

首先让我们来看下 Redux的combineReducers 函数。

代码语言:javascript复制
// 传入一个 object
export default function combineReducers(reducers) {
 // 获取该 Object 的 key 值
  const reducerKeys = Object.keys(reducers)
  // 过滤后的 reducers
  const finalReducers = {}
  // 获取每一个 key 对应的 value
  // 在开发环境下判断值是否为 undefined
  // 然后将值类型是函数的值放入 finalReducers
  for (let i = 0; i < reducerKeys.length; i  ) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 拿到过滤后的 reducers 的 key 值
  const finalReducerKeys = Object.keys(finalReducers)

  // 在开发环境下判断,保存不期望 key 的缓存用以下面做警告  
  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
  // 该函数解析在下面
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
// combineReducers 函数返回一个函数,也就是合并后的 reducer 函数
// 该函数返回总的 state
// 并且你也可以发现这里使用了闭包,函数里面使用到了外面的一些属性
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    // 该函数解析在下面
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    // state 是否改变
    let hasChanged = false
    // 改变后的 state
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i  ) {
    // 拿到相应的 key
      const key = finalReducerKeys[i]
      // 获得 key 对应的 reducer 函数
      const reducer = finalReducers[key]
      // state 树下的 key 是与 finalReducers 下的 key 相同的
      // 所以你在 combineReducers 中传入的参数的 key 即代表了 各个 reducer 也代表了各个 state
      const previousStateForKey = state[key]
      // 然后执行 reducer 函数获得该 key 值对应的 state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 判断 state 的值,undefined 的话就报错
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 然后将 value 塞进去
      nextState[key] = nextStateForKey
      // 如果 state 改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // state 只要改变过,就返回新的 state
    return hasChanged ? nextState : state
  }
}

combineReducers 函数主要用来接收一个对象,将参数过滤后返回一个函数。该函数里有一个过滤参数后的对象 finalReducers,遍历该对象,然后执行对象中的每一个 reducer 函数,最后将新的 state 返回。

接下来让我们来看看 combinrReducers 中用到的两个函数:assertReducerShape和compose函数。

代码语言:javascript复制
// 这是执行的第一个用于抛错的函数
function assertReducerShape(reducers) {
// 将 combineReducers 中的参数遍历
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    // 给他传入一个 action
    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    // 如果得到的 state 为 undefined 就抛错
    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. `  
          `If the state passed to the reducer is undefined, you must `  
          `explicitly return the initial state. The initial state may `  
          `not be undefined. If you don't want to set a value for this reducer, `  
          `you can use null instead of undefined.`
      )
    }
    // 再过滤一次,考虑到万一你在 reducer 中给 ActionTypes.INIT 返回了值
    // 传入一个随机的 action 判断值是否为 undefined
    const type =
      '@@redux/PROBE_UNKNOWN_ACTION_'  
      Math.random()
        .toString(36)
        .substring(7)
        .split('')
        .join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. `  
          `Don't try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" `  
          `namespace. They are considered private. Instead, you must return the `  
          `current state for any unknown actions, unless it is undefined, `  
          `in which case you must return the initial state, regardless of the `  
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // 这里的 reducers 已经是 finalReducers
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'

  // 如果 finalReducers 为空
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed '  
      'to combineReducers is an object whose values are reducers.'
    )
  }
    // 如果你传入的 state 不是对象
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "`  
      {}.toString.call(inputState).match(/s([a-z|A-Z] )/)[1]  
      `". Expected argument to be an object with the following `  
      `keys: "${reducerKeys.join('", "')}"`
    )
  }
    // 将参入的 state 于 finalReducers 下的 key 做比较,过滤出多余的 key
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

// 如果 unexpectedKeys 有值的话
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} `  
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. `  
      `Expected to find one of the known reducer keys instead: `  
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}

接下来让我们先来看看 compose 函数。

代码语言:javascript复制
// 这个函数设计的很巧妙,通过传入函数引用的方式让我们完成多个函数的嵌套使用,术语叫做高阶函数
// 通过使用 reduce 函数做到从右至左调用函数
// 对于上面项目中的例子
compose(
    applyMiddleware(thunkMiddleware),
    window.devToolsExtension ? window.devToolsExtension() : f => f
) 
// 经过 compose 函数变成了 applyMiddleware(thunkMiddleware)(window.devToolsExtension()())
// 所以在找不到 window.devToolsExtension 时你应该返回一个函数
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

然后我们在来看一下 createStore 函数的部分代码。

代码语言:javascript复制
export default function createStore(reducer, preloadedState, enhancer) {
  // 一般 preloadedState 用的少,判断类型,如果第二个参数是函数且没有第三个参数,就调换位置
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // 判断 enhancer 是否是函数
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 类型没错的话,先执行 enhancer,然后再执行 createStore 函数
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 判断 reducer 是否是函数
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  // 当前 reducer
  let currentReducer = reducer
  // 当前状态
  let currentState = preloadedState
  // 当前监听函数数组
  let currentListeners = []
  // 这是一个很重要的设计,为的就是每次在遍历监听器的时候保证 currentListeners 数组不变
  // 可以考虑下只存在 currentListeners 的情况,如果我在某个 subscribe 中再次执行 subscribe
  // 或者 unsubscribe,这样会导致当前的 currentListeners 数组大小发生改变,从而可能导致
  // 索引出错
  let nextListeners = currentListeners
  // reducer 是否正在执行
  let isDispatching = false
  // 如果 currentListeners 和 nextListeners 相同,就赋值回去
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  // ......
}

0 人点赞