基于useSyncExternalStore封装一个自己的R
useSyncExternalStore基本使用
首先需要掌握一下useSyncExternalStore
这个api,这个api稍微有点理解成本,它可以监听外部状态的改变并且暴露自主UI渲染的能力给我们开发者,所谓外部状态,就是web应用中可访问的任意状态,而不局限于基于框架定义的组件状态。它共有三个参数,第三个参数是与ssr
相关的,这里主要说一下前两个:
- 第一个参数是一个回调函数,可简单理解为hook初次执行时调用一次,其它一些情况也可能触发其执行,详细了解去看下源码吧,这里不是我们需要的重点。并且提供给回调函数一个函数入参,调用即可触发UI的重新渲染,换句话说暴露给了我们通知React渲染UI的能力。
- 第二个参数也是回调函数,这个回调函数应该返回外部状态的一个快照,通俗点说就是返回监听的状态的值,包括第一个回调函数是否执行也是依据这个函数的返回值是否改变决定的。
- 返回状态快照的值,即第二个函数参数的返回值。
语言比较抽象,写个demo就好理解了:
function App() {
const scrollY = useSyncExternalStore((callback) => { window.addEventListener('scroll', callback) }, () => window.scrollY );
return (
<>
<div style={{height: '3000px'}}>{ scrollY }</div>
</>
)
}
export default App
解释一下,useSyncExternalStore
的第二个参数说明我们监听的状态是window.scrollY
,我们希望在window.scrollY
改变时触发视图的更新,所以第一个参数就非常好写了,即把更新视图的回调传给scroll
事件即可。
新特性useSyncExternalStore的意义何在
React useSyncExternalStore 一览这篇文章中详细阐述了“渲染撕裂”的原因,简单概括来说就是react中断渲染时,渲染所依赖的外部的一些状态发生改变,造成恢复渲染时本应一致的渲染效果出现不同(渲染依赖的状态改变)。
漫谈 React 系列(七) - 一起来学习 useSyncExternalStore这篇文章中也给出了渲染撕裂的动画展示,(但是我写了demo想复现“渲染撕裂”并没有成功,就不放代码了)不可否认的是从react原理层面确实是存在这样的问题的。
思路分析
既然以useSyncExternalStore
为“核心”去实现状态管理,我们需要一个全局对象,首先它要去存储我们要管理的状态,其次要有能力修改这个状态,但是修改时需要通知所有使用状态的地方(视图更新)。所以说对象还需要提供一个收集依赖的方法,或者直白点说收集更新视图的回调。
预期的Store
模型:
const storeDemo = {
state: xxx,// 存储状态
listeners: [], // 存储状态的“副作用函数”
getState() {}, // 返回state
setState() {}, // 修改状态并触发相关依赖
subscribe() {}, // (暴露给外部的)订阅状态的方法
}
实现基本状态模型
有了上面的分析,代码层面就比较简单了,我们以类的形式实现store
的定义:
export default class StateModel {
constructor(state) {
this.state = state;
this.listeners = new Set();
}
// (暴露给状态使用者的)订阅状态的方法
subscribe(listener) {
this.listeners.add(listener);
// 提供取消订阅的能力
return () => {
this.listeners.delete(listener);
}
}
// 修改数据
setState(payLoad) {
const newState = { ...this.state, ...payLoad };
this.state = newState;
this.listeners.forEach((l) => l()); // 触发所有“副作用”(试图更新)
}
// 获取数据快照
getState() {
return this.state;
}
}
状态模型结合react
仿照redux的api,useSelector
预期接收一个函数,我们提供给它state对象,用户可以用其返回state
中想要使用的状态;useDispatch
即返回一个修改store.state
的函数即可。
import { useSyncExternalStore } from "react"
const useModel = function (store) {
const state = useSyncExternalStore(store.subscribe.bind(store), store.getState.bind(store));
return {
useSelector(selector) {
return selector(state);
},
useDispatch() {
return (payLoad) => {
store.setState(payLoad);
}
}
}
}
export default useModel;
使用指南
src/store/counter.js
import StateModel from "../state-model";
const counterStore = new StateModel({count: 0});
export default counterStore;
src/App.jsx
import useModel from "./mode-with-react";
import counterStore from "./store/count";
import Child from "./components/Child";
function App() {
const {useSelector, useDispatch} = useModel(counterStore);
const count = useSelector((state) => {
return state.count;
});
const dispatch = useDispatch();
return (
<>
<button onClick={() => dispatch({count: count + 1})}>点我加一</button>
<div>{ count }</div>
<Child />
</>
)
}
export default App
src/components/Child.jsx
import useModel from "../../mode-with-react";
import counterStore from "../../store/count";
function Child() {
const {useSelector } = useModel(counterStore);
const count = useSelector((state) => {
return state.count;
});
return (
<>
<div >{ count }</div>
</>
)
}
export default Child;
父组件中修改状态,可以发现子组件同步修改,功能完成。