记一次React Context性能优化

·  阅读 831
记一次React Context性能优化

问题背景

有一个页面布局结构是左边菜单,右边长页面,可以一直往下滚动。左边菜单点击,则右边页面自动滚动到对应模块位置,同理,右边页面滚动左边菜单对应高亮。

我一看这么长的页面一次展示完?就不能分页? 于是就向产品反馈这一次性展示这么多性能可能会不好,但是产品不管啊他一定要这个效果。本着提升自己的宗旨,我决定不和产品谈判了,到时候出现性能问题自己想办法优化。

长页面滚动的常见优化方式就是动态加载了吧,但这里考虑到菜单和模块内容需要双向联动所以放弃了这个想法。

然后为啥我要用context给这些组件传递数据。因为模块很多,一个最小的模块分为一个组件的话,组件大概得写几十个。而且很重要的一点是,因为是一个页面,后端决定直接用一个socket给我提供数据。如此一来我就没有办法在每个模块内部自己调数据了,只能由上层获取传入下层组件。

那为啥不用redux?因为懒,hhhh.

image.png

context简单易用,直接把数据通过Provider往下注入就行。

然后问题来了,这个socket大概会推送几十次,因为有几十个模块,每一次推送我都得更新contextvalue,以保证下层组件能拿到最新值。这个更新就出大问题了。

每一次context的更新都会导致使用了该context的组件触发re-render,即使该组件用memo包裹且props未改变

然后每个组件,都会re-render几十次。。。wtf,我往下滚动页面的时候,肉眼可见的卡顿。

image.png

那我不修改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>
    )
}
复制代码

源码

use-selected-context

分类:
前端
标签:
分类:
前端
标签: