一、前言
本文来自广东靓仔的同事--杰哥的投稿,千呼万唤始出来~
相信大家比较熟悉的React状态管理方案有以下几个:
1. dva,umi巨无霸开箱即送,公司项目大爱
2. redux,典中典,当代年轻人不会这么好精力搞了,个人认为缺点比优点多
3.mobx,基本不存在,不如直接去玩vue
4. props传递,一个顶部组件管理value和setValue,作为props下沉到所需子孙组件,层级太深,会造成props drilling(直译不知什么意思,以前网上看的这个词,应该是很深的意思),而且没必要的中介组件也得接力传递,莫名其妙
5. context,被同一个上下文包裹下的组件,都可以互相传递数据。他是最有React的味道,因为他就是React内置的方法😆。但是我相信,很少人会用。因为他太React了,什么都给使用者去实现!你用了他肯定要面对这个问题
- 有的组件明明没有订阅数据缺无辜重复渲染
那什么时候会使用context呢?
答案是:做第三方(组件)且通信复杂的时候。这样可以保持组件纯粹,越少依赖第三方拓展性越好、越安全。
二、Context用起
三、unstated-next源码
源码大概有40行,精简下做个demo分析,如下:
小型unstated-next
export function createContainer(useHook) { const Context = createContext(null); const Provider = (props) => { const value = useHook(props.initialState); return <Context.Provider value={value}>{props.children}</Context.Provider>; }; const useContainer = () => { return useContext(Context); }; return { Provider, useContainer }; }
在导出的createContainer函数中不难看出,当调用它时会返回一个根据context产出的Provider和useContext(这里命名为useContainer)
用起来是什么样子的呢?
跟传统Context一样,也得有"Provider"包裹。
在这里首先得创建一个"Container"
unstated-next的使用0
import { createContainer } from "./unstated-next"; const useHook = (initialState) => { const [color, setColor] = useState(initialState); return { color, setColor }; }; const ColorContainer = createContainer(useHook); // 传入自定义hook,把逻辑卷里面
跟原始context的Provider有个小区别,就是value改成initialState,同时你不再需要在App搞useState的getter和setter(干净),App也就没re-render(性能),这里只传初始值(聚焦)
unstated-next的使用1
// ... export default function App() { return ( <div className="App"> <ColorContainer.Provider initialState="red"> <Title /> <div> <div>--------</div> <div> <Content /> <Pure /> </div> </div> <Button /> </ColorContainer.Provider> </div> ); }
里面的组件怎么消费呢,ColorContainer.useContainer()返回自定义hook里面的数据,跟contentd的useContext用法类似,区别就是不用传上下文,因为刚开始创建ColorContainer已经绑定了感受一下变化:useContext(context) → ColorContainer.useContainer()
unstated-next的使用2
// ... const Title = () => { console.count("Title"); const { color } = ColorContainer.useContainer(); return <div style={{ color }}>我是标题</div>; }; // ... const Button = () => { console.count("button"); const { setColor } = ColorContainer.useContainer(); return ( <button onClick={() => setColor(getRandomColor())> 点我改颜色 </button> ); };
优点:
- 上手简单、理解容易,抄下来到自己项目都可以,没有其他依赖
- 不用再频繁import createContext, useContext、context,因为都放在一个"Container"里面了
- 让你逻辑和UI分离,逻辑靠自定义hook传进创建阶段,不再像普通context那样一窝蜂逻辑和UI的数据都传入Provider
缺点:
- 跟context一样会误伤没有订阅相应数据的组件
四、 重写
context重复渲染现象
从上一节尾demo里看到,useHook的导出的成员包括color和count相关的值
Provider里有一个Count,他仅仅跟count这个变量”有关“
但是当我点击改变颜色的按钮时,点几次就多渲染几次
这时候加memo有用吗?没用,memo只是能根据props解决问题,能引起组件render的途径不单单因为oldProps !== newProps,还有context的变化、fiber.type的变化等,这里正是context发生变化引起render,不论是哪个值
思考方案
面对React context的无脑更新,我们能采取什么措施?
减少更新
尽量减少,常见的方法是context的读写分离、多变放里,少变放外。
控制更新
就是夺取更新的权力,做到没有订阅变动数据的组件,不会随便重复渲染一般我们怎么让组件更新
- 自己的state用useState(useReducer)
- 自己的父组件改变传进来的props
- forceUpdate,在类组件可以this.forceUpdate(),在函数组件可以利用hook来实现
上面的方法可以看出,forceUpdate的可控性是最好的,我什么时候想渲染就什么时候调用,我名为手动更新
那手动更新的时机是什么-相应依赖变化,那就要有一个新旧值比较的流程,值相同的时候不更新,先网上随便抄一抄shallowEqual
shallowEqual
const shallowEqual = <T extends object>(oldValue: T, newValue: T) => { if (Object.is(oldValue, newValue)) { return true; } if ( typeof oldValue !== 'object' || oldValue === null || typeof newValue !== 'object' || newValue === null ) { return false; } const keysOldValue = Object.keys(oldValue); const keysNewValue = Object.keys(newValue); if (keysOldValue.length !== keysNewValue.length) { return false; } for (let i = 0, len = keysOldValue.length; i < len; i++) { const currentKey = keysOldValue[i]; if ( !keysNewValue.hasOwnProperty(currentKey) || !Object.is(oldValue[currentKey], newValue[currentKey]) ) { return false; } } return true; };
控制一下Provider的value,让他的值不是useState(useReducer)控制,由useRef控制,改写后发现怎么点击也没再渲染,证明劫持渲染成功。
ref接管value
function Provider(props) { const value = useHook(props.initialState) const contextRef = useRef(value) return <Provider value={contextRef}>{props.children}</Provider> }
接着是看看消费数据的组件怎么设计,用多了dva直接就命名一个hook叫useSelector,用法一样,看样子是有精确订阅"count"这个数据的能力
消费组件新增hook-useSelector
const Count = (props) => { console.count("Count"); //const { count } = ColorContainer.useContainer(); const count = useSelector(_ => _.count) return <div>{count}</div>; };
接着实现这个useSelector,当然它是存在createContainer里面,作为一个成员导出思路:入参selector是一个函数,value是整个state,selector(value)期望获得最新的值,即上面例子最新的count值
useSelector初步实现
// ... const Context = createContext() function useSelector(selector) { const value = useContext(Context).current const selected = selector(value) } return { useSelector, Provider, }
改造到现在,点击是没有重新渲染,我们回到关键的Provider。因为contextValue 由useRef控制,只要我们不改变,它永远都不会使内容更新。接下来在Provider继续入手,将contextRef开放,对它赋值,记得别将contextRef传给Provider的value,因为contextRef由始至终能保持是同一个引用useEffect监听走起,useEffect内部需要执行的是【执行监听”需要“的值】
Provider监听
function Provider(props) { const value = useHook(props.initialState) const contextRef = useRef(value) contextRef.current = value useEffect(() => { // 监听逻辑 }) return <Provider value={contextRef}>{props.children}</Provider> }
如何实现监听逻辑,我这里是想到靠一个单例来通信,简单实现一个添加状态事件和清除事件
Singleton
class Singleton { constructor(listeners) { this.listeners = listeners; } add(states) { this.listeners.push(states); } clean() { this.listeners = []; } // 执行 exc(value) { this.listeners.forEach(cb => { cb(value) }) } }
接着,在创建(createContainer)时获得一个实例watcher,在Provider里监听执行callback
添加监听逻辑
function createContainer(useHook){ const Context = createContext(null); const watcher = new Singleton([]); // 新加代码 function Provider(props) { const value = useHook(props.initialState) const contextRef = useRef(value) contextRef.current = value useEffect(() => { // 在监听逻辑添加执行回调队列 watcher.exc(value) // 新加代码 }) return <Provider value={contextRef}>{props.children}</Provider> } }
先看看上面的callback的真身是什么样子的
在节点挂载完成阶段,将callback即下面的watch函数添加到watcher单例
watch函数
一句话,各种比较,值相同就return,不相同就触发forceUpdate,强制更新ref控制的值。下面用到的shallowEqual在上面有实现
useSeletor修改
const useSelector = (selector) => { const value = useContext(Context).current; const selected = selector(value); const forceUpdate = useForceUpdate(); const ref = useRef({ selected, value }); ref.current = { selected, value }; useEffect(() => { const watch = (newValue) => { if (!ref.current) { return; } if (newValue === ref.current.value) { return; } const newSelected = selector(newValue); if (shallowEqual(newSelected, ref.current.selected)) { return; } forceUpdate(); }; watcher.add(watch); return () => { watcher.clean(); }; }, []); return selected; };
效果图
Count没有重复渲染!因为它没有订阅color的数据。
增加更多API
要让使用方代码进一步减少,usePicker只关注需要什么属性,不用再像useSelector传一个回调函数。
usePicker
const pick = (obj, arr) => arr.reduce((iter, val) => (val in obj && (iter[val] = obj[val]), iter), {}); // ... const usePicker = (target) => { return useSelector((_) => { // 数组 if (Array.isArray(target)) { return pick(_, target); } // 字符串 if (typeof target === "string") { return pick(_, [target]); } return _; }); };
写法支持
五、最后
其他优秀方案
heo
本文的封装灵感源自于此,但比它更易理解
hox
umijs下的