React源码解析-UI更新(单个DOM元素)III

2018-11-14 admin

在前面的系列文章里,已经对 React 的首次渲染和 事务(transaction)作了比较详细的介绍,接下来终于讲到它最核心的一个方法:setState。作为声明式的框架,React 接管了所有页面更新相关的操作。我们只需要定义好状态和UI的映射关系,然后根据情况改变状态,它自然就能根据最新的状态将页面渲染出来,开发者不需要接触底层的 DOM 操作。状态的变更靠的就是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 的内容,调用关系如下:

clipboard.png

三、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 的过程。

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://segmentfault.com/a/1190000017014321

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-45463.html

文章标题:React源码解析-UI更新(单个DOM元素)III

相关文章
React Native 用JavaScript编写原生ios应用
ReactNative 可以基于目前大热的开源JavaScript库React.js来开发iOS和Android原生App。而且React Native已经用于生产环境——Facebook Groups iOS 应用就是基于它开发的。 Re...
2015-11-12
jQuery中DOM树操作之复制元素的方法
本文实例讲述了jQuery中DOM树操作之复制元素的方法。分享给大家供大家参考。具体分析如下: 复制元素 前面提到的操作包括:插人新创建的元素、将元素从文档中的一个位置移动 到另一个位置,以及通过新元素来包装已有的元素。可是,有时候也会用到...
2015-11-13
Easyui Tree获取当前选择节点的所有顶级父节点
只支持四层目录结构,比较笨的一个方法 JS代码 var node = $(&#x27;#tree&#x27;).tree(&#x27;getSelected&#x27;); &#x2F;&#x2F;获取该节点所有父节点 ...
2017-03-17
jQuery给多个不同元素添加class样式的方法
本文实例讲述了jQuery给多个不同元素添加class样式的方法。分享给大家供大家参考。具体分析如下: jQuery可以通过addClass()方法给多个不同的html元素同时添加相同的class &lt;!DOCTYPE html&gt;...
2017-03-22
DOM之通俗易懂讲解
DOM 是所有前端开发每天打交道的东西,但是随着 jQuery 等库的出现,大大简化了 DOM 操作,导致大家慢慢的 “遗忘” 了它的本来面貌。不过,要想深入学习前端知识,对 DOM 的了解是不可或缺的,所以本文力图系统的讲解下 DOM 的...
2016-01-13
Javascript实现数组中的元素上下移动
交换数组可以实现元素上下移动了,这个效果我们在表格或以前排序算法中都会用到,下面来看一个JavaScript下实现交换数组元素上下移动例子 在写项目的时候,要实现一个数组记录上下移动的示例。写起来也没有没麻烦,无非是交换数组元素。最终实现代...
2017-05-02
js获取数组的最后一个元素
在js里面如何获取一个数组的最后一个元素呢?这里总结了两种方法,有需要的朋友可以看看。 (1)js内置pop方法 pop() 方法用于删除并返回数组的最后一个元素,注意这里在获取了数组的最后一个元素的同时也将原数组的最后一个元素给删除了。如...
2017-03-22
jsdom 中文文档(纯翻译)
jsdom是一个纯粹由 javascript 实现的一系列 web标准,特别是 WHATWG 组织制定的DOM和 HTML 标准,用于在 nodejs 中使用。大体上来说,该项目的目标是模拟足够的Web浏览器子集,以便用于测试和挖掘真实世界...
2018-05-14
好消息!好消息!饿了么ElementUI用户的福音——ElementUIVerify!
如果你受够了饿了么ElementUI原生的校验方式,那就来试试它吧! 前言 饿了么ElementUI虽好,但表单校验的体验不够理想 如果说产品开发要讲究用户体验,那插件开发也要讲究开发体验,而好的开发体验,要靠好的api设计来保障 本人专注...
2017-12-24
[翻译]基于Webpack4使用懒加载分离打包React代码
原文地址:https://engineering.innovid.com/code-splitting-using-lazy-loading-with-react-redux-typescript-and-webpack-4-3ec601...
2018-03-11
回到顶部