• home > webfront > ECMAS > react >

    React代数效应学习笔记

    Author:zhoulujun Date:

    代数效应是一项研究中的编程语言特性,用于将副作用从函数调用中分离,实现代码在自调用函数中自上而下执行的操作。简而言之,代数效应是一种异常(exception)机制,可让throw编码功能继续其操作。

    Hook 是 React 16.8 的新增特性,在 Hooks 出现之前,我们有两种方法可以复用组件逻辑:Render Props 和高阶组件。Hooks 的出现,让组件逻辑的复用变得更简单,同时解决了「嵌套地域」的问题。React Hooks 带来的好处不仅是 “更 FP,更新粒度更细,代码更清晰”,还有如下三个特性:

    • 多个状态不会产生嵌套,写法还是平铺的(renderProps 可以通过 compose 解决,可不但使用略为繁琐,而且因为强制封装一个新对象而增加了实体数量)。

    • Hooks 可以引用其他 Hooks。

    • 更容易将组件的 UI 与状态分离。

    Hook API

    建议先过一遍,https://react.docschina.org/docs/hooks-reference.html

    钩子名作用
    useState初始化和设置状态
    useEffectcomponentDidMount/componentDidUpdate/componentWillUnmount 结合体,所以可以监听 useState 定义值的变化
    useContext定义一个全局的对象,类似 Context
    useReducer可以增强函数提供类似 Redux 的功能
    useCallback记忆作用,共有两个参数,第一个参数为一个匿名函数,就是我们想要创建的函数体,第二参数为一个数组,里面的每一项是用来判断是否需要重新创建函数体的变量,如果传入的变量值保持不变,返回记忆结果,如果任何一项改变,则返回新的结果
    useMemo作用和传入参数与 useCallback 一致,useCallback 返回函数,useDemo 返回值
    useRef获取 ref 属性对应的 DOM
    useImperativeMethods自定义使用 Ref 时公开给父组件的实例值
    useMutationEffect作用与 useEffect 相同,但在更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发
    useLayoutEffect作用与 useEffect 相同,但在所有 DOM 改变后同步触发

    我们下面来看几个平时使用频率较高的 Hook

    useState

    const [state, setState] = useState(initialState); 返回一个 state,以及更新 state 的函数。

    • 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同—— 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用。思考领悟:为什么叫做 useState 而不是 createState ?

    • setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

      • 在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state

      • React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。

      • 更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过

      • React 使用 Object.is 比较算法 来比较 state,setState() 自动具备浅合并功能,useState()更新引用需要手动浅合并。

    useState提供的状态更新函数不会自动合并更新对象,而是替换它,所以你需要手动合并更新(如果是对象的话)。setState在更新时会将新的状态对象与当前状态合并。

    useState可以在组件的任何地方使用,但必须位于函数的顶层(不能在循环、条件或嵌套函数中调用)。

    使用多个 state 变量来保存 state

    const [width, setWidth] = useState(100);
    const [height, setHeight] = useState(100);
    const [left, setLeft] = useState(0);
    const [top, setTop] = useState(0);

    使用多个 state 变量可以让 state 的粒度更细,更易于逻辑的拆分和组合。

    所有的 state 放到一个 object 中(单个state)

    const [state, setState] = useState({
      width: 100,
      height: 100,
      left: 0,
      top: 0
    });

    使用单个 state 变量,每次更新 state 时需要合并之前的 state。因为 useState 返回的 setState 会替换原来的值。这一点和 Class 组件的 this.setState 不同。 

    • 将完全不相关的 state 拆分为多组 state。比如 size 和 position。

    • 如果某些 state 是相互关联的,或者需要一起发生改变,就可以把它们合并为一组 state。比如 left 和 top。

    有点像Vue vuex mapState mapMutations 联合体,只是类比,推荐《一文彻底搞懂 react hooks 的原理和实现



    useReducer

    const [state, dispatch] = useReducer(reducer, initialArg, init); 跟useState是一样,只是接收的是redux的reduce

    const initialState = {count: 0};
    
    function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
          return {count: state.count - 1};
        default:
          throw new Error();
      }
    }
    
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
      return (
        <>
          Count: {state.count}
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
      );
    }

    React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用 useReducer(reducer, undefined, reducer) 来模拟 Redux 的行为,但我们不鼓励你这么做。

    局部状态不推荐使用 useReducer ,会导致函数内部状态过于复杂,难以阅读。 useReducer 建议在多组件间通信时,结合 useContext 一起使用。

    useEffect

    赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。类似vue的 $nextTick,不同的是:组件卸载时需要清除 effect,初始化时候执行

    赋值给 useLayoutEffect 的函数会在所有的 DOM 变更之后同步调用 

    useEffect(() => {//useEffect 的代码既会在初始化时候执行,也会在后续每次 rerender 时执行,而返回值在析构时执行。
      const subscription = props.source.subscribe();
      return () => {
        // 清除订阅
        subscription.unsubscribe();
      };
    },[props.source]);//只有当第二参数里面的值改变后才会重新创建订阅。不传为任意改变都创建

    第二个参数「dependency array」(依赖数组),只有当依赖数组发生变化时,才会执行 useEffect 的回调函数。但是,如果有遗漏,可能会造成 bug。这其实就是 JS 闭包问题。

    接收依赖数组作为参数的 Hook 还有 useMemo、useCallback 和 useImperativeHandle。

    依赖数组中千万不要遗漏回调函数内部依赖的值

    请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得处理额外操作很方便。

    useContext

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

    当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

    调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化

    useRef

    const refContainer = useRef(initialValue); useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。 这个我也是懵懵懂懂的

    function TextInputWithFocusButton() {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` 指向已挂载到 DOM 上的文本输入元素
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" />
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }

    本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

    useCallback和useMemo

    返回一个 memoized 回调函数  和 memoized 值。

     可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。

    memo

    在计算机领域,记忆化是一种主要用来提升计算机程序速度的优化技术方案。它将开销较大的函数调用的返回结果存储起来,当同样的输入再次发生时,则返回缓存好的数据,以此提升运算效率

    在 《JavaScript 忍者秘籍》的 3.2.2 节中「自记忆函数」中有这样的介绍:记忆化是一种构建函数的处理过程,能够记住上次计算结果。在这个果壳里,当函数计算得到结果时就将该结果按照参数存储起来。采用这种方式时,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍。像这样避免既重复又复杂的计算可以显著地提高性能。

    之前只能使用 class component 来利用 PureComponent 带来的性能优势

    React.memo() 是什么?

    React.memo() 和 PureComponent 很相似,它帮助我们控制何时重新渲染组件。

    组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是通过 PureComponent 和 React.memo(),我们可以仅仅让某些组件进行渲染。

    const MySnowyComponent = React.memo(function MyComponent(props) {
      // only renders if props have changed!
    });
    
    // can also be an es6 arrow function
    const OtherSnowy = React.memo(props => {
      return <div>my memoized component</div>;
    });
    
    // and even shorter with implicit return
    const ImplicitSnowy = React.memo(props => (
      <div>implicit memoized component</div>
    ));
    // 包裹已有的组件
    const RocketComponent = props => <div>my rocket component. {props.fuel}!</div>;
    // create a version that only renders on prop changes
    const MemoizedRocketComponent = React.memo(RocketComponent);

    React.memo 适用函数式组件

    useMemo本身也有开销。useMemo 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo 可能会影响程序的性能。

    要想合理使用 useMemo,我们需要搞清楚 useMemo 适用的场景:

    • 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。

    • 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。



    虽然 Hooks 看起来更酷炫,更简洁。但是在实际开发中我更倾向于使用 Class 来声明组件。



    参考文章:

    React 16.6 之 React.memo() https://www.jianshu.com/p/9293daab4161

    React Hooks 你真的用对了吗? https://zhuanlan.zhihu.com/p/85969406

    一篇看懂 React Hooks https://zhuanlan.zhihu.com/p/50597236

    React Hooks 不完全总结 https://www.ahonn.me/blog/react-hooks-incomplete-summary



    转载本站文章《React代数效应学习笔记》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2021_0717_8647.html