Reactjs源码走读 --- setState时触发render的过程

在调用setState时,react会帮我们去更新DOM,重新去render一次,这个过程是怎么实现的呢?了解它就能很好的帮助我们理解为什么有时我们通过setState改变了一个变量的值,而再次使用时还是原来的值。
先来看个例子。

一个点击事件中调用setState

import React from 'react';

export default class RIndex extends React.Component {
    state = {
        text1: 1
    }

    constructor(props) {
        super(props);
        this.change = this.change.bind(this);
    }

    change() {
        this.setState({
            text1: this.state.text1 + 1
        });
        debugger;
    }

    render() {
        return (
            <div>
                <h1>This is RIndex</h1>
                <h3>{this.state.text1}</h3>
                <button onClick={this.change}>change</button>
            </div>
        );
    }
}

我们调用setState时,使用的是this,而这个this是当前react组件,当前react组件继承自React.Componet,所以setState应该是React.Component中定义的方法,找到源码中的React.js文件,一步步找下去会发现,setState的调用路径如下:

/** ====== React.js ====== */
...
Component: ReactBaseClasses.Component
...
/** ====== ReactBaseClasses ====== */
...
ReactComponent.prototype.setState = function(...) {
  ...
  // this.updater是ReactUpdateQueue,找到这个对应关系很简单,只需要全局搜索enqueueSetState方法,
  // 但要弄明白是在哪里赋值的,那就需要花很长时间了
  this.updater.enqueueSetState(this, partialState);
  ...
}
...
/** ====== ReactUpdateQueue ======*/
...
function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}
...
enqueueSetState: function (publicInstance, partialState) {
  enqueueUpdate(internalInstance);
}
...
/** ====== ReactUpdates ======*/
...
function enqueueUpdate(component) {}
...

查看调用路径中的方法,会发现,这个路径中,仅仅是将入参中的state放到一个实例的属性中,以及将当前组件放到ReactUpdates 的全局变量dirtyComponents中,并没有发现真正是在哪里去触发更新DOM的操作。回想整个流程,我们的setState是放在事件回调中的,那么事件触发时会不会做了什么事呢?

关注事件触发代码

找到事件触发的代码

/** ReactEventListener */
dispatchEvent: function (topLevelType, nativeEvent) {
  ...
  ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  ...
}

发现在事件触发的方法中,有一个ReactUpdates.batchedUpdates方法的调用,看名字应该和更新DOM有关系,查看该方法的调用链路

/** ====== ReactUpdates ======*/
function batchedUpdates(callback, a, b, c, d, e) {
  ensureInjected();
  return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
/** ====== ReactDefaultBatchingStrategy ======*/
batchedUpdates: function (callback, a, b, c, d, e) {
  // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
}

其中batchingStrategy是在react组件初始化的时候注册进来的,指向ReactDefaultBatchingStrategy对象。ReactDefaultBatchingStrategy对象的batchedUpdates方法,第一个参数callback就是事件触发时传入的handleTopLevelImpl,是真正的事件回调函数,从这个if语句看,应该是若已经update过了,就直接触发callback,若没有update,则调用transaction.perform。
Transaction是react中定义的事务,Transaction文件中有说明以及一个很形象的图:


Transaction

可以看出Transaction就是可以在你想要调用的方法之前及之后包入其他方法,且保证这些外层方法一定会执行。
查看ReactDefaultBatchingStrateg中的transaction的实现

/** ====== ReactDefaultBatchingStrategy ======*/
...
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// 传入两个wrapper生成transaction
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}
_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

可以看到ReactDefaultBatchingStrategy中的transaction包了两个wrapper,第一个wrapper的close中传入了ReactUpdates.flushBatchedUpdates,也就是在事件触发后,会调用ReactUpdates.flushBatchedUpdates方法,而flushBatchedUpdates后续的代码才是正直触发了更新DOM的操作。
注:可以好好理解一下Transcation,react源码中大量使用了它。

总结

完整的调用流程为:事件触发 ---> 包装一个事务 ---> 事务在触发事件回调之后执行更新DOM操作。也就是若我们在事件回调中多次调用了setState,实际react是先把这些setState里设置的值全部放到当前组件的_pendingStateQueue对象中,最后再一次调用更新DOM的方法的。
因此,当我们在一个事件回调函数中写了两次setState时,实际只执行了一次更新DOM的操作。如下代码的结果也就不难理解了

state = {
  count: 0
}
// 事件执行一次后,state中的count为1
change() {
  // this.state.count在这里为0
  this.setState({count: this.state.count + 1);
  // this.state.count在这里也为0
  this.setState({count: this.state.count + 1);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容