透过Preact源码学习hook

315 阅读4分钟

6.jpg

概述

preact的hook是基于数组的数据结构实现的,而react是通过链表的数据结构实现的;Preact通过currentIndex索引来记录useHook在list中的位置,并放入一个引用对象值。

useState的实现

使用方式

const [count, setCount] = useState(0);  //入参还可以是
// update
setCount(2);
setCount(i => i + 1)

实现方式

export function useState(initialState) {
    currentHook = 1;
    return useReducer(invokeOrReturn, initialState);
}
function invokeOrReturn(arg, f) {
    return typeof f == 'function' ? f(arg) : f;
}
  1. 通过代码可以看出useState是通过useReducer实现的,默认传入了一个reducer函数(invokeOrReturn)
  2. 看到这有个疑问就是initialState可以传入一个函数怎么实现的?答案就在useReducer的实现

useReducer的实现

实现方式

export function useReducer(reducer, initialState, init) {
    /** @type {import('./internal').ReducerHookState} */
    // list 索引currentIndex; value: {}
    const hookState = getHookState(currentIndex++, 2);
    // 赋值reducer
    hookState._reducer = reducer;
    // 第一次默认进来没有值
    if (!hookState._component) {
            // useState返回值[state, useState]
            hookState._value = [
                    !init ? invokeOrReturn(undefined, initialState) : init(initialState),

                    action => {
                            // 如果是useState进来的,action是fn则fn(hookState._value[0]), 否则返回action
                            const nextValue = hookState._reducer(hookState._value[0], action);
                            if (hookState._value[0] !== nextValue) {
                                    hookState._value = [nextValue, hookState._value[1]];
                                    // 触发更新
                                    hookState._component.setState({});
                            }
                    }
            ];

            hookState._component = currentComponent;
    }

    return hookState._value;
}
  1. 上面的疑问实现就在!init ? invokeOrReturn(undefined, initialState) : init(initialState),;当initialState为函数时就initialState(undefined)返回value
  2. 上面代码重点就是getHookState函数

getHookState的实现

