React 源码深度解读(九):单个元素更新

  • 前言

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.enqueueUpdateinternalInstance是用于内部操作的 ReactCompositeComponent 实例,这里将它的_pendingStateQueue初始化为空数组并插入一个新的 state({desc:'end',color:'green'})。

结合之前 transaction 的内容,调用关系如下:

React 源码深度解读(九):单个元素更新

三、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.deleteValueForPropertyDOMPropertyOperations.deleteValueForAttribute,属性的设置靠的是DOMPropertyOperations.setValueForPropertyDOMPropertyOperations.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;
};

最终的调用关系见下图:

React 源码深度解读(九):单个元素更新

  • 五、总结

本文将 setState 的整个流程从头到尾走了一遍,下一篇将会详细的介绍 Diff 的策略。

相关推荐