Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 源码解析: setState #19

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

React 源码解析: setState #19

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

React 源码解析: setState

这一节探讨 setState。

本节 demo 如下:

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 1
      }
    }
    clickButton = () => {
      this.setState({
        data: 2
      })
    }
    render () {
      return (
       [
         <div key="key">{this.state.data}</div>,
         <button key="btn" onClick={this.clickButton}>click me</button>
       ]
      );
    }
}
ReactDOM.render(
 <App />,
 document.getElementById("root")
)

setState 定义

组件是通过 React.Component 继承而来的,React.Component 定义在 ReactBaseClasses.js 文件中:

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

...

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

所以 setState 是通过继承而来的。在 setState 函数中调用 this.updater.enqueueSetState 函数,其中 this.updater 是实例化 Component 时初始化的。

以开始 demo 为例,App(Component)组件是经过以下步骤初始化的:

updateClassComponent(...)
constructClassInstance(...)
new ctor(props, context)

ctor 就是 App,所以 constructor() 实例 Component 时,参数为空。this.updater = ReactNoopUpdateQueue,但是 ReactNoopUpdateQueue 此时为空。其实在 constructClassInstance 函数中,在 new ctor(props, context) 之后,还调用了一个函数 adoptClassInstance:

function adoptClassInstance(workInProgress: Fiber, instance: any): void {
  instance.updater = classComponentUpdater;
  workInProgress.stateNode = instance;
  // The instance needs access to the fiber so that it can schedule updates
  setInstance(instance, workInProgress);
  if (__DEV__) {
    instance._reactInternalInstance = fakeInternalInstance;
  }
}

在该函数中 instance.updater = classComponentUpdater 对 updater 进行赋值。

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    flushPassiveEffects();
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {...}

    flushPassiveEffects();
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueForceUpdate(inst, callback) {...},
};

在 classComponentUpdater 对象中存在 enqueueSetState 函数。现在理清了 setState 代码。enqueueSetState 函数就是实际执行 setState 函数的地方。

setState 批量更新

我们知道 setState 有批量更新,比如:

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 1
      }
    }
    clickButton = () => {
      this.setState({
        data: this.state.data + 1
      });
      this.setState({
        data: this.state.data + 1
      });
      this.setState({
        data: this.state.data + 1
      });
    }
    render () {
      return (
       [
         <div key="div">{this.state.data}</div>,
         <button key="btn" onClick={this.clickButton}>click me</button>
       ]
      );
    }
}

我们点击按钮时,div 内容变为 1,也就是说 this.setState 只有最后一个起作用。

调用 setState 函数,最终会进入 enqueueSetState 函数,在 enqueueSetState 中会调用 enqueueUpdate(fiber, update)。enqueueUpdate 函数的作用是:创建 updateQueue 队列,通过 firstUpdate、lastUpdate、lastUpdate.next 去维护一个更新的队列。最后会在 performWork 中,相同的 key 会被覆盖,只会对最后一次的 setState 进行更新。

所以在 clickButton 函数中,前两个 setState 是无效的。

enqueueSetState 函数最后会调用 scheduleWork 更新。如果每次 setState 都让 scheduleWork 更新完成,那么每次 setState 都应该有效的,这和批量更新时矛盾的。React 针对这种情况作了处理:分为合成事件的 setState 和生命周期的 setState。

在合成事件中我们知道会把 isBatchingUpdates 置为 true,具体是在函数 interactiveUpdates$1 中:

function interactiveUpdates$1(fn, a, b) {
  
  ...
  
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return unstable_runWithPriority(unstable_UserBlockingPriority, function () {
      return fn(a, b);
    });
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

当调用 setState 进入到 requestWork 函数时,因为 isBatchingUpdates 为 true 而直接返回。在 interactiveUpdates$1 函数中 try finally 语句保证了执行完事件函数后,最后都要执行 performSyncWork 方法将变动部分重新渲染。

在 commit 流程章节中,已经介绍生命周期函数会在 commitRoot 中执行。

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 1
      }
    }
    
    componentDidMount() {
      this.setState({
        data: this.state.data + 1
      });
      this.setState({
        data: this.state.data + 1
      });
    }
    
    render () {
      return (
       [
         <div key="div">{this.state.data}</div>,
         <button key="btn">click me</button>
       ]
      );
    }
}

当执行 componentDidMount 时会调用 setState 函数,回到 enqueueSetState 函数中。在进入 requestWork函数时,因为 isRendering 为 true,直接返回。和合成事件中的 setState 一样,生命周期函数中的 setState 到最后肯定都要重新渲染。保证重新渲染地方是在 performWork 函数中的 while 语句:

