一、hooks基本用法

1、react-hooks用法官网有,我们直接看一个栗子

import React, {useState} from 'react';

// 子组件
const Child = ({data, addCount}: { data: any, addCount: any }) => {
    console.log('child render'); // 标记一下,判断组件是否渲染
    return <button onClick={addCount}>{data.count}</button>;
};

const Index = (props: any) => {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('hx');
    
    let data = {count}
    
    const addCount = () => {
        setCount(count + 1);
    }
    return <div>
        <Child data={data} addCount={addCount}/>
        
        <input type="text" value={name} onChange={(e: any) => setName(e.target.value)}/>
    </div>;
};

export default Index;

以上就是hook一个基本用法,Child组件只是展示data数据,input可以改变name,展示效果如下
图片.png

2、性能问题

多余渲染问题

  • 有一个问题,就是当改变input的值时,控制台打印出了 "child render", 这是为什么,改变input的值只是改变了name的值,而data.count并没有变
  • 这里就需要优化一下,我们需要只有需要渲染时才去渲染,如果dom中的变量值没发生改变,就不需要渲染

那到底是什么原因导致了没必要的渲染?

  • 我们知道我们每次改变state的值,就会触发函数组件内部重新执行,并且如果渲染需要的变量发生了变化,就会触发渲染
  • 其中下面代码,也就是data和addCount每次执行都会被重新赋值一下,即使count没有发生变化,函数也没有发生变化,但是由于函数和对象都是引用类型,重新赋值后,其实值已经发生了变化,所以child才会一直被渲染

    let data = {count}  
    const addCount = () => {
      setCount(count + 1);
    }

    我们优化一下,这里用到了react-hooks中的 memo, useMemo, useCallback

    // + 引入 memo,useMemo,useCallback
    import React, { useState, memo,useMemo,useCallback} from 'react';
    
    const Child = ({data, addCount}: { data: any, addCount: any }) => {
      console.log('child render');
      return <button onClick={addCount}>{data.count}</button>;
    };
    
    // + memo函数包裹Child组件,会判断属性有没有变化,没变化就不渲染
    const MyChild = memo(Child);  
    
    const Index = (props: any) => {
      const [count, setCount] = useState(0);
      const [name, setName] = useState('hx');
      // 重点 useMemo 表示依赖不变,就不会返回新的变量
      let data = useMemo(() => ({count}), [count]); 
      // 重点 useCallback 表示依赖不变,就不会返回新的函数
      const addCount = useCallback(() => { 
          setCount(count + 1);
      }, [count]);
    
      return <div>
          <MyChild data={data} addCount={addCount}/>
          <input type="text" value={name} onChange={(e: any) => setName(e.target.value)}/>
      </div>;
    };
    
    export default Index;
  • 用useMemo处理引用类型的变量,此函数会根据count前后两次做对比,查看有没有发生变化,没有变化,会将原来的值付给 data,useMemo需要和memo搭配使用
  • 用useCallback也是一样道理,如果count没发生变化,会将前一次的函数赋给addCount, 从而不触发渲染

    二、useEffect和useLayoutEffect

    1、useEffect和useLayoutEffect区别

    import React, { useState,useEffect,useLayoutEffect, useRef } from 'react';
    // import { useState } from './myHooks.ts';
    
    const IndexView = (props: any) => {
      const ref = useRef<any>();
      const style = {
          width: '100px',
          height: '100px',
          background: 'red'
      };
    
      useEffect(()=>{
          if(ref.current){
              ref.current.style.transform=`translate(500px)`
              ref.current.style.transition=`all 500ms`
          }
      },[]);
    
      return <div style={style} ref={ref}>内容</div>
    };
    
    export default IndexView;

    image
    可以看出动画是正常的移动到了右边

但是,我们将useEffect换成useLayoutEffect,就失去了动画效果
图片.png
原因就是useEffect是副作用,会等到第一次渲染之后才会执行,而useLayoutEffect是在渲染之前执行的,其实就是useEffect是宏任务,useLayoutEffect是微任务,具体可看 js运行机制 一文

