1. 程式人生 > >React面試之生命週期與狀態管理

React面試之生命週期與狀態管理

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 時被呼叫。例如:

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生命週期的建議用法。

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 ,那麼結果可能並不如你期待的一樣。

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 才會改變,三次呼叫等同於如下程式碼
Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)

當然你也可以通過以下方式來實現呼叫三次 setState 使得 count 為 3。

handle() {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}

如果你想在每次呼叫 setState 後獲得正確的 state ,可以通過如下程式碼實現。

handle() {
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
        console.log(this.state)
    })
}

Redux 原始碼簡析

首先讓我們來看下 Redux的combineReducers 函式。

// 傳入一個 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函式。

// 這是執行的第一個用於拋錯的函式
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 函式。

// 這個函式設計的很巧妙,通過傳入函式引用的方式讓我們完成多個函式的巢狀使用,術語叫做高階函式
// 通過使用 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 函式的部分程式碼。

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()
    }
  }
  // ......
}