Appearance
Table of Contents generated with DocToc
React Lifecycle analysis
The Fiber mechanism was introduced in the V16 release. The mechanism affects some of the lifecycle calls to a certain extent and introduces two new APIs to solve the problems.
In previous versions, if you had a very complex composite component and then changed the state
of the topmost component, the call stack might be long.
If the call stack is too long, and complicated operations are performed in the middle, it may cause the main thread to be blocked for a long time, resulting in a bad user experience. Fiber is born to solve this problem.
Fiber is essentially a virtual stack frame, and the new scheduler freely schedules these frames according to their priority, thereby changing the previous synchronous rendering to asynchronous rendering, and segmenting the update without affecting the experience.
React has its own set of logic on how to prioritize. For things that require high real-time performance, such as animation, which means it must be rendered once within 16 ms to ensure that it is not stuck, React pauses the update every 16 ms (within 16ms) and returns to continue rendering the animation.
For asynchronous rendering, there are now two stages of rendering: reconciliation
and commit
. The former process can be interrupted, while the latter cannot be suspended, and the interface will be updated until it is completed.
Reconciliation stage
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
Commit stage
componentDidMount
componentDidUpdate
componentWillUnmount
Because the reconciliation
phase can be interrupted, the lifecycle functions that will be executed in the reconciliation
phase may be called multiple times, which may cause bugs. So for these functions, except for shouldComponentUpdate
, should be avoided as much as possible, and a new API is introduced in V16 to solve this problem.
getDerivedStateFromProps
is used to replace componentWillReceiveProps
, which is called during initialization and update
js
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
is used to replace componentWillUpdate
, which is called after the update
but before the DOM update to read the latest DOM data.
The usage advice of Lifecycle methods in React V16
js
class ExampleComponent extends React.Component {
// Used to initialize the state
constructor() {}
// Used to replace `componentWillReceiveProps` , which will be called when initializing and `update`
// Because the function is static, you can't get `this`
// If need to compare `prevProps`, you need to maintain it separately in `state`
static getDerivedStateFromProps(nextProps, prevState) {}
// Determine whether you need to update components, mostly for component performance optimization
shouldComponentUpdate(nextProps, nextState) {}
// Called after the component is mounted
// Can request or subscribe in this function
componentDidMount() {}
// Used to get the latest DOM data
getSnapshotBeforeUpdate() {}
// Component is about to be destroyed
// Can remove subscriptions, timers, etc. here
componentWillUnmount() {}
// Called after the component is destroyed
componentDidUnMount() {}
// Called after component update
componentDidUpdate() {}
// render component
render() {}
// The following functions are not recommended
UNSAFE_componentWillMount() {}
UNSAFE_componentWillUpdate(nextProps, nextState) {}
UNSAFE_componentWillReceiveProps(nextProps) {}
}
setState
setState
is an API that is often used in React, but it has some problems that can lead to mistakes. The core reason is that the API is asynchronous.
First, calling setState
does not immediately cause a change to state
, and if you call multiple setState
at a time, the result may not be as you expect.
js
handle() {
// Initialize `count` to 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
}
First, both prints are 0, because setState
is an asynchronous API and will only execute after the sync code has finished running. The reason for setState
is asynchronous is that setState
may cause repainting of the DOM. If the call is repainted immediately after the call, the call will cause unnecessary performance loss. Designed to be asynchronous, you can put multiple calls into a queue and unify the update process when appropriate.
Second, although setState
is called three times, the value of count
is still 1. Because multiple calls are merged into one, only state
will change when the update ends, and three calls are equivalent to the following code.
js
Object.assign(
{},
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
)
Of course, you can also call setState
three times by the following way to make count
3
js
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
}
If you want to get the correct state
after each call to setState
, you can do it with the following code:
js
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
console.log(this.state)
})
}
Redux Source Code Analysis
Let's take a look at the combineReducers
function first.
js
// pass an object
export default function combineReducers(reducers) {
// get this object's keys
const reducerKeys = Object.keys(reducers)
// reducers after filtering
const finalReducers = {}
// get the values corresponding to every key
// in dev environment, check if the value is undefined
// then put function type values into 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]
}
}
// get the keys of the reducers after filtering
const finalReducerKeys = Object.keys(finalReducers)
// in dev environment check and save unexpected key to cache for warnings later
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
// explanations of the function is below
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// combineReducers returns another function, which is reducer after merging
// this function returns the root state
// also notice a closure is used here. The function uses some outside properties
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
// explanations of the function is below
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
// if state changed
let hasChanged = false
// state after changes
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
// get the key with index
const key = finalReducerKeys[i]
// get the corresponding reducer function with key
const reducer = finalReducers[key]
// the key in state tree is the same as the key in finalReducers
// so the key of the parameter passed to combineReducers represents each reducer as well as each state
const previousStateForKey = state[key]
// execute reducer function to get the state corresponding to the key
const nextStateForKey = reducer(previousStateForKey, action)
// check the value of state, report error if it's undefined
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
// put the value into nextState
nextState[key] = nextStateForKey
// if state changed
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// as long as state changed, return the new state
return hasChanged ? nextState : state
}
}
combineReducers
is simple in general. In short, it accepts an object and return a function after processing the parameters. This function has an object finalReducers that stores the processed parameters. The object is then itereated on, each reducer function in it is executed, and the new state is returned.
Let's then take a look at the two functions used in combineReducers.
js
// the first function used to throw errors
function assertReducerShape(reducers) {
// iterate on the parameters in combineReducers
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
// pass an action
const initialState = reducer(undefined, { type: ActionTypes.INIT })
// throw an error if the state is 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.`
)
}
// process again, considering the case that the user returned a value for ActionTypes.INIT in the reducer
// pass a random action and check if the value is 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
) {
// here the reducers is already finalReducers
const reducerKeys = Object.keys(reducers)
const argumentName =
action && action.type === ActionTypes.INIT
? 'preloadedState argument passed to createStore'
: 'previous state received by the reducer'
// if finalReducers is empty
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.'
)
}
// if the state passed is not an object
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('", "')}"`
)
}
// compare the keys of the state and of finalReducers and filter out the extra keys
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
// if unexpectedKeys is not empty
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.`
)
}
}
Let's then take a look at compose
function
js
// This function is quite elegant. It let us stack several functions via passing the references of functions. The term is called Higher-order function.
// call functions from the right to the left with reduce function
// for the example in the project above
compose(
applyMiddleware(thunkMiddleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
// with compose it turns into applyMiddleware(thunkMiddleware)(window.devToolsExtension()())
// so you should return a function when window.devToolsExtension is not found
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)))
}
Let's then analyze part of the source code of createStore
function
js
export default function createStore(reducer, preloadedState, enhancer) {
// normally preloadedState is rarely used
// check type, is the second parameter is a function and there is no third parameter, then exchange positions
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// check if enhancer is a function
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// if there is no type error, first execute enhancer, then execute createStore
return enhancer(createStore)(reducer, preloadedState)
}
// check if reducer is a function
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
// current reducer
let currentReducer = reducer
// current state
let currentState = preloadedState
// current listener array
let currentListeners = []
// this is a very important design. The purpose is that currentListeners array is an invariant when the listeners are iterated every time
// we can consider if only currentListeners exists. If we execute subscribe again in some subscribe execution, or unsubscribe, it would change the length of the currentListeners array, so there might be an index error
let nextListeners = currentListeners
// if reducer is executing
let isDispatching = false
// if currentListeners is the same as nextListeners, assign the value back
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// ......
}
We look at applyMiddleware
function next
Before that I need to introduce a concept called function Currying. Currying is a technology for changing a function with multiple parameters to a series of functions with a single parameter.
js
function add(a,b) { return a + b }
add(1, 2) => 3
// for the above function, we can use Currying like so
function add(a) {
return b => {
return a + b
}
}
add(1)(2) => 3
// you can understand Currying like this:
// we store an outside variable with a closure, and return a function that takes a parameter. In this function, we use the stored variable and return the value.
js
// this function should be the most abstruse part of the whole source code
// this function returns a function Curried
// therefore the funciton should be called like so: applyMiddleware(...middlewares)(createStore)(...args)
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// here we execute createStore, and pass the parameters passed lastly to the applyMiddleware function
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
let chain = []
// every middleware should have these two functions
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// pass every middleware in middlewares to middlewareAPI
chain = middlewares.map(middleware => middleware(middlewareAPI))
// same as before, calle very middleWare from right to left, and pass to store.dispatch
dispatch = compose(...chain)(store.dispatch)
// this piece is a little abstract, we'll analyze together with the code of redux-thunk
// createThunkMiddleware returns a 3-level function, the first level accepts a middlewareAPI parameter
// the second level accepts store.dispatch
// the third level accepts parameters in dispatch
{function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// check if the parameters in dispatch is a function
if (typeof action === 'function') {
// if so, pass those parameters, until action is no longer a function, then execute dispatch({type: 'XXX'})
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
export default thunk;}
// return the middleware-empowered dispatch and the rest of the properties in store.
return {
...store,
dispatch
}
}
}
Now we've passed the hardest part. Let's take a look at some easier pieces.
js
// Not much to say here, return the current state, but we can't call this function when reducer is running
function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState
}
// accept a function parameter
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
// the major design of this part is already covered in the description of nextListeners. Not much to talk about otherwise
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
// return a cancel subscription function
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
function dispatch(action) {
// the prototype dispatch will check if action is an object
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// note that you can't call dispatch function in reducers
// it would cause a stack overflow
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
// execute the composed function after combineReducers
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// iterate on currentListeners and execute saved functions in the array
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// at the end of createStore, invoke an action dispatch({ type: ActionTypes.INIT });
// to initialize state