前言
React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过程。在学习 React 源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。本文会大量用到原文中的例子,想体会原汁原味的感觉,推荐阅读原文。
本系列文章基于 React 15.4.2 ,以下是本系列其它文章的传送门:
React 源码深度解读(一):首次 DOM 元素渲染 - Part 1
React 源码深度解读(二):首次 DOM 元素渲染 - Part 2
React 源码深度解读(三):首次 DOM 元素渲染 - Part 3
React 源码深度解读(四):首次自定义组件渲染 - Part 1
React 源码深度解读(五):首次自定义组件渲染 - Part 2
React 源码深度解读(六):依赖注入
React 源码深度解读(七):事务 - Part 1
React 源码深度解读(八):事务 - Part 2
React 源码深度解读(九):单个元素更新
React 源码深度解读(十):Diff 算法详解
正文
在前面的系列文章里,已经对 React 的首次渲染和 事务(transaction)作了比较详细的介绍,接下来终于讲到它最核心的一个方法:setState
。作为声明式的框架,React 接管了所有页面更新相关的操作。我们只需要定义好状态和UI的映射关系,然后根据情况改变状态,它自然就能根据最新的状态将页面渲染出来,开发者不需要接触底层的 DOM 操作。状态的变更靠的就是setState
这一方法,下面我们来揭开它神秘的面纱。
二、setState
介绍开始前,先更新一下例子:
class App extends Component {
constructor(props) {
super(props);
this.state = {
desc: 'start',
color: 'blue'
};
this.timer = setTimeout(
() => this.tick(),
5000
);
}
tick() {
this.setState({
desc: 'end',
color: 'green'
});
}
render() {
const {desc, color} = this.state;
return (
<div className="App">
<div className="App-header">
<img src="main.jpg" className="App-logo" alt="logo" />
<h1> "Welcom to React" </h1>
</div>
<p className="App-intro" style={{color: color}}>
{ desc }
</p>
</div>
);
}
}
export default App;
state 保存了一个文本信息和颜色,5秒后触发更新,改变对应的文本与样式。
下面我们来看下setState
的源码:
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
这里的updater
也是通过依赖注入的方式,在组件实例化的时候注入进来的。相关代码如下:
// ReactCompositeComponent.js
mountComponent: function (
transaction,
hostParent,
hostContainerInfo,
context
) {
...
// 这里的 transaction 是 ReactReconcileTransaction
var updateQueue = transaction.getUpdateQueue();
var doConstruct = shouldConstruct(Component);
// 在这个地方将 updater 注入
var inst = this._constructComponent(
doConstruct,
publicProps,
publicContext,
updateQueue
);
...
}
// ReactReconcileTransaction.js
var ReactUpdateQueue = require('ReactUpdateQueue');
getUpdateQueue: function () {
return ReactUpdateQueue;
}
// ReactUpdateQuene.js
var ReactUpdates = require('ReactUpdates');
enqueueSetState: function (publicInstance, partialState) {
...
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState'
);
if (!internalInstance) {
return;
}
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
this.updater.enqueueSetState
最终落地的代码是ReactUpdates.enqueueUpdate
。internalInstance
是用于内部操作的 ReactCompositeComponent 实例,这里将它的_pendingStateQueue
初始化为空数组并插入一个新的 state({desc:'end',color:'green'})。
结合之前 transaction 的内容,调用关系如下:
三、Transaction 最终操作
从上面的调用关系图可以看出,transaction 最终会调用 ReactUpdates 的 runBatchedUpdates 方法。
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
...
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
...
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber
);
...
}
}
接着是调用 ReactReconciler 的 performUpdateIfNecessary,然后到 ReactCompositeComponent 的一系列方法:
performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(
this,
this._pendingElement,
transaction,
this._context
);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(
transaction,
this._currentElement,
this._currentElement,
this._context,
this._context
);
} else {
this._updateBatchNumber = null;
}
},
updateComponent: function (
transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext
) {
var inst = this._instance;
...
var nextState = this._processPendingState(nextProps, nextContext);
...
this._performComponentUpdate(
nextParentElement,
nextProps,
nextState,
nextContext,
transaction,
nextUnmaskedContext
);
},
_processPendingState: function (props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
...
var nextState = Object.assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
Object.assign(
nextState,
typeof partial === 'function' ?
partial.call(inst, nextState, props, context) :
partial
);
}
return nextState;
},
_performComponentUpdate: function (
nextElement,
nextProps,
nextState,
nextContext,
transaction,
unmaskedContext
) {
var inst = this._instance;
...
this._updateRenderedComponent(transaction, unmaskedContext);
...
},
/**
* Call the component's `render` method and update the DOM accordingly.
*/
_updateRenderedComponent: function (transaction, context) {
// ReactDOMComponent
var prevComponentInstance = this._renderedComponent;
// 上一次的Virtual DOM(ReactElement)
var prevRenderedElement = prevComponentInstance._currentElement;
// 调用 render 获取最新的Virtual DOM(ReactElement)
var nextRenderedElement = this._renderValidatedComponent();
...
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
);
}
...
},
这里最重要的方法分别为_processPendingState
和_updateRenderedComponent
。_processPendingState
是真正更新 state 的地方,可以看到它其实就是一个Object.assign
的过程。在实际开发过程中,如果需要基于之前的 state 值连续进行运算的话,如果直接通过对象去 setState 往往得到的结果是错误的,看以下例子:
// 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});
假设 count 的初始值是 0 。连续 3 次 setState 后,期望的结果应该是 3 。但实际得到的值是 1 。原因很简单,因为 3 次 setState 的时候,取到的this.state.count
都是 0 (state 在 set 完后不会同步更新)。如果想得到期望的结果,代码要改成下面的样子:
function add(nextState, props, context) {
return {count: nextState.count + 1};
}
this.setState(add);
this.setState(add);
this.setState(add);
结合源码来看,如果 setState 的参数类型是 function,每次合并后的nextState
都会作为参数传入,得到的结果自然是正确的了:
Object.assign(
nextState,
typeof partial === 'function'
? partial.call(inst, nextState, props, context)
: partial,
);
_updateRenderedComponent
会取出实例的 ReactDOMComponent,然后调用 render 方法,得出最新的 Virtual DOM 后启动 Diff 的过程。
四、Diff
ReactReconciler.receiveComponent
最终会调用 ReactDOMComponent 的 receiveComponent 方法,进而再调用 updateComponent 方法:
updateComponent: function (transaction, prevElement, nextElement, context) {
var lastProps = prevElement.props;
var nextProps = this._currentElement.props;
...
this._updateDOMProperties(lastProps, nextProps, transaction);
this._updateDOMChildren(
lastProps,
nextProps,
transaction,
context
);
...
},
这个方法只有 2 个操作,一个是更新属性,另一个是更新子孙结点。先来看看更新属性的操作:
_updateDOMProperties: function (lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;
// 删除旧的属性
for (propKey in lastProps) {
// 筛选出后来没有但之前有的属性
if (nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null) {
continue;
}
if (propKey === STYLE) {
var lastStyle = this._previousStyleCopy;
// 初始化 styleUpdates,之前所有的 style 属性设置为空
for (styleName in lastStyle) {
// 将旧的 style 属性设置为空
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
this._previousStyleCopy = null;
}
...
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
DOMPropertyOperations.deleteValueForProperty(getNode(
this), propKey);
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp =
propKey === STYLE ? this._previousStyleCopy :
lastProps != null ? lastProps[propKey] : undefined;
// 值相等则跳过
if (!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
nextProp == null && lastProp == null) {
continue;
}
if (propKey === STYLE) {
if (nextProp) {
nextProp = this._previousStyleCopy = Object.assign({}, nextProp);
} else {
this._previousStyleCopy = null;
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[
styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
styleUpdates = nextProp;
}
}
...
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
var node = getNode(this);
// If we're updating to null or undefined, we should remove the property
// from the DOM node instead of inadvertently setting to a string. This
// brings us in line with the same behavior we have on initial render.
if (nextProp != null) {
DOMPropertyOperations.setValueForProperty(node,
propKey, nextProp);
} else {
DOMPropertyOperations.deleteValueForProperty(node,
propKey);
}
}
}
if (styleUpdates) {
CSSPropertyOperations.setValueForStyles(
getNode(this),
styleUpdates,
this
);
}
},
这里主要有 2 个循环,第一个循环删除旧的属性,第二个循环设置新的属性。属性的删除靠的是DOMPropertyOperations.deleteValueForProperty
或DOMPropertyOperations.deleteValueForAttribute
,属性的设置靠的是DOMPropertyOperations.setValueForProperty
或DOMPropertyOperations.setValueForAttribute
。以 setValueForAttribute 为例子,最终是调用 DOM 的 api :
setValueForAttribute: function (node, name, value) {
if (!isAttributeNameSafe(name)) {
return;
}
if (value == null) {
node.removeAttribute(name);
} else {
node.setAttribute(name, '' + value);
}
},
针对 style 属性,由styleUpdates
这个对象来收集变化的信息。它会先将旧的 style 内的所有属性设置为空,然后再用新的 style 来填充。得出新的 style 后调用CSSPropertyOperations.setValueForStyles
来更新:
setValueForStyles: function (node, styles, component) {
var style = node.style;
for (var styleName in styles) {
...
if (styleValue) {
style[styleName] = styleValue;
} else {
...
style[styleName] = '';
}
}
},
接下来看 updateDOMChildren 。
updateDOMChildren: function (lastProps, nextProps, transaction,
context) {
var lastContent =
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children :
null;
var nextContent =
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children :
null;
...
if (nextContent != null) {
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
}
}
...
},
结合我们的例子,最终会调用updateTextContent
。这个方法来自 ReactMultiChild ,可以简单理解为 ReactDOMComponent 继承了 ReactMultiChild 。
updateTxtContent: function (nextContent) {
var prevChildren = this._renderedChildren;
// Remove any rendered children.
ReactChildReconciler.unmountChildren(prevChildren, false);
for (var name in prevChildren) {
if (prevChildren.hasOwnProperty(name)) {
invariant(false,
'updateTextContent called on non-empty component.'
);
}
}
// Set new text content.
var updates = [makeTextContent(nextContent)];
processQueue(this, updates);
},
function makeTextContent(textContent) {
// NOTE: Null values reduce hidden classes.
return {
type: 'TEXT_CONTENT',
content: textContent,
fromIndex: null,
fromNode: null,
toIndex: null,
afterNode: null,
};
},
function processQueue(inst, updateQueue) {
ReactComponentEnvironment.processChildrenUpdates(
inst,
updateQueue,
);
}
这里的 ReactComponentEnvironment 通过依赖注入的方式注入后,实际上是 ReactComponentBrowserEnvironment 。最终会调用 DOMChildrenOperations 的 processUpdates:
processUpdates: function (parentNode, updates) {
for (var k = 0; k < updates.length; k++) {
var update = updates[k];
switch (update.type) {
...
case 'TEXT_CONTENT':
setTextContent(
parentNode,
update.content
);
if (__DEV__) {
ReactInstrumentation.debugTool.onHostOperation({
instanceID: parentNodeDebugID,
type: 'replace text',
payload: update.content.toString(),
});
}
break;
...
}
}
},
// setTextContent.js
var setTextContent = function(node, text) {
if (text) {
var firstChild = node.firstChild;
if (firstChild && firstChild === node.lastChild && firstChild.nodeType === 3) {
firstChild.nodeValue = text;
return;
}
}
node.textContent = text;
};
最终的调用关系见下图:
五、总结
本文将 setState 的整个流程从头到尾走了一遍,下一篇将会详细的介绍 Diff 的策略。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。