redux源码-详解ensureCanMutateNextListeners函数

286 阅读3分钟

思考

1、createStore函数中存储listeners为什么需要nextListenerscurrentListeners两个数组?

2、ensureCanMutateNextListeners函数有什么意义?

分析

调用store.subscribe()进行初始化订阅时,subscribe()方法接收一个函数作为监听器(listeners),用于在每次dispatch后触发执行

1、在createStore函数中,首先会进行如下初始化:定义两个数组用于存储listeners

let currentListeners = []
let nextListeners = currentListeners

2、在观察下ensureCanMutateNextListeners函数:

发现这个函数只有简单的几行,首先判断nextListeners是否等于currentListeners(引用类型比较是否相等时,比较的是其存储变量的地址),如果两个数组相等(也就是其地址相等)则使用slice进行拷贝,使得两个数组不等,然后所有的listeners都会集中到nextListeners

 function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
 }

3、观察subscribe函数:

在每次订阅时候,调用ensureCanMutateNextListeners函数,并将listener记录在nextListeners数组中

function subscribe(listener) {
    // ...
    let isSubscribed = true
    /**
     * 新增订阅时,调用ensureCanMutateNextListeners函数确保可以改变nextListeners
     * 将新的listener添加到nextListeners数组中
     */
    ensureCanMutateNextListeners()            
    nextListeners.push(listener)                
​
    //返回一个取消订阅的函数unsubscribe
    return function unsubscribe() {            
        // ......
        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        //取消订阅,删除对应的listener
        nextListeners.splice(index, 1)   
    }
}

4、观察dispatch函数:

function dispatch(action) {
  // ...
  try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  }
  /**
   * 相当于nextListeners首先赋值给currentListeners
   * 取currentListeners作为disPatch之后要触发的listener
   */ 
  const listeners = (currentListeners = nextListeners)
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()            
  }
  return action
}

重点

以上是在完成disPatch后触发listener的正常场景,在了解完正常场景后,我们可以在redux源码中看到这样一段注释:

The subscriptions are snapshotted just before every dispatch() call.If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on the dispatch() that is currently in progress.However, the next dispatch() call, whether nested or not, will use a more recent snapshot of the subscription list.

简单解释下,subscriptions在每次触发dispatch之前都是一份快照(snapshotted)。如果在listener被调用期间进行新的订阅或者取消订阅,在本次的dispatch过程中是不会受到影响的。然而,在下一次dispatch时,无论是否嵌套调用,将都会使用最近的subscription快照。

按照这个规则,试想如果只有currentListeners数组存储listener会发生什么:

场景一
  • 调用createStore函数时初始化currentListeners为空数组
  • subscribe触发订阅向currentListeners数组中添加listener监听器,其中此listener中会再一次触发subscribe订阅,如下:
const unsubscribe = store.subscribe(() => {
  store.subscribe(() => {})
})
  • 触发dispatch时第一次调用listenerlistener中再一次触发订阅,订阅时再一次将此次的listener推入currentListeners数组中。即此时currentListeners数组长度为2,由于dispatch时相当于是循环遍历currentListeners数组,导致在本次dispatch时会继续执行新的listener。那么本次dispatch就受到了影响。违反了redux的设计思想,引发异常!
场景二
// 第一次订阅A  listenerA
const unsubscribeA = store.subscribe(() => {
  console.log('aaaa')
})
// 在第二次订阅时解绑A  listenerB
store.subscribe(() => unsubscribeA())
// 触发第三次订阅  listenerC
store.subscribe(() => {
  console.log('ccccc')
})
store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })
​

在这个Demo执行完后,currentListeners数组中存在三个listener:[listenerA, listenerB, listenerC]

调用dispatch,执行以下逻辑

const listeners = currentListeners
for (let i = 0; i < listeners.length; i++) {
  const listener = listeners[i]
  listener()            
}

当循环执行到i=1时,触发listenerB,在listenerB中解绑listenerA,此时我们的listenerscurrentListeners数组变为[listenerB, listenerC],只剩下两个元素

接下来继续执行循环i++,此时i=2。原本正常应该继续触发listenerC,可是此时数组第三项为undefined,进而跳过listenerC的执行,引发异常!

总结

为了解决以上问题,首先决定添加多一个数组nextListeners,但是这还不够,因为nextListenerscurrentListeners此时引用地址是相等的,所以借助ensureCanMutateNextListeners函数将两个数组隔离,使两个数组不能相互影响,即使在本次listener中触发或者取消订阅,都不会影响本次dispatch,在下一次dispatch时才会使用最近的subscription快照(nextListeners)!!!