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
}
- 兩次的列印都為 0,因為 setState 是個非同步 API,只有同步程式碼執行完畢才會執行。setState非同步的原因我認為在於,setState 可能會導致 DOM的重繪,如果呼叫一次就馬上去進行重繪,那麼呼叫多次就會造成不必要的效能損失。設計成非同步的話,就可以將多次呼叫放入一個佇列中,在恰當的時候統一進行更新過程。
- 雖然呼叫了三次 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()
}
}
// ......
}