资讯专栏INFORMATION COLUMN

React源码分析与实现(二):状态、属性更新 -> setState

BlackFlagBin / 1071人阅读

摘要:流程图大概如下的源码比较简单,而在执行更新的过程比较复杂。这段代码的核心就是调用,然后对老的属性和状态存一下,新的更新一下而已。最后是把执行推入的队列中,等待组件的更新。最后调用统一修改的属性。

</>复制代码

  1. 原文链接地址:https://github.com/Nealyang%EF%BC%9A%E7%8A%B6%E6%80%81%E3%80%81%E5%B1%9E%E6%80%A7%E6%9B%B4%E6%96%B0%20-%3E%20setState.md) 转载请注明出处
状态更新

</>复制代码

  1. 此次分析setState基于0.3版本,实现比较简单,后续会再分析目前使用的版本以及事务机制。

流程图大概如下

setState的源码比较简单,而在执行更新的过程比较复杂。我们直接跟着源码一点一点屡清楚。

ReactCompositeComponent.js

</>复制代码

  1. /**
  2. * Sets a subset of the state. Always use this or `replaceState` to mutate
  3. * state. You should treat `this.state` as immutable.
  4. *
  5. * There is no guarantee that `this.state` will be immediately updated, so
  6. * accessing `this.state` after calling this method may return the old value.
  7. *
  8. * @param {object} partialState Next partial state to be merged with state.
  9. * @final
  10. * @protected
  11. */
  12. setState: function(partialState) {
  13. // Merge with `_pendingState` if it exists, otherwise with existing state.
  14. this.replaceState(merge(this._pendingState || this.state, partialState));
  15. },

注释部分说的很明确,setState后我们不能够立即拿到我们设置的值。

而这段代码也非常简单,就是将我们传入的state和this._pendingState做一次merge,merge的代码在util.js下

</>复制代码

  1. var merge = function(one, two) {
  2. var result = {};
  3. mergeInto(result, one);
  4. mergeInto(result, two);
  5. return result;
  6. };
  7. function mergeInto(one, two) {
  8. checkMergeObjectArg(one);
  9. if (two != null) {
  10. checkMergeObjectArg(two);
  11. for (var key in two) {
  12. if (!two.hasOwnProperty(key)) {
  13. continue;
  14. }
  15. one[key] = two[key];
  16. }
  17. }
  18. }
  19. checkMergeObjectArgs: function(one, two) {
  20. mergeHelpers.checkMergeObjectArg(one);
  21. mergeHelpers.checkMergeObjectArg(two);
  22. },
  23. /**
  24. * @param {*} arg
  25. */
  26. checkMergeObjectArg: function(arg) {
  27. throwIf(isTerminal(arg) || Array.isArray(arg), ERRORS.MERGE_CORE_FAILURE);
  28. },
  29. var isTerminal = function(o) {
  30. return typeof o !== "object" || o === null;
  31. };
  32. var throwIf = function(condition, err) {
  33. if (condition) {
  34. throw new Error(err);
  35. }
  36. };

诊断代码的逻辑非常简单,其实功能就是Object.assign() ,但是从上面代码我们可以看出react源码中的function大多都具有小而巧的特点。

最终,将merge后的结果传递给replaceState

</>复制代码

  1. replaceState: function(completeState) {
  2. var compositeLifeCycleState = this._compositeLifeCycleState;
  3. invariant(
  4. this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
  5. compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
  6. "replaceState(...): Can only update a mounted (or mounting) component."
  7. );
  8. invariant(
  9. compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
  10. compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
  11. "replaceState(...): Cannot update while unmounting component or during " +
  12. "an existing state transition (such as within `render`)."
  13. );
  14. this._pendingState = completeState;
  15. // Do not trigger a state transition if we are in the middle of mounting or
  16. // receiving props because both of those will already be doing this.
  17. if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
  18. compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
  19. this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
  20. var nextState = this._pendingState;
  21. this._pendingState = null;
  22. var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
  23. transaction.perform(
  24. this._receivePropsAndState,
  25. this,
  26. this.props,
  27. nextState,
  28. transaction
  29. );
  30. ReactComponent.ReactReconcileTransaction.release(transaction);
  31. this._compositeLifeCycleState = null;
  32. }
  33. },

撇开50% 判断warning代码不说,从上面代码我们可以看出,只有在componsiteLifeState不等于mounting和receiving_props 时,才会调用 _receivePropsAndState函数来更新组件。

我们可以演示下:

</>复制代码

  1. var ExampleApplication = React.createClass({
  2. getInitialState() {
  3. return {}
  4. },
  5. componentWillMount() {
  6. this.setState({
  7. a: 1,
  8. })
  9. console.log("componentWillMount", this.state.a)
  10. this.setState({
  11. a: 2,
  12. })
  13. console.log("componentWillMount", this.state.a)
  14. this.setState({
  15. a: 3,
  16. })
  17. console.log("componentWillMount", this.state.a)
  18. setTimeout(() => console.log("a5"), 0)
  19. setTimeout(() => console.log(this.state.a,"componentWillMount"))
  20. Promise.resolve("a4").then(console.log)
  21. },
  22. componentDidMount() {
  23. this.setState({
  24. a: 4,
  25. })
  26. console.log("componentDidMount", this.state.a)
  27. this.setState({
  28. a: 5,
  29. })
  30. console.log("componentDidMount", this.state.a)
  31. this.setState({
  32. a: 6,
  33. })
  34. console.log("componentDidMount", this.state.a)
  35. },
  36. render: function () {
  37. var elapsed = Math.round(this.props.elapsed / 100);
  38. var seconds = elapsed / 10 + (elapsed % 10 ? "" : ".0");
  39. var message =
  40. "React has been successfully running for " + seconds + " seconds.";
  41. return React.DOM.p(null, message);
  42. }
  43. });

所以以上结果我们可以看出,在componentWillMount生命周期内setState后this.state不会改变,在componentDidMount是正常的。因为在上一篇文章中我们也有说到,在mountComponent过程中,会把compositeLifeCycleState设置为MOUNTING状态,在这个过程中,是不会执行receivePropsAndState的,所以this.state也就不会更新,同理,在receivePropsAndState的过程中,会把compositeLifeCycleState置成RECEIVING_PROPS状态,也不会执行state更新以及render执行,在updateComponent过程中又执行了mountComponent函数,mountComponent函数调用了render函数。

而在现在我们使用16或者15版本中,我们发现:

</>复制代码

  1. componentDidMount() {
  2. this.setState({val: this.state.val + 1});
  3. console.log(this.state.val); // 第 1 次 log
  4. this.setState({val: this.state.val + 1});
  5. console.log(this.state.val); // 第 2 次 log
  6. setTimeout(() => {
  7. this.setState({val: this.state.val + 1});
  8. console.log(this.state.val); // 第 3 次 log
  9. this.setState({val: this.state.val + 1});
  10. console.log(this.state.val); // 第 4 次 log
  11. }, 0);
  12. }

最后打印的结果为:0,0,2,3

为什么有这样呢?其实源于源码中的这段代码:

</>复制代码

  1. function enqueueUpdate(component) {
  2. ensureInjected();
  3. // Various parts of our code (such as ReactCompositeComponent"s
  4. // _renderValidatedComponent) assume that calls to render aren"t nested;
  5. // verify that that"s the case. (This is called by each top-level update
  6. // function, like setProps, setState, forceUpdate, etc.; creation and
  7. // destruction of top-level components is guarded in ReactMount.)
  8. if (!batchingStrategy.isBatchingUpdates) {
  9. batchingStrategy.batchedUpdates(enqueueUpdate, component);
  10. return;
  11. }
  12. dirtyComponents.push(component);
  13. }

因为这里涉及到事务的概念、批量更新以及benchUpdate等,在我们目前分析的版本中还未迭代上去,后面我们会跟着版本升级慢慢说道。

属性更新

首先我们知道,属性的更新必然是由于state的更新,所以其实组件属性的更新流程就是setState执行更新的延续,换句话说,也就是setState才能出发组件属性的更新,源码里就是我在处理state更新的时候,顺带检测了属性的更新。所以这段源码的开始,还是从setState中看

</>复制代码

  1. _receivePropsAndState: function(nextProps, nextState, transaction) {
  2. if (!this.shouldComponentUpdate ||
  3. this.shouldComponentUpdate(nextProps, nextState)) {
  4. this._performComponentUpdate(nextProps, nextState, transaction);
  5. } else {
  6. this.props = nextProps;
  7. this.state = nextState;
  8. }
  9. },

代码非常的简单,一句话解释:当shouldComponentUpdate为true时,则执行更新操作。

</>复制代码

  1. _performComponentUpdate: function(nextProps, nextState, transaction) {
  2. var prevProps = this.props;
  3. var prevState = this.state;
  4. if (this.componentWillUpdate) {
  5. this.componentWillUpdate(nextProps, nextState, transaction);
  6. }
  7. this.props = nextProps;
  8. this.state = nextState;
  9. this.updateComponent(transaction);
  10. if (this.componentDidUpdate) {
  11. transaction.getReactOnDOMReady().enqueue(
  12. this,
  13. this.componentDidUpdate.bind(this, prevProps, prevState)
  14. );
  15. }
  16. },

这段代码的核心就是调用this.updateComponent,然后对老的属性和状态存一下,新的更新一下而已。如果存在componentWillUpdate就执行一下,然后走更新流程。最后是把执行componentDidUpdate推入getReactOnDOMReady的队列中,等待组件的更新。