function getHookState(index, type) {
    if (options._hook) {
       options._hook(currentComponent, index, currentHook || type);
    }
    currentHook = 0;

    // Largely inspired by:
    // * https://github.com/michael-klein/funcy.js/blob/f6be73468e6ec46b0ff5aa3cc4c9baf72a29025a/src/hooks/core_hooks.mjs
    // * https://github.com/michael-klein/funcy.js/blob/650beaa58c43c33a74820a3c98b3c7079cf2e333/src/renderer.mjs
    // Other implementations to look at:
    // * https://codesandbox.io/s/mnox05qp8
    const hooks =
        currentComponent.__hooks ||
        (currentComponent.__hooks = {
            _list: [],
            _pendingEffects: []
        });

    if (index >= hooks._list.length) {
        hooks._list.push({});
    }
    return hooks._list[index];
}
  1. 当前组件的currentComponent下初始化一个__hooks,index也就是currentIndex默认值为0用来记录当前组件下useHook的api使用,用list来收集,并依据currentIndex来一一对应,并返回一个{},赋值给hookState`
  2. 看到这有一个疑问就是useState(0); useState(1)多次使用时,当重新渲染时,怎么跟踪到上一帧下的useState(0)对应的hookState;Preact的解决办法时render后将currentIndex重置为0
options._render = vnode => {
    if (oldBeforeRender) oldBeforeRender(vnode);
    currentComponent = vnode._component;
    //将其重置为0
    currentIndex = 0;

    const hooks = currentComponent.__hooks;
    if (hooks) {
            //....
            hooks._pendingEffects = [];
    }
}

useEffect的实现

使用方式

useEffect(() => {
    // 执行副作用操作
    Api.get();
    // 清除函数 移除订阅,清空定时器
    return () => {}
}, [])

实现方式

export function useEffect(callback, args) {
    /** @type {import('./internal').EffectHookState} */
    // 根据currentIndex的索引返回list中state的值
    const state = getHookState(currentIndex++, 3);
    if (!options._skipEffects && argsChanged(state._args, args)) {
        state._value = callback;
        state._args = args;

        currentComponent.__hooks._pendingEffects.push(state);
    }
}
function argsChanged(oldArgs, newArgs) {
    return (
        !oldArgs ||
        oldArgs.length !== newArgs.length ||
        newArgs.some((arg, index) => arg !== oldArgs[index])
    );
}
  1. 第一次执行时state._args是没值;所以进行初始化赋值操作
  2. 将state放入_pendingEffects进行收集;等待执行
  3. 所以执行时机在什么时候呢?在执行render
options._render = vnode => {
    if (oldBeforeRender) oldBeforeRender(vnode);

    currentComponent = vnode._component;
    currentIndex = 0;

    const hooks = currentComponent.__hooks;
    if (hooks) {
        hooks._pendingEffects.forEach(invokeCleanup);
        hooks._pendingEffects.forEach(invokeEffect);
        // 执行完后清空,第一点当args没变时就不会遍历执行;第二当args发生变化时重新更新list
        hooks._pendingEffects = [];
    }
};
function invokeCleanup(hook) {
    // A hook cleanup can introduce a call to render which creates a new root, this will call options.vnode
    // and move the currentComponent away.
    const comp = currentComponent;
    if (typeof hook._cleanup == 'function') hook._cleanup();
    currentComponent = comp;
}

/**
 * Invoke a Hook's effect
 * @param {import('./internal').EffectHookState} hook
 */
function invokeEffect(hook) {
    // A hook call can introduce a call to render which creates a new root, this will call options.vnode
    // and move the currentComponent away.
    const comp = currentComponent;
    hook._cleanup = hook._value();
    currentComponent = comp;
}
  1. invokeCleanup从这个函数可以看出useEffect在第一次执行时清除函数是不执行的,因为不存在;后面都是先hook._cleanup()hook._value();也就实现了useEffect的机制

useContext的实现

使用方式

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

实现方式

export function useContext(context) {
    // Context.provider的实例对象
    const provider = currentComponent.context[context._id];
    // We could skip this call here, but than we'd not call
    // `options._hook`. We need to do that in order to make
    // the devtools aware of this hook.
    /** @type {import('./internal').ContextHookState} */
    const state = getHookState(currentIndex++, 9);
    // The devtools needs access to the context object to
    // be able to pull of the default value when no provider
    // is present in the tree.
    state._context = context;
    if (!provider) return context._defaultValue;
    // This is probably not safe to convert to "!"
    if (state._value == null) {
        state._value = true;
        provider.sub(currentComponent);
    }
    return provider.props.value;
}
  1. const provider = currentComponent.context[context._id];返回的React.createContext().Provider实例对象
Provider(props) {
    if (!this.getChildContext) {
        let subs = [];
        let ctx = {};
        // 就这两行代码
        ctx[contextId] = this;

        this.getChildContext = () => ctx;
        // 实现Provider自身的shouldComponentUpdate,
        // 应该就是来实现Provider 及其内部 consumer 组件都不受制于 组件自身的shouldComponentUpdate 函数
        this.shouldComponentUpdate = function(_props) {
            if (this.props.value !== _props.value) {
                subs.some(enqueueRender);
            }
        }
        // ...
        this.sub = c => {
            subs.push(c);
            let old = c.componentWillUnmount;
            c.componentWillUnmount = () => {
                    subs.splice(subs.indexOf(c), 1);
                    if (old) old.call(c);
            };
        };
    }

    return props.children;
}
  1. provider不存在时,就用defaultValue;要注意的是当<ThemeContext.Provider>没有给定value时就是undefined
  2. 从代码中可以看出重写组件的componentWillUnmount,当组件卸载前会将在subs中移除当前组件,因此在contextvalue发生改变后,重新渲染正确的组件列表

useMemo的实现

使用方式

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

实现方式

export function useMemo(factory, args) {
    /** @type {import('./internal').MemoHookState} */
    const state = getHookState(currentIndex++, 7);
    // 每次args发生变化时重新进行赋值,求值
    if (argsChanged(state._args, args)) {
        state._value = factory();
        state._args = args;
        state._factory = factory;
    }

    return state._value;
}

useCallback的实现

就是基于useMemo

使用方式

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

实现方式

export function useCallback(callback, args) {
    currentHook = 8;
    return useMemo(() => callback, args);
}

useRef的实现

使用方式

const refContainer = useRef(initialValue);

实现方式

巧妙的使用useMemo

export function useRef(initialValue) {
    currentHook = 5;
    // 返回一个可变的引用对象
    return useMemo(() => ({ current: initialValue }), []);
}
  1. 这样就生成一个在组件整个生命周期中地址不变的可变的引用数据类型

总结

对于Hooks的api理解,仅仅是个人的学习总结,有不对的地方还望指出,一起学习,共同进步。