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
}
- 两次的打印都为 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。
代码语言: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()
}
}
// ......
}