-
Notifications
You must be signed in to change notification settings - Fork 4
Description
通过 redux 源码学习我们知道了,redux 创建的 store 我们可以通过 store. getState()
来获取当前状态,通过 store.dispatch(action)
来更新状态,通过 store.subscribe(listener)
来注册一个当 dispatch 之后会被调用的监听函数。
而 react-redux 其实就是通过 store.subscribe(listener)
来把 redux 应用到 react 中的。
Provider, useStore, useDispatch
在 react 中使用时,我们一般会如下去配置 Provider ,传入一个 store 值,然后把我们的 App 包裹起来,这样在 App 任意组件中我们都可以使用 useStore 和 useDispatch 了。
<Provider store={store}>
<App />
</Provider>
其实通过 useStore 拿到的值就是传给 Provider 的 store ,通过 useDispatch 拿到的值就是 store.dispatch 。
这背后的实现就是用的 react 提供的 context 功能。相关源码如下。
首先创建一个 context ,然后其它地方需要的时候就引用这个 context 即可。
在 Provider 里就有使用上面创建的 context 。
下面这个 hook 调用时的返回值就是上面传给 Context.Provider 的 value,也就是 contextValue ,也就是 {store, subscription}
。
所以 useStore 的实现就很简单啦,就是去调用这个 hook 就好了。
而 useDispatch 的返回值就是 store.dispatch 。
所以简单点理解,Provider, useStore, useDispatch 就是使用的 react 的 context 来存储 store 和获取 store 。
涉及到需要全局共享数据的地方都会很容易见到 context 的身影,比如 antd 的 ConfigProvider 。
useSelector
首先要去看看好一下 Subscription 这个类的实现,在 Provider 和 useSelector 中都有用到。我把源码加了一些注释,如下。
import { getBatch } from './batch'
// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendants
const nullListeners = { notify() {} }
function createListenerCollection() {
const batch = getBatch()
let first = null
let last = null
return {
clear() {
first = null
last = null
},
// 执行所有注册的监听器
// 注意这里用到了 batch ,而这个 batch 默认会被设为 react 的 unstable_batchedUpdates ,看起来是用来做优化的,react 的源码看不懂 =.=
notify() {
batch(() => {
let listener = first
while (listener) {
listener.callback()
listener = listener.next
}
})
},
get() {
let listeners = []
let listener = first
while (listener) {
listeners.push(listener)
listener = listener.next
}
return listeners
},
// 注册一个监听器,内部实现是用的双向链表,相对于单向链表来说会更容易去删除一个节点
subscribe(callback) {
let isSubscribed = true
let listener = (last = {
callback,
next: null,
prev: last,
})
if (listener.prev) {
listener.prev.next = listener
} else {
first = listener
}
return function unsubscribe() {
if (!isSubscribed || first === null) return
isSubscribed = false
if (listener.next) {
listener.next.prev = listener.prev
} else {
last = listener.prev
}
if (listener.prev) {
listener.prev.next = listener.next
} else {
first = listener.next
}
}
},
}
}
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
// 添加一个监听器
addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}
// 触发所有的监听器
notifyNestedSubs() {
this.listeners.notify()
}
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
isSubscribed() {
return Boolean(this.unsubscribe)
}
// 这里面根据是否有 this.parentSub 执行了不同的代码
// 在 Provider 中创建的 Subscription 实例不会有 parentSub ,所以这里执行的是 this.store.subscribe(this.handleChangeWrapper)
// 而这个 this.store 就是 redux 的 store ,也就是每次 dispatch 后,都会执行 Provider 的 Subscription 实例的 handleChangeWrapper 方法
// 而在 useSelector 中创建 Subscription 实例时,会传 parentSub ,并且 parentSub 就是 Provider 中创建的 Subscription 实例
// 所以这里执行的 this.parentSub.addNestedSub(this.handleChangeWrapper) 的效果
// 是把 useSelector 的 Subscription 实例的 handleChangeWrapper 注册到 Provider 的 Subscription 实例的监听器列表中
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
this.listeners = createListenerCollection()
}
}
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
下面是 Provider 源码,在 Provider 中创建 Subscription 实例时只传入了 store ,所以在 trySubscribe 时会执行的是 this.store.subscribe(this.handleChangeWrapper)
。传入的这个 store 其实就是 redux 的 store ,同时给 onStateChange 赋值为 notifyNestedSubs 了。那么当 dispatch 后,就会去执行 this.handleChangeWrapper ,也就是 onStateChange , 也就是 notifyNestedSubs 了。这里 notifyNestedSubs 实际上执行的其实是所有 useSelector 中的 checkForUpdates ,再往下面会看到。
对于 useSelector 来说,是把 store 和 subscription 都传给了 useSelectorWithStoreAndSubscription ,然后创建 Subscription 实例时是有 contextSub 的,所以 trySubscribe 时会执行的是 this.parentSub.addNestedSub(this.handleChangeWrapper)
,this.parentSub 就是 Provider 的 Subscription 实例,那也就是把 this.handleChangeWrapper 注册到 Provider 的 Subscription 实例的 listeners 中了。
在 useSelector 中 onStateChange 被赋值为 checkForUpdates 了,这个函数是在 dispatch 后,Provider 的 Subscription 实例执行所有注册的 listeners 然后就会执行到 checkForUpdates 。
以上就是 Provider 和 useSelector 结合 store.subscribe 的一个大概工作流程。
forceRender
在 useSelector 中我们可以看到这样一行代码: const [, forceRender] = useReducer((s) => s + 1, 0)
。还是挺有意思的,当你希望自定义 hook 重新执行时,就调用一下 forceRender 。
useIsomorphicLayoutEffect
在 Provider 和 useSelector 中我们都有看到 useIsomorphicLayoutEffect ,它其实就是浏览器环境就等同于 useLayoutEffect ,服务端就等同于 useEffect 。相关源码如下。
import { useEffect, useLayoutEffect } from 'react'
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
? useLayoutEffect
: useEffect
为什么要去尽量使用 useLayoutEffect 呢? useLayoutEffect 和 useEffect 有哪些区别呢?
Hooks API Reference – React 文档中有写:
useLayoutEffect: The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
Prefer the standard useEffect when possible to avoid blocking visual updates.
简单点说就是 useLayoutEffect 会比 useEffect 先执行,并且 useLayoutEffect 中代码执行完,包括对 state 的更新执行完之后,才会把 state 变化更新到屏幕上去。
可以去 这篇文章 去更直观感受下区别。
至于说 Provider 和 useSelector 中为什么要去尽量使用 useLayoutEffect ,猜测是某些不太常见的场景会有 bug 所以最后采用了 useLayoutEffect ?但是我没找到具体是啥场景 …
如果要强行解释的话,我只能说使用 useLayoutEffect 可以使得 trySubscribe 更早的执行。比如如果说在 useSelector 中是使用的 useEffect ,那么下面代码就会导致 dispatch 时 useSelector 中 checkForUpdates 还没注册:
useEffect(() => {
dispatch(xxxx)
}, [])
const xxx = useSelector(xx)
呃 … 可能以后遇到更复杂的场景会更理解一些这里对 useLayoutEffect 的应用。