最通俗易懂的Mobx批量更新

590 阅读6分钟

一、前言

批量更新机制应该是前端框架中一个老生常谈的问题了,这些知识也是面试官比较喜欢问的,那么在不同的技术框架背景下,处理批量更新的手段也是各不相同,今天我们主要来探讨一下 Mobx 的批量更新机制,在学习本章之前需要对 Mobx 的响应式机制有所了解,具体可阅读我这篇文章 最通俗易懂的Mobx响应式机制

二、原理介绍

下面通过这个案例进行介绍:

class Store {
    count = 1;
    name = 'xl';
    age = 12;
    batchCount = true;
    change() {
        this.count = 2  
        this.count = 3 
        this.count = 4 
        this.name = 'xl1' 
        this.age = 13 
    }
    batchCount = false;
    constructor() {
        makeObservable(this, {
            count: observable,
            name: observable,
            age: observable,
            change: action
        })
        autorun(() => {
            console.log(this.count)
            console.log(this.name)
            console.log(this.age)
        })
    }
}

const store = new Store();

store.change();

在这个例子中,我们创建可以一个副作用(reaction),他会监听 count、name 以及 age 的变化,一旦这些值发生变化该富足用就会自动执行。同时我们也定义了一个 change 方法,在这个方法中我们多次更新了 count 的值,同时也更新了 name 和 age,当我们执行 change 方法时最终副作用当然只会执行一次,下面我会一步步进行讲解。

1、startBatch和endBatch

在介绍批量更新的原理之前我们先来了解两个比较重要的函数 startBatch 和 endBatch,顾名思义 startBatch 表示当前开始进行批量更新了,endBatch 表示批量更新已经结束了,是否处于批量更新其实就是通过一个全局变量 inBatch 来控制,当 inBatch>0 时表示当前处于批量更新。

这里要注意了 inBatch 是 number 类型的,学过 react 的同学可能了解过 React16 是怎么实现批量更新的,他是通过一个 boolare 类型的变量来控制的,当变量为 true 时表示当前正在进行批量更新,那为什么 mobx 要用一个 number 类型的变量来控制呢?下面会具体介绍的。

我们先看一下 startBatch 的实现:

function startBatch() {
  // 将inBatch执行++操作
  globalState.inBatch++;
}

这个方法很简单就是执行 inBatch++,下面看下 endBatch 函数。

function endBatch() {
     // 执行inBatch--操作
     if (--globalState.inBatch === 0) {
        // 此时批量更新已经结束了,需要执行批量更新过程中收集到的所有的副作用了,因为在批零更新的过程中副作用是不会被执行的而是会被收集起来,在这里被统一执行。
        runReactions(); 
        // ...
      }
}

// runReactions
function runReactions() {
      // 如果当前处于批量更新则不执行当前的副作用
      if (globalState.inBatch > 0) {
        return;
      }
      // 在批量更新或者普通更新的过程中所有的副作用都会被存放到pendingReactions上,注意一个副作用只会入队一次
      var allReactions = globalState.pendingReactions;
      var remainingReactions = allReactions.splice(0);
      // 遍历所有的副作用,调用他们的runReaction_方法
      for (var i = 0, l = remainingReactions.length; i < l; i++) {
        remainingReactions[i].runReaction_();
      }
}

看到这里大家应该对批量更新因该有一定的了解了,下面来解答几个问题:

1、mobx 为什么要用一个 number 类型的变量来处理批量更新? 我们来看一下这个例子并简单来模拟一下流程:

const pendingReactions = []
let inBatch = 0;
change() {
    //进入action方法需要进行批量更新
    // inBatch++;
    
    // 触发count的setter方法需要进行批量更新并将依赖count的所有副作用添加到对列中
    // inBatch++
    //pendingReactions.push(reactions1)
    this.count = 2  
    // count更新完需要结束批量更新,并执行队列中的副作用但是此时inBatch>0表示还在批量更新的流程中,所以暂时还不能执行
    //inBatch--
    // if(inBatch === 0){runReactions()}
    // 后面的流程都一样
    this.count = 3 
    this.count = 4 
    this.name = 'xl1' 
    this.age = 13 
    
    // action方法中的批量更新已经结束需要通知所有的副作用
    // inBatch-- 此时inBatch=0
    // if(inBatch === 0){runReactions()}
}

