前端进阶之React 渲染原理

Table of Contents generated with DocToc

React 生命周期分析

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

在之前的版本中,如果你拥有一个很复杂的复合组件,然后改动了最上层组件的 state,那么调用栈可能会很长

调用栈过长,再加上中间进行了复杂的操作,就可能导致长时间阻塞主线程,带来不好的用户体验。Fiber 就是为了解决该问题而生。

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

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

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

Reconciliation 阶段

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 阶段

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 生命周期函数用法建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 ,那么结果可能并不如你期待的一样。

1
2
3
4
5
6
7
8
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 才会改变,三次调用等同于如下代码

1
2
3
4
5
6
Object.assign(
{},
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
{ count: this.state.count + 1 }
);

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

1
2
3
4
5
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
}

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

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

Redux 源码分析

首先让我们来看下 combineReducers 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 传入一个 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 中用到的两个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 这是执行的第一个用于抛错的函数
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 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 这个函数设计的很巧妙,通过传入函数引用的方式让我们完成多个函数的嵌套使用,术语叫做高阶函数
// 通过使用 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 函数的部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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();
}
}
// ......
}

接下来先来介绍 applyMiddleware 函数

在这之前我需要先来介绍一下函数柯里化,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

1
2
3
4
5
6
7
8
9
10
function add(a,b) { return a + b }
add(1, 2) => 3
// 对于以上函数如果使用柯里化可以这样改造
function add(a) {
return b => {
return a + b
}
}
add(1)(2) => 3
// 你可以这样理解函数柯里化,通过闭包保存了外部的一个变量,然后返回一个接收参数的函数,在该函数中使用了保存的变量,然后再返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 这个函数应该是整个源码中最难理解的一块了
// 该函数返回一个柯里化的函数
// 所以调用这个函数应该这样写 applyMiddleware(...middlewares)(createStore)(...args)
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 这里执行 createStore 函数,把 applyMiddleware 函数最后次调用的参数传进来
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 = []
// 每个中间件都应该有这两个函数
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 把 middlewares 中的每个中间件都传入 middlewareAPI
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 和之前一样,从右至左调用每个中间件,然后传入 store.dispatch
dispatch = compose(...chain)(store.dispatch)
// 这里只看这部分代码有点抽象,我这里放入 redux-thunk 的代码来结合分析
// createThunkMiddleware返回了3层函数,第一层函数接收 middlewareAPI 参数
// 第二次函数接收 store.dispatch
// 第三层函数接收 dispatch 中的参数
{function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// 判断 dispatch 中的参数是否为函数
if (typeof action === 'function') {
// 是函数的话再把这些参数传进去,直到 action 不为函数,执行 dispatch({tyep: 'XXX'})
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}
const thunk = createThunkMiddleware();

export default thunk;}
// 最后把经过中间件加强后的 dispatch 于剩余 store 中的属性返回,这样你的 dispatch
return {
...store,
dispatch
}
}
}

好了,我们现在将困难的部分都攻克了,来看一些简单的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 这个没啥好说的,就是把当前的 state 返回,但是当正在执行 reducer 时不能执行该方法
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;
}
// 接收一个函数参数
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.');
}
// 这部分最主要的设计 nextListeners 已经讲过,其他基本没什么好说的
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 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) {
// 原生的 dispatch 会判断 action 是否为对象
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?'
);
}
// 注意在 Reducers 中是不能执行 dispatch 函数的
// 因为你一旦在 reducer 函数中执行 dispatch,会引发死循环
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.');
}
// 执行 combineReducers 组合后的函数
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
// 然后遍历 currentListeners,执行数组中保存的函数
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}

return action;
}
// 然后在 createStore 末尾会发起一个 action dispatch({ type: ActionTypes.INIT });
// 用以初始化 state