useSyncExternalStore是react18引入的一个新的hooks,用于订阅外部store,被订阅的这个外部store实际上是基于发布订阅模式创建的一个可更新状态的一个类或者一个单独的文件。如果对发布订阅模式不是很熟悉的,可以先去查看文章:详解"观察者"模式和"发布订阅"模式,基于该模式我们创建一个外部store,并引入组件中,使用useSyncExternalStore监听状态的改变,并同步更新组件。
创建外部store
首先我们先看官网对useSyncExternalStore参数的要求:
- subscribe 函数应当订阅该 store 并返回一个取消订阅的函数。
- getSnapshot 函数应当从该 store 读取数据的快照。
参数解析:
- subscribe:一个函数,接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变,它应当调用被提供的 callback。这会导致组件重新渲染。subscribe 函数会返回清除订阅的函数。
- getSnapshot:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。
创建store文件
先根据官网对参数的介绍创建一个store文件,根据要求文件应该有以下属性和方法:
- count:属性,该store的状态
- subscribers:订阅者列表,里面将来会存放订阅者传进来的callback函数,并在设置新的状态时,循环列表,调用所有的callback函数来通知订阅者
- setCount:用于修改状态(count)的值,并且循环执行subscribers中的callback方法,通知订阅者
- subscribe:用于订阅该状态,该函数需要传入一个callback函数,用于状态改变时调用,起到通知订阅者的作用。该函数还返回了一个取消订阅的函数,用于订阅者调用订阅函数时可以获得该取消订阅函数,在需要的时机将其取消订阅。
- getCount:用于获取状态的方法
type Subscriber = () => void
let count = 0
let subscribers: Subscriber[] = []
const setCount = (newCount: number) => {
count = newCount
subscribers.forEach((item) => {
item()
})
}
const subscribe = (callback: Subscriber) => {
subscribers.push(callback)
return () => {
subscribers = subscribers.filter((item) => item !== callback)
}
}
const getCount = () => {
return count
}
export {
setCount,
subscribe,
getCount
}
在组件中使用
我们创建好了这个store就可以引入组件使用了,根据useSyncExternalStore参数要求,将subscribe和getCount函数传入,代码如下,当点击SetCount按钮时,就可以实时获取到最新的count的值并更新到页面上了。
import React, { memo, useSyncExternalStore } from 'react'
import { setCount, subscribe, getCount } from './store'
function Demo06() {
const count = useSyncExternalStore(subscribe, getCount)
return (
<div>
<p>This is Demo04 Page {count}</p>
<button type="button" onClick={() => { setCount(count + 1) }}>SetCount</button>
</div>
)
}
export default memo(Demo06)
useSyncExternalStore函数原理剖析
我们在上面使用useSyncExternalStore函数实现了引入外部store,并且实现了当状态改变时,重新渲染组件并获取最新值。那我们还有疑惑就是,useSyncExternalStore内部是如何实现的呢。
其实这里最重要的一个东西就是subscribe的参数callback,当我们调用useSyncExternalStore函数时,react内部会自动创建一个callback函数并传入subscribe,然后store中的subscribers就会保存该callback,当count的值被改变时,该callback函数被调用,react在内部实现取消订阅,然后重新渲染组件。
组件重新渲染之后又会重新订阅,如此反复
自己实现useSyncExternalStore函数
根据上面的原理剖析,我们大概了解useSyncExternalStore的实现原理,那么我们也就可以自己实现一个useSyncExternalStore方法,并起到与useSyncExternalStore相同的作用,具体代码如下:
代码解析:
- 首先我们定义一个syncExternalStore方法,该方法与官方的useSyncExternalStore方法一样,接收两个参数,subscribeValue和getSnapshot
- 在该方法中我们定义一个callback方法进行强制重新渲染组件
- 我们定义一个unsubscribeRef,用于保存后面返回的取消定义函数
- 我们调用subscribeValue方法传入callback,并将返回的取消订阅函数传入unsubscribeRef中
- 最后函数返回getSnapshot()的返回结果
- 下面在调用syncExternalStore函数之前先取消定义上次的订阅,以保证订阅函数中的该组件的callback函数只有一个
import React, { memo, useCallback, useRef, useState } from 'react'
import { setCount, subscribe, getCount } from './store'
type commonFunc = () => void
type SubscribeFunc = (callback: commonFunc) => commonFunc
function Demo07() {
const [number, forceUpdate] = useState(0)
const unsubscribeRef = useRef<commonFunc>()
const handleForceUpdate = useCallback(() => {
forceUpdate(number + 1)
}, [number])
const syncExternalStore = (subscribeValue: SubscribeFunc, getSnapshot: () => number): number => {
const callback = () => {
handleForceUpdate()
}
unsubscribeRef.current = subscribeValue(callback)
return getSnapshot()
}
if (unsubscribeRef.current) {
unsubscribeRef.current()
}
const count = syncExternalStore(subscribe, getCount)
return (
<div>
<p>This is Demo04 Page {count}</p>
<button type="button" onClick={() => { setCount(count + 1) }}>SetCount</button>
</div>
)
}
export default memo(Demo07)
当我们运行代码点击按钮时,也可以实时获取最新的count的值并显示在页面上,这就是useSyncExternalStore函数实现的基本原理,当然react内部肯定做了更好的处理,我这里只是简单实现一下,便于大家理解。