资讯专栏INFORMATION COLUMN

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

kid143 / 2872人阅读

摘要:作为声明式的框架,接管了所有页面更新相关的操作。是用于内部操作的实例,这里将它的初始化为空数组并插入一个新的。连续次后,期望的结果应该是。原因很简单,因为次的时候,取到的都是在完后不会同步更新。

前言

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 (
      

"Welcom to React"

{ desc }

); } } 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 的内容,调用关系如下:

三、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;
};

最终的调用关系见下图:

五、总结

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99298.html

相关文章

  • React 源码深度解读(三):首次 DOM 元素渲染 - Part 3

    摘要:在学习源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。到此为止,首次渲染就完成啦总结从启动到元素渲染到页面,并不像看起来这么简单,中间经历了复杂的层级调用。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过...

    U2FsdGVkX1x 评论0 收藏0
  • React 源码深度解读(八):事务 - Part 2

    摘要:前言是一个十分庞大的库,由于要同时考虑和,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过程。在学习源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常...

    airborne007 评论0 收藏0
  • React 源码深度解读(六):依赖注入

    摘要:依赖注入和控制反转,这两个词经常一起出现。一句话表述他们之间的关系依赖注入是控制反转的一种实现方式。而两者有大量的代码都是可以共享的,这就是依赖注入的使用场景了。下一步就是创建具体的依赖内容,然后注入到需要的地方这里的等于这个对象。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级...

    glumes 评论0 收藏0
  • React 源码深度解读(四):首次自定义组件渲染 - Part 1

    摘要:本篇开始介绍自定义组件是如何渲染的。组件将自定义组件命名为,结构如下经过编译后,生成如下代码构建顶层包装组件跟普通元素渲染一样,第一步先会执行创建为的。调用顺序已在代码中注释。先看图,这部分内容将在下回分解 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非...

    Warren 评论0 收藏0
  • React 源码深度解读(二):首次 DOM 元素渲染 - Part 2

    摘要:本文将要讲解的调用栈是下面这个样子的平台无关相关如果看源码,我们会留意到很多相关的代码,我们暂时先将其忽略,会在后续的文章中进行讲解。现在我们来看一下各实例间的关系目前为止的调用栈平台无关相关下一篇讲解三总结本文讲解了转化为的过程。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 R...

    wean 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<