Genluo / Precipitation-and-Summary

梳理体系化的知识点&沉淀日常开发和学习
MIT License
15 stars 1 forks source link

react hooks #65

Open Genluo opened 4 years ago

Genluo commented 4 years ago

最近项目中慢慢在使用hooks,感觉总是有点奇怪,原因是不知道hooks背后的实现,虽然看到很多实现hooks的伪代码,但是内心总是存在一些疑问,hooks是如何在代码中实现的?为了知其然知其所以然,开始准备写篇文章整理下自己对hooks的了解和选择preact从代码中中深究下hooks的实现

1、为什么需要Hooks?

当react提出组件化,那么摆在组件化面前的一个问题是:每个组价都要考虑数据共享问题和状态复用问题,针对数据共享问题,react官方和社区提供了非常多的方案,比如:

那么针对组件之间的状态复用,react官方给出的方案是:

那么官方后期给出的函数组件+hooks方案主要原因是为了解决类组件中通过闭包实现的状态逻辑复用问题,因为使用HOC带来了新的问题,比如:

但是现在,当我们使用函数组件+hooks的时候,那么这些问题就能够得到最直接的解决方案,并且函数组件可以通过hooks来实现re-render,同时解决了函数组件不能脱离类组件而存在的问题,这时候函数组件已经能够完成类组件的大部分功能,我们可以渐渐迁移到函数组件+hooks写法,带来的好处可以开发复用一些业务逻辑,将业务逻辑和组件渲染彻底分离开来,并且hooks提供的api也比较简单,核心api只有两个,通过这两个API简化了类组件的生命周期的概念,同时替换到函数组件+hooks组件也能带来如下好处:

但是也产生了新的问题,社区中对此的反应是:函数组件已经不够纯了,react距离其开发理念越走越远,但是不论怎么说,使用hooks来编写新的组件确实挺令人激动地,因为他补全了函数组件的state的概念,让我们不用在class组件和Function之间纠结到底应该使用那些功能同时通过hooks我们可以封装很多hooks工具,来快速实现相关业务功能更好的组织代码的结构,更加干净的拆分函数中的渲染逻辑和业务逻辑,总之,我感觉hooks的出现为我们现有的代码编写带来了新的体验

2、如何使用

官方文档是最好的教程

2、hooks如何实现

主要分为两大部分,一部分是如何实现hooks函数,另一部分是如何修改原有的函数组件的生命周期,实现重新渲染

(1)实现hooks函数

这一块主要将函数封装为两种,一种是存储型,比如useState、useReducer、useContext,另一种实现一些副作用比如:useEffect、useLayoutEffect、useRef、useImperativeHandle、useMemo、useCallback,剩下的有context共享的useContext,useDebugValue相关的API

1、第一类存储型(触发更新)

// 每次调用render方法会初始化这两个变量
let currentComponent;
let currentIndex;

// 获取当前组件的state
function getHookState(index) {
    const hooks =
        currentComponent.__hooks ||
        (currentComponent.__hooks = { _list: [], _pendingEffects: [] });

    if (index >= hooks._list.length) {
        hooks._list.push({});
    }
    return hooks._list[index];
}

function invokeOrReturn(arg, f) {
    return typeof f === 'function' ? f(arg) : f;
}

export function useReducer(reducer, initialState, init) {
    const hookState = getHookState(currentIndex++);
    if (!hookState._component) {
        hookState._component = currentComponent;

        hookState._value = [
            !init ? invokeOrReturn(undefined, initialState) : init(initialState),

            action => {
                const nextValue = reducer(hookState._value[0], action);
                // 类型强校验
                if (hookState._value[0] !== nextValue) {
                    hookState._value[0] = nextValue;
                    hookState._component.setState({}); //重新渲染
                }
            }
        ];
    }

    return hookState._value;
}
export function useState(initialState) {
    return useReducer(invokeOrReturn, initialState);
}
export function useContext(context) {
    const provider = currentComponent.context[context._id];
    if (!provider) return context._defaultValue;
    const state = getHookState(currentIndex++);
    if (state._value == null) {
        state._value = true;
        provider.sub(currentComponent); // 组件订阅,context变动触发组件重新render
    }
    return provider.props.value;
}

2、实现一些副作用

function argsChanged(oldArgs, newArgs) {
    return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}

export function useEffect(callback, args) {
    const state = getHookState(currentIndex++);
    if (argsChanged(state._args, args)) {
        state._value = callback;
        state._args = args;
        currentComponent.__hooks._pendingEffects.push(state);
    }
}
export function useLayoutEffect(callback, args) {
    const state = getHookState(currentIndex++);
    if (argsChanged(state._args, args)) {
        state._value = callback;
        state._args = args;

        currentComponent._renderCallbacks.push(state);
    }
}

3、存储,类似类的变量(如果依赖不发生改变,则值引用不发生变化)

export function useMemo(callback, args) {
    /** @type {import('./internal').MemoHookState} */
    const state = getHookState(currentIndex++);
    if (argsChanged(state._args, args)) {
        state._args = args;
        state._callback = callback;
        return (state._value = callback());
    }

    return state._value; // 旧的引用
}
export function useCallback(callback, args) {
  return useMemo(() => callback, args);
}
export function useRef(initialValue) {
  return useMemo(() => ({ current: initialValue }), []);
}
export function useImperativeHandle(ref, createHandle, args) {
    useLayoutEffect(
        () => {
            if (typeof ref === 'function') ref(createHandle());
            else if (ref) ref.current = createHandle();
        },
        args == null ? args : args.concat(ref)
    );
}

(2)函数组件的声明周期改造

这一块主要是针对副作用一块,在副作用执行的期间我们为hooks中绑定了很多_pendingEffects和_renderCallback的state,那么在组价运行期间将会执行这些数组之中的内容

let oldBeforeRender = options._render;  //  原有声明周期的判断
let currentComponent;
let currentIndex;

function invokeCleanup(hook) {
    if (hook._cleanup) hook._cleanup(); // 每个useEffect的返回值
}

function invokeEffect(hook) {
    const result = hook._value();
    if (typeof result === 'function') hook._cleanup = result;
}

options._render = vnode => {
    if (oldBeforeRender) oldBeforeRender(vnode);

    currentComponent = vnode._component;
    currentIndex = 0;

    if (currentComponent.__hooks) {
        currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
        currentComponent.__hooks._pendingEffects.forEach(invokeEffect);
        currentComponent.__hooks._pendingEffects = [];
    }
};
let oldAfterDiff = options.diffed;
let afterPaintEffects = [];

options.diffed = vnode => {
    if (oldAfterDiff) oldAfterDiff(vnode);

    const c = vnode._component;
    if (!c) return;

    const hooks = c.__hooks;
    if (hooks) {
        if (hooks._pendingEffects.length) {
            afterPaint(afterPaintEffects.push(c));
        }
    }
};
let oldCommit = options._commit;

options._commit = (vnode, commitQueue) => {
    commitQueue.some(component => {
        component._renderCallbacks.forEach(invokeCleanup);
        component._renderCallbacks = component._renderCallbacks.filter(cb =>
            cb._value ? invokeEffect(cb) : true
        );
    });

    if (oldCommit) oldCommit(vnode, commitQueue);
};
let oldBeforeUnmount = options.unmount;

options.unmount = vnode => {
    if (oldBeforeUnmount) oldBeforeUnmount(vnode);

    const c = vnode._component;
    if (!c) return;

    const hooks = c.__hooks;
    if (hooks) {
        hooks._list.forEach(hook => hook._cleanup && hook._cleanup());
    }
}

3、设计工具hooks

1、节流、防抖

2、请求封装

3、深度比较依赖数组useDeepCompareEffect

4、使用useCurrentProps包装总是变化的props

function useCurrentValue<T>(value: T): React.RefObject<T> {
  const ref = React.useRef(null);
  ref.current = value;
  return ref;
}

const App: React.FC = ({ onChange }) => {
  const onChangeCurrent = useCurrentValue(onChange)
};

5、使用hooks处理动画相关逻辑

4、拓展

Genluo commented 4 years ago

❗️❗️❗️错误警告

function Example() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