function performWork(minExpirationTime, isYieldy) {
  // Keep working on roots until there's no more work, or until there's a higher
  // priority event.
  findHighestPriorityRoot();

  if (isYieldy) {
  
    ...
    
  } else {
    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && minExpirationTime <= nextFlushedExpirationTime) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
      findHighestPriorityRoot();
    }
  }

  ...
  
  finishRendering();
}

当执行完 commitRoot 函数后,会逐层返回到 performWork 函数中,此时 isRendering 已经重置为 false。然后调用 findHighestPriorityRoot 函数,在 findHighestPriorityRoot 函数作用是找出优先级最高的 root。

function findHighestPriorityRoot() {
  var highestPriorityWork = NoWork;
  var highestPriorityRoot = null;
  if (lastScheduledRoot !== null) {
    var previousScheduledRoot = lastScheduledRoot;
    var root = firstScheduledRoot;
    while (root !== null) {
      var remainingExpirationTime = root.expirationTime;
      if (remainingExpirationTime === NoWork) {
      
        ...
        
      } else {
        if (remainingExpirationTime > highestPriorityWork) {
          // Update the priority, if it's higher
          highestPriorityWork = remainingExpirationTime;
          highestPriorityRoot = root;
        }
        if (root === lastScheduledRoot) {
          break;
        }
        if (highestPriorityWork === Sync) {
          // Sync is highest priority by definition so
          // we can stop searching.
          break;
        }
        previousScheduledRoot = root;
        root = root.nextScheduledRoot;
      }
    }
  }

  nextFlushedRoot = highestPriorityRoot;
  nextFlushedExpirationTime = highestPriorityWork;
}

上面的例子会进入:

if (remainingExpirationTime > highestPriorityWork) {
  // Update the priority, if it's higher
  highestPriorityWork = remainingExpirationTime;
  highestPriorityRoot = root;
}

所以最后 nextFlushedRoot、nextFlushedExpirationTime 都有值,因此while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && minExpirationTime <= nextFlushedExpirationTime) 为 true,所以会再次渲染。

有一个问题是:remainingExpirationTime(root.expirationTime)什么时候赋值的。经过分析,是通过以下步骤得到值得:

setState(enqueueSetState) => computeExpirationForFiber => scheduleWork => markPendingPriorityLevel

setState 异步

如果熟悉 setState 批量更新,setState 异步也很好理解了。需要注意的是这里的异步是通过同步达到的(异步代表的是一种效果),在官网中有这么一段话:

setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

setState 是请求而不是执行更新的命令,所以存在异步。比如:

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 0,
        count: 0,
      }
    }

    clickButton = () => {
      this.setState({
        data: this.state.data + 1
      });
      
      console.log(this.state.data); // 0
    }

    render () {
      return (
        [
          <div key="div">{this.state.data}</div>,
          <button key="btn" onClick={this.clickButton}>click me</button>,
        ]
      )
    }
}

在 clickButton 函数中 this.state.data 函数输出 0 而非 1。setState 批量更新分析中已经知道,执行完 clickButton 函数之后才会更新,所以在 clickButton 是拿不到最新的 state。当然在 setState 是可以取得最新值的。

原生事件和 setTimeout 中的 setState

在原生事件和 setTimeout 中的 setState 是没有机制来保证批量更新和异步的,避免在原生事件和 setTimeout 中使用 setState。

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 0,
        count: 0,
      }
    }

    clickButton = () => {
    	// 每次 setState 都会生效
      this.setState({
        data: this.state.data + 1
      });
      this.setState({
        data: this.state.data + 1
      });

      console.log(this.state.data); // 2
    }

    componentDidMount() {
      document.getElementById('div').addEventListener('click', this.clickButton);
    }

    render () {
      return (
        <div id="div">{this.state.data}</div>
      )
    }
}

在原生事件中,setState 是没有批量更新和异步效果。

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 0,
        count: 0,
      }
    }

    clickButton = () => {
      setTimeout(() => {
      	// 每次 setState 都会生效
        this.setState({
          data: this.state.data + 1
        });
        this.setState({
          data: this.state.data + 1
        });
        
        console.log(this.state.data); // 2
      });
    }

    render () {
      return (
        [
          <div key="div">{this.state.data}</div>,
          <button key="btn" onClick={this.clickButton}>click me</button>,
        ]
      )
    }
}

在 setTimeout 中 setState 也会失去批量更新和异步效果,即使 setState 在合成事件或者生命周期函数中。setTimeout 这样的异步函数在 event loop 模型下,会在最后才能执行,此时 React 中确保 setState 批量更新和异步的机制早已失效。

需要我们注意的是:setState 在合成事件和生命周期函数中有异步的效果,在原生事件和 setTimeout 中是没有异步的。setState 异步是通过同步实现的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant