概述
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;
}
- 通过代码可以看出
useState
是通过useReducer
实现的,默认传入了一个reducer函数(invokeOrReturn) - 看到这有个疑问就是
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;
}
- 上面的疑问实现就在
!init ? invokeOrReturn(undefined, initialState) : init(initialState),
;当initialState
为函数时就initialState(undefined)
返回value
- 上面代码重点就是
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];
}
- 当前组件的currentComponent下初始化一个__hooks,
index
也就是currentIndex
默认值为0用来记录当前组件下useHook的api使用,用list来收集,并依据currentIndex来一一对应,并返回一个
{},赋值给
hookState` - 看到这有一个疑问就是
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])
);
}
- 第一次执行时
state._args
是没值;所以进行初始化赋值操作 - 将state放入
_pendingEffects
进行收集;等待执行 - 所以执行时机在什么时候呢?在执行
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;
}
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;
}
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;
}
- 当
provider
不存在时,就用defaultValue
;要注意的是当<ThemeContext.Provider>
没有给定value
时就是undefined
- 从代码中可以看出重写组件的
componentWillUnmount
,当组件卸载前会将在subs
中移除当前组件,因此在context
的value
发生改变后,重新渲染正确的组件列表
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 }), []);
}
- 这样就生成一个在组件整个生命周期中地址不变的可变的引用数据类型
总结
对于Hooks的api理解,仅仅是个人的学习总结,有不对的地方还望指出,一起学习,共同进步。