这样就比较明了了。

2、为什么把所有要执行的副作用都要托管到 pendingReactions 数组中? 这也是为了方便管理,比如我们直接执行一次 this.count = 2,那么队列中将会保存依赖 count 的所有副作用,当我们执行 change 方法时,pendingReactions 就会保存 依赖 count、name 和 age 的所有的副作用。

2、mobx是怎么处理注册的action方法?

在上面这个例子中我们将 change 方法标志为 action,我们来看一下 mobx 处理的结果:

image.png

我们看到 change 方法居然变成了一个叫 res 的方法,别急我们看到 res 方法中最终是调用了 executeAction 方法的,我们断点调试一下看看这个函数接收的各个参数是什么:

image.png 我们看到参数 fn 其实就是我们定义的 change 方法,下面就来看下在 executeAction 中是怎么处理 fn 方法的。

  // 通知mobx开始执行action方法了,在这个方法中会执行startBatch方法
  var runInfo = _startAction(actionName, canRunAsDerivation, scope, args);
  try {
    return fn.apply(scope, args); // 执行 change 方法
  } catch (err) {
    runInfo.error_ = err;
    throw err;
  } finally {
    // 这个方法中会执行endBatch方法
    _endAction(runInfo);
  }
}

我们看到在 executeAction 方法中只做了三件事,一是调用 startBatch 方法通知开始进入批量更新了,二是调用 fn 函数,在这个函数会触发属性的 setter 方法将属性的副作用保存到 pendingReactions 中,三是调用 endBatch 方法,执行 pendingReactions 中所有的副作用。

2、副作用的收集

最通俗易懂的Mobx响应式机制这篇文章中我们介绍过当我们触发了属性的 setter 方法时就会去通知依赖这个属性的所有的副作用重新执行,具体代码如下:

class ObservableValue{
    // ....
    
    // 更新value的方法
    setNewValue_(newValue: T) {
        const oldValue = this.value_
        this.value_ = newValue
        // 通知所有的副作用重新执行
        this.reportChanged()
        // ...
    }
}

其实最关键的就是 reportChanged 方法,这是批量更新的入口,其实实现也很简单:

 reportChanged() {
    startBatch()
    // 这个函数中会遍历当前属性的所有的副作用,最终会调用副作用的schedule_方法并将这个副作用push到pendingReactions中
    propagateChanged(this)
    endBatch()
}

//schedule_方法
 _proto.schedule_ = function schedule_() {
    if (!this.isScheduled_) {
      this.isScheduled_ = true;
      // this表示当前的副作用
      globalState.pendingReactions.push(this);
      runReactions();
    }
};

讲到这里其实批量更新的原理就介绍完了,其实思路挺简单的,首先是通过 inBatch 来表示当前是否是在批量更新的流程中,其次就是将所有的副作用全部保存在一个队列中,当批量更新结束就执行这个队列中的副作用。

三、思考

通过上面的介绍其实大家也会发现一个问题,mobx 的批量更新机制也是同步的,如果我改成下面这样的写法其实就不会再进行批量更新了:

class Store {
    count = 1;
    name = 'xl';
    age = 12;
    change() {
       setTimeout(() => {
            this.count = 2  
            this.count = 3 
            this.count = 4 
            this.name = 'xl1' 
            this.age = 13 
       }, 2000)
    }
    constructor() {
        makeObservable(this, {
            count: observable,
            name: observable,
            age: observable,
            change: action
        })
        autorun(() => {
            console.log(this.count)
            console.log(this.name)
            console.log(this.age)
        })
    }
}
const store = new Store();
store.change();

其实这和 React16 中的批量更新机制的问题是一样的,如果我们是异步更新属性就会跳出批量更新机制,但是在 mobx 中也很好解决,只要我们使用 action 将我们的函数包裹住就行了,如下所示:

change() {
   setTimeout(action(() => {
        this.count = 2  
        this.count = 3 
        this.count = 4 
        this.name = 'xl1' 
        this.age = 13 
   }), 2000)
}

而 React16 也为我们提供了一个 unstable_batchedupdates API,让我们手动去设置批量更新。