如果你先点击「Show alert」然后增加计数器的计数,那这个 alert 会显示在你点击『Show alert』按钮时的 count 变量。这避免了那些因为假设 props 和 state 没有改变的代码引起问题。

如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。

最后,你看到陈旧的 props 和 state 的另一个可能的原因,是你使用了「依赖数组」优化但没有正确地指定所有的依赖。举个例子,如果一个 effect 指定了 [] 作为第二个参数,但在内部读取了 someProp,它会一直「看到」 someProp 的初始值。解决办法是要么移除依赖数组,要么修正它。

注意 我们提供了一个 exhaustive-deps ESLint 规则作为 eslint-plugin-react-hooks 包的一部分。它会在依赖被错误指定时发出警告,并给出修复建议。

跳过state更新

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用Object.is比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

Genluo commented 4 years ago

🔥🔥🔥ref向外暴露接口

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 的父组件可以调用 fancyInputRef.current.focus()。

Genluo commented 4 years ago

❗️❗️❗️如何正确测量节点

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

useRef类似类常量,只会在第一次加载完成之后赋值,之后不会主动更改值

在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。请记住,当ref.current指向的对象内容发生变化,useRef并不会通知你,变更.current并不会引起组件的重新渲染,使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

Genluo commented 4 years ago

🔥🔥🔥组件性能优化

1、渲染优化

你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较

const Button = React.memo((props) => {
  // 你的组件
});

这不是一个 Hook 因为它的写法和 Hook 不同。React.memo 等效于 PureComponent,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)

React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。

推荐使用 React.useMemo 而不是 React.memo,因为在组件通信时存在 React.useContext 的用法,这种用法会使所有用到的组件重渲染,只有 React.useMemo 能处理这种场景的按需渲染。

2、计算优化

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

这行代码会调用 computeExpensiveValue(a, b)。但如果依赖数组 [a, b] 自上次赋值以来没有改变过,useMemo 会跳过二次调用,只是简单复用它上一次返回的值。

记住,传给 useMemo 的函数是在渲染期间运行的。不要在其中做任何你通常不会在渲染期间做的事。举个例子,副作用属于 useEffect,而不是 useMemo。

方便起见,useMemo 也允许你跳过一次子节点的昂贵的重新渲染:

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

3、函数优化

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

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

Genluo commented 4 years ago

🔥🔥🔥底层原理

一文彻底搞懂react hooks

1、React 是如何把对 Hook 的调用和组件联系起来的?

React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。

每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

2、Hook 使用了哪些现有技术?

Hook 由不同的来源的多个想法构成:

react-future 这个仓库中包含我们对函数式 API 的老旧实验。 React 社区对 render prop API 的实验,其中包括 Ryan Florence 的 Reactions Component 。 Dominic Gannaway 的用 adopt 关键字 作为 render props 的语法糖的提案。 DisplayScript 中的 state 变量和 state 单元格。 ReasonReact 中的 Reducer components。 Rx 中的 Subscriptions。 Multicore OCaml 提到的 Algebraic effects。 Sebastian Markbåge 想到了 Hook 最初的设计,后来经过 Andrew Clark,Sophie Alpert,Dominic Gannaway,和 React 团队的其它成员的提炼。

Genluo commented 4 years ago

🔥🔥🔥 context使用

1、使用

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 的 value prop 决定。

2、context API使用

const MyContext = React.createContext(defaultValue);

注意:此API只能调用一个context,如需调用多个context,使用coutomer

Genluo commented 4 years ago

❗️❗️❗️useEffect和useLayoutEffect的区别

1、useLayoutEffect执行时机

useLayoutEffect会在所有的DOM变更之后同步调用effect,可以使用它来读取DOM布局并同步触发重渲染 useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的

2、useEffect执行时机

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。useEffect会在组件渲染到屏幕之后执行,默认情况下,effect将会在将在每轮渲染结束后执行

Genluo commented 4 years ago

🔥🔥🔥问题整理

1、应该使用单state还是应该使用多state

2、deps 依赖过多,导致 Hooks 难以维护?

3、该不该使用useMemo方法?

(1)什么时候使用

(2)使用应该思考的问题?

Genluo commented 4 years ago

❗️❗️❗️ React.memo和React. useMemo的区别?