问题背景
有一个页面布局结构是左边菜单,右边长页面,可以一直往下滚动。左边菜单点击,则右边页面自动滚动到对应模块位置,同理,右边页面滚动左边菜单对应高亮。
我一看这么长的页面一次展示完?就不能分页?
于是就向产品反馈这一次性展示这么多性能可能会不好,但是产品不管啊他一定要这个效果。本着提升自己的宗旨,我决定不和产品谈判了,到时候出现性能问题自己想办法优化。
长页面滚动的常见优化方式就是动态加载了吧,但这里考虑到菜单和模块内容需要双向联动所以放弃了这个想法。
然后为啥我要用context
给这些组件传递数据。因为模块很多,一个最小的模块分为一个组件的话,组件大概得写几十个。而且很重要的一点是,因为是一个页面,后端决定直接用一个socket
给我提供数据。如此一来我就没有办法在每个模块内部自己调数据了,只能由上层获取传入下层组件。
那为啥不用redux
?因为懒,hhhh.
context
简单易用,直接把数据通过Provider
往下注入就行。
然后问题来了,这个socket
大概会推送几十次,因为有几十个模块,每一次推送我都得更新context
的value
,以保证下层组件能拿到最新值。这个更新就出大问题了。
每一次
context
的更新都会导致使用了该context
的组件触发re-render
,即使该组件用memo
包裹且props
未改变
然后每个组件,都会re-render
几十次。。。wtf,我往下滚动页面的时候,肉眼可见的卡顿。
那我不修改context
不就行了吗?可是不修改context
子组件如何才能拿到新值呢?这个时候观察者模式
就派上用场了,然后我们只需要加上一个依赖项,当依赖项有更新,此时发布
消息,让订阅者,也就是子组件能够收到最新的值且触发更新即可。
使用观察者模式设计一个Hook
理想情况下子组件调用获取context
的值是这样的:
// 比如context值为 {value1: '', value2: ''}
// 给Hook传入一个依赖项,只有该依赖项的属性value1更新时,才触发更新
const { value1 } = useModel(['value1']);
复制代码
那么开始写消息的发布者,发布者要能存储context
的值,且能读取该值,包括收集订阅者的订阅。每次修改context
值的时候,发布给所有订阅者,订阅者再自行判断是否需要更新组件。
class Listenable {
constructor(state) {
this._listeners = [];
this.value = state;
}
getValue() {
return this.value;
}
setValue(value) {
const previous = this.value;
this.value = value;
this.notifyListeners(this.value, previous);
}
addListener(listener) {
this._listeners.push(listener);
}
removeListener(listener) {
const index = this._listeners.indexOf(listener);
if (index > -1) {
this._listeners.splice(index, 1);
}
}
hasListener() {
return this._listeners.length > 0;
}
notifyListeners(current, previous) {
if (!this.hasListener()) {
return;
}
for (const listener of this._listeners) {
listener(current, previous);
}
}
}
复制代码
然后是订阅者,也就是子组件调用的Hook
import isEqual from 'lodash.isequal';
export default function useModel(context, deps = []) {
const [state, setState] = useState(context.getValue());
const stateRef = useRef(state);
stateRef.current = state;
const listener = useCallback((curr, pre) => {
// 如果存在依赖,则只判断依赖部分
let [current, previous] = getDepsData(curr, pre);
if (isChange(current, previous)) {
setState(current);
}
}, []);
// 如果state在组件添加 listener 之前就被其他组件修改了,那么需要调用 此处以更新 state值
// 比如 A 组件 在useEffect 中修改了 state,B组件和A为兄弟组件,但B组件后渲染,两者都使用 useModel 获取 state,此时两者都拿到最初的state
// 然后 A 组件 的 listener 被添加,先执行了 useListener 中的添加操作,之后执行A组件中useEffect修改state的操作
// 此时 state 被修改,但是 B 组件之前已经拿到了state,是旧的值,所以需要更新
const onListen = useCallback(() => {
let [current, previous] = getDepsData(context.getValue(), stateRef.current);
if (isChange(current, previous)) {
listener(current, previous);
}
}, [context, listener]);
useListener(context, listener, onListen);
// 根据依赖项获取前后值
const getDepsData = useCallback((current, previous) => {
if (deps.length) {
let currentTmp = {};
let previousTmp = {};
deps.map(k => {
currentTmp[k] = current[k];
previousTmp[k] = previous[k];
});
current = currentTmp;
previous = previousTmp;
}
return [current, previous];
}, []);
// 对比变化
const isChange = useCallback((current, previous) => {
if (current instanceof Object) {
return !isEqual(current, previous);
}
return false;
}, []);
const setContextValue = useCallback((v) => {
context.setValue(v);
}, [context]);
return [context.getValue(), setContextValue];
}
复制代码
最终调用方式
父组件使用
import Context from './context';
import { ShareState } from 'use-selected-context';
export default () => {
const [value] = useState(new ShareState({a: 0, b: 0}))
// 修改调用 value.setValue()
const onClick = () => {
let o = value.getValue()
let nd = {a: o.a, b: o.b + 1};
value.setValue(nd);
}
return (
<div>
<Context.Provider value={value}>
<Child />
<Child2 />
<Button onClick={onClick}>b+1</Button>
</Context.Provider>
</div>
)
}
复制代码
子组件调用
import context from './context';
import useModel from 'use-selected-context'
export default () => {
const contextValue = useContext(context)
// 传入依赖项属性数组,只有 a 变才会 re-render 该组件
// 不传入依赖项时,其他属性更新,该组件也会刷新
const [v, setV] = useModel(contextValue, ['a']);
return (
<div>a值为:{v.a}</div>
)
}
复制代码