</>复制代码

  1. _renderValidatedComponent: function() {
  2. ReactCurrentOwner.current = this;
  3. var renderedComponent = this.render();
  4. ReactCurrentOwner.current = null;
  5. return renderedComponent;
  6. },
  7. ...
  8. ...
  9. updateComponent: function(transaction) {
  10. var currentComponent = this._renderedComponent;
  11. var nextComponent = this._renderValidatedComponent();
  12. if (currentComponent.constructor === nextComponent.constructor) {
  13. if (!nextComponent.props.isStatic) {
  14. currentComponent.receiveProps(nextComponent.props, transaction);
  15. }
  16. } else {
  17. var thisID = this._rootNodeID;
  18. var currentComponentID = currentComponent._rootNodeID;
  19. currentComponent.unmountComponent();
  20. var nextMarkup = nextComponent.mountComponent(thisID, transaction);
  21. ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
  22. currentComponentID,
  23. nextMarkup
  24. );
  25. this._renderedComponent = nextComponent;
  26. }
  27. },

这里我们直接看updateComponent更新流程,首先获取当前render函数的组件,然后获取下一次render函数的组件,_renderValidatedComponent就是获取下一次的render组件。 通过Constructor来判断组件是否相同,如果相同且组件为非静态,则更新组件的属性,否则卸载当前组件,然后重新mount下一个render组件并且直接暴力更新。

接着会调用render组件的receiveProps方法,其实一开始这个地方我也是非常困惑的,this指向傻傻分不清楚,后来经过各种查阅资料知道,它其实是一个多态方法,如果是复合组件,则执行ReactCompositeComponent.receiveProps,如果是原生组件,则执行ReactNativeComponent.receiveProps。源码分别如下:

</>复制代码

  1. receiveProps: function(nextProps, transaction) {
  2. if (this.constructor.propDeclarations) {
  3. this._assertValidProps(nextProps);
  4. }
  5. ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
  6. this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
  7. if (this.componentWillReceiveProps) {
  8. this.componentWillReceiveProps(nextProps, transaction);
  9. }
  10. this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
  11. var nextState = this._pendingState || this.state;
  12. this._pendingState = null;
  13. this._receivePropsAndState(nextProps, nextState, transaction);
  14. this._compositeLifeCycleState = null;
  15. },

有人可能注意到这里的this._receivePropsAndState函数,这不是刚才调用过么?怎么又调用一遍?没错,调用这个的this已经是currentComponent了,并不是上一个this。currentComponent是当前组件的render组件,也就是当前组件的子组件。子组件同样也可能是复合组件或者原生组件。正式通过这种多态的方式,递归的解析每级嵌套组件。最终完成从当前组件到下面的所有叶子节点的树更新。

其实话说回来,compositeComponent最终还是会遍历递归到解析原生组件,通过我们整体浏览下ReactNativeComponent.js代码可以看出。

我们先从 receiveProps方法开始看

</>复制代码

  1. receiveProps: function(nextProps, transaction) {
  2. assertValidProps(nextProps);
  3. ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
  4. this._updateDOMProperties(nextProps);
  5. this._updateDOMChildren(nextProps, transaction);
  6. this.props = nextProps;
  7. },
  8. function assertValidProps(props) {
  9. if (!props) {
  10. return;
  11. }
  12. var hasChildren = props.children != null ? 1 : 0;
  13. var hasContent = props.content != null ? 1 : 0;
  14. var hasInnerHTML = props.dangerouslySetInnerHTML != null ? 1 : 0;
  15. }

删除安全警告和注释其实代码非常简答,首先assertValidProps就是校验props是否合法的,更新属性的方法就是_updateDOMProperties

</>复制代码

  1. _updateDOMProperties: function(nextProps) {
  2. var lastProps = this.props;
  3. for (var propKey in nextProps) {
  4. var nextProp = nextProps[propKey];
  5. var lastProp = lastProps[propKey];
  6. //判断新老属性中的值是否相等
  7. if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
  8. continue;
  9. }
  10. //如果是style样式,遍历新style,如果去旧style不相同,则把变化的存入styleUpdates对象中。最后调用 updateStylesByID 统一修改dom的style属性。
  11. if (propKey === STYLE) {
  12. if (nextProp) {
  13. nextProp = nextProps.style = merge(nextProp);
  14. }
  15. var styleUpdates;
  16. for (var styleName in nextProp) {
  17. if (!nextProp.hasOwnProperty(styleName)) {
  18. continue;
  19. }
  20. if (!lastProp || lastProp[styleName] !== nextProp[styleName]) {
  21. if (!styleUpdates) {
  22. styleUpdates = {};
  23. }
  24. styleUpdates[styleName] = nextProp[styleName];
  25. }
  26. }
  27. if (styleUpdates) {
  28. ReactComponent.DOMIDOperations.updateStylesByID(
  29. this._rootNodeID,
  30. styleUpdates
  31. );
  32. }
  33. } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
  34. var lastHtml = lastProp && lastProp.__html;
  35. var nextHtml = nextProp && nextProp.__html;
  36. if (lastHtml !== nextHtml) {
  37. ReactComponent.DOMIDOperations.updateInnerHTMLByID(//注意这里是innerHtml,所以dangerouslyInnerHTML会展示正常的HTML
  38. this._rootNodeID,
  39. nextProp
  40. );
  41. }
  42. } else if (propKey === CONTENT) {
  43. ReactComponent.DOMIDOperations.updateTextContentByID(//这里是innerText,所以content与children原封不动的把HTML代码打印到页面上
  44. this._rootNodeID,
  45. "" + nextProp
  46. );
  47. } else if (registrationNames[propKey]) {
  48. putListener(this._rootNodeID, propKey, nextProp);
  49. } else {
  50. ReactComponent.DOMIDOperations.updatePropertyByID(
  51. this._rootNodeID,
  52. propKey,
  53. nextProp
  54. );
  55. }
  56. }
  57. },

这里面方法没有太多的hack技巧,非常的简单直白,不多带带拧出来说,我直接写到注释里面了。

最后直接更新组件的属性

</>复制代码

  1. setValueForProperty: function(node, name, value) {
  2. if (DOMProperty.isStandardName[name]) {
  3. var mutationMethod = DOMProperty.getMutationMethod[name];
  4. if (mutationMethod) {
  5. mutationMethod(node, value);
  6. } else if (DOMProperty.mustUseAttribute[name]) {
  7. if (DOMProperty.hasBooleanValue[name] && !value) {
  8. node.removeAttribute(DOMProperty.getAttributeName[name]);
  9. } else {
  10. node.setAttribute(DOMProperty.getAttributeName[name], value);
  11. }
  12. } else {
  13. var propName = DOMProperty.getPropertyName[name];
  14. if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) {
  15. node[propName] = value;
  16. }
  17. }
  18. } else if (DOMProperty.isCustomAttribute(name)) {
  19. node.setAttribute(name, value);
  20. }
  21. }

整体属性更新的流程图大概如下:

结束语

通篇读完,是不是有种

react源码中包含很多的点的知识,比如我们之前说的VDOM、包括后面要去学习dom-diff、事务、缓存等等,都是一个点,而但从一个点来切入难免有的会有些枯燥没卵用,别急别急~

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

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

相关文章

  • 如何用ahooks控制时机的hook?

      本篇主要和大家沟通关于ahooks ,我们可以理解为加深对 React hooks 的了解。  我们先说下关于抽象自定义 hooks。构建属于自己的 React hooks 工具库。  其实我们应该培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。  注:本系列对 ahooks 的源码解析是基于v3.3.13。  现在就进入主题用ahooks 来封装 React要注意的时机?  Fun...

    3403771864 评论0 收藏0
  • React源码解析之React.Component()/PureComponent()

    摘要:只涉及了,其他均没有自己实现。这种组件的复用性是最强的。所以会新建,只继承的原型,不包括,以此来节省内存。 showImg(https://segmentfault.com/img/remote/1460000019783989); 一、React.Component() GitHub:https://github.com/AttackXiaoJinJin/reactExplain/...

    Cristalven 评论0 收藏0
  • 精读《React PowerPlug 源码

    摘要:今天我们就来解读一下的源码。比较有意思,将定时器以方式提供出来,并且提供了方法。实现方式是,在组件内部维护一个定时器,实现了组件更新销毁时的计时器更新销毁操作,可以认为这种定时器的生命周期绑定了组件的生命周期,不用担心销毁和更新的问题。 1. 引言 React PowerPlug 是利用 render props 进行更好状态管理的工具库。 React 项目中,一般一个文件就是一个类,...

    teren 评论0 收藏0
  • React 源码深度解读(九):单个元素更新

    摘要:作为声明式的框架,接管了所有页面更新相关的操作。是用于内部操作的实例,这里将它的初始化为空数组并插入一个新的。连续次后,期望的结果应该是。原因很简单,因为次的时候,取到的都是在完后不会同步更新。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过程...

    kid143 评论0 收藏0
  • 前端路由实现 react-router 源码分析

    摘要:回调函数将在更新时触发,回调中的起到了新的的作用。注册回调在中使用注册的回调函数,最终放在模块的回调函数数组中。 原文地址:https://github.com/joeyguo/blog/issues/2 在单页应用上,前端路由并不陌生。很多前端框架也会有独立开发或推荐配套使用的路由系统。那么,当我们在谈前端路由的时候,还可以谈些什么?本文将简要分析并实现一个的前端路由,并对 reac...

    ISherry 评论0 收藏0

发表评论

0条评论

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