另外,也可以看一下下图浏览器运行机制
浏览器事件循环
总之

  • useEffect会等渲染完成后更新值 state => 渲染 => 执行effect => 渲染
  • useLayoutEffect 会先更新值再渲染 state => 执行effect => 渲染

    2、动手写一下uesEffect和useLayoutEffect

    我们已经看到下发代码中执行回调的方式,分别是setTimout(宏任务)和promise.then(微任务)

    // useEffect
    let lastEffectDependencies: any[];
    function useEffect(callback: any, dependencies: any[]) {
      if (lastEffectDependencies) {
          const isChange = !dependencies.every((item: any, index: number) => {
              return item === lastEffectDependencies[index];
          });
          if (isChange) {
              setTimeout(callback); // 宏任务
              lastEffectDependencies = dependencies;
          }
      } else {
          setTimeout(callback);
          lastEffectDependencies = dependencies;
      }
    }
    
    // useLayoutEffect
    let lastLayoutEffectDependencies: any[];
    function useLayoutEffect(callback: any, dependencies: any[]) {
      if (lastLayoutEffectDependencies) {
          const isChange = !dependencies.every((item: any, index: number) => {
              return item === lastLayoutEffectDependencies[index];
          });
          if (isChange) {
              Promise.resolve().then(callback); //微任务 或者queueMicrotask
              lastLayoutEffectDependencies = dependencies;
          }
      } else {
          Promise.resolve().then(callback);
          lastLayoutEffectDependencies = dependencies;
      }
    }

    另外,我们知道useEffect是不能放到if语句中的; 因为useEffect是可以写多个的,从上述代码中我们也得知,每个Effect的依赖是靠lastEffectDependencies数组维护,而每次执行hooks函数,执行到effect时,都是靠数组index去找对应的值做对比;如果写到了if中,那这种对应关系就错乱了。

    三、其它hooks函数简单手写

    1、useRef

    let lastRef: any;
    export function useRef(initRef: any) {
      lastRef = lastRef || initRef;
      return {
          current: lastRef
      };
    }

    2、useState

    let lastState: any[] = [];
    let index = 0;
    
    export function useState(initState: any) {
      lastState[index] = lastState[index] || initState;
      const currentIndex = index;
    
      function setState(newState: any) {
          lastState[currentIndex] = newState;
          render();  // render函数, 可以理解 ReactDOM.render
          index = 0; // setState后,会重新执行hook函数,并触发渲染,也就是**useState也会重新执行,因此index要重置为0**
      }
      return [lastState[index++], setState]; // 数组为了数据结构
    }

    疑问:为什么useState要用数组去维护value和set? 从上述我们知道其实为了能够结构赋值

    [value, setValue] = [lastState[index++], setState]

    3、useCallback

    let lastCallback: any;
    let lastCallbackDependencies: any[];
    
    function useCallback(callback: any, dependencies: any[]) {
      if (lastCallbackDependencies) {
          const isChange = !dependencies.every((item: any, index: number) => {
              return item === lastCallbackDependencies[index];
          });
          if (isChange) {
              lastCallback = callback;
              lastCallbackDependencies = dependencies;
          }
      } else {
          lastCallback = callback;
          lastCallbackDependencies = dependencies;
      }
    
      return lastCallback;
    }

    4、useNemo

    let lastMemo: any;
    let lastMemoDependencies: any[];
    
    export function useMemo(callback: any, dependencies: any[]) {
      if (lastMemoDependencies) {
          const isChange = !dependencies.every((item: any, index: number) => {
              return item === lastMemoDependencies[index];
          });
          if (isChange) {
              lastMemo = callback();
              lastMemoDependencies = dependencies;
          }
      } else {
          lastMemo = callback();
          lastMemoDependencies = dependencies;
      }
    
      return lastMemo;
    }

    提一下,useNemo维护的是变量,为什么要用回调函数呢?因为使用useNemo的变量依赖的是另外的数据,如果依赖的数据发生变化,我们需要让变量也随之变化,这就需要通过函数调用才能实现;否则直接写死了,变量是不会随之变化的

    5、useReducer

    let lastReducerState: any;
    export function useReducer(reducer: any, state: any) {
      lastReducerState = lastReducerState || state;
    
      function dispatch(action: any) {
          lastReducerState = reducer(lastReducerState, action);
          render();
      }
    
      return [lastState, dispatch];
    }

    6、useContext

    function useContext(context: any) {
      return context.__currentValue;
    }

参考文章

useEffect 和 useLayoutEffect 的区别
从零手写实现 React Hooks

系列

重学react——slot
重学react——state和生命周期
重学react——redux
重学react——hooks以及原理
重学react——context/reducer
重学react——router
重学react——高阶组件
build your own react
React——fiber


lihaixing
463 声望719 粉丝

前端就爱瞎折腾