React之函数组件及常用hook核心原理

94 阅读9分钟

1.渲染原理

  • jsx基于@babel/preset-react-app,将jsx文件编译成ast语法树,再解析ast对象拼接成React.createElement的函数
  • 调用React.createElement生成virtualDom
  • 调用render方法传入virtualDom和container,渲染真实dom;
    • 16调用ReactDom.render(virtualDom,container)
    • 18调用React.createRoot(container).render(virtualDom)

2.函数组件渲染/更新

函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的“私有上下文”!

  • 重新执行函数时,内部的代码也需要重新执行
  • 涉及的函数需要重新的构建{这些函数的作用域(函数执行的上级上下文),是每一次执行组件产生的闭包}
  • 每一次执行函数组件,也会把useState重新执行,但是:
    • 执行useState,只有第一次,设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值「而不是初始值」
    • 返回的修改状态的方法,每一次都是返回一个新的
import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';

const Demo = function Demo() {
    console.log('RENDER渲染 产生新的闭包');
    let [x, setX] = useState(10),
    const handle = () => {
        setX(x + 1);
    };

    return <div className="demo">
        <span className="num">x:{x}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

export default Demo;

3.useState

  1. useState自带了性能优化的机制:
  • 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于Object.is作比较」
  • 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类似于PureComponent,在shouldComponentUpdate中做了浅比较和优化」
  1. useState的更新机制
  • 18版本中:当前同步任务中的所有操作,都会先存放到一个数组中,始终在下一个js事件队列中批量更新,所以它始终是异步的;类组件的setState也是同样的原理!
  • 16版本中:也会收集存放到数组中,只是它只在唯一的一次微任务中执行;如果你在同步任务中调用,它就是异步的;如果你在异步中调用,它就成了同步的;如果你在定时器中调用,由于定时器在微任务后执行,所以会跳过微任务,再执行时就成了同步!
import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';

const Demo = function Demo() {
    console.log('RENDER渲染 产生新的闭包');
    let [num, setNum] = useState(0);
    
    const handle1 = () => {
        setNum(100);
        setTimeout(() => {
            console.log(num); // 0 始终是上一次函数执行产生的闭包中的数据
        }, 1000);
        
        setTimeout(() => {
            // 18版本永远会在下一个任务队列中执行!
            // 16版本只会在一个微任务中执行,再往后的都是同步!
            setNum(200) 
        }, 2000);
    };
    
    const handle = () => {
        for (let i = 0; i < 10; i++) {
            setNum(num + 1); // 会被批处理合并成一次 最后永远是1
        }
    };
    
    return <div className="demo">
        <span className="num">{num}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

export default Demo;
  1. 如果传递的是函数,函数中可以接收上一次的state,和类组件中的setState一样
// 如何在每一次循环中都递增数据,而不是只递增一次?
const handle = () => {
    for (let i = 0; i < 10; i++) {
        setX((prev) => {
          console.log(prev); // prev:存储上一次的状态值
          return prev + 1; //返回的信息是我们要修改的状态值
        });
    }
};
  1. 使用flushSync可以立即清空一次setState队列
const handle = () => {
    // 立即刷新任务队列
    flushSync(() => {
        setX(x + 1);
        setY(y + 1);
    });
    setZ(z + 1);
};
  1. 对useState初始值的优化

每次更新状态都会重新执行函数组件中的所有内容,虽然useState对初始值做了保护,如果初始值依赖于某个props属性,也会导致其重复设置初始值;


// 每一次都会重复执行这一坨
let { x, y } = props, total = 0;
for (let i = x; i <= y; i++) {
    total += +String(Math.random()).substring(2);
}

let [num, setNum] = useState(total)

// 改成下面这种形式
let [num1, setNum1] = useState(() => {
    let { x, y } = props,
      total = 0;
    for (let i = x; i <= y; i++) {
      total += +String(Math.random()).substring(2);
    }
    return total;
});
  1. 简单实现useState
var _state;
function useState(initialValue) {
    if (typeof _state === "undefined") {
        if(typeof initialValue==="function"){
            // 初始值如果是函数,就调用该函数获取返回值
            _state = initialValue();
        }else{
            _state = initialValue
        }
    };
    var setState = function setState(value) {
        if(Object.is(_state,value)) return;
        if(typeof value==="function"){
            // 如果传进来的是函数,就把上一次的状态传过去
            _state = value(_state);
        }else{
            _state = value;
        }
        // 通知视图更新
    };
    return [_state, setState];
}
  1. 实现一个partialState

useState的setState没有类组件中的setState那样拥有partailState能力,每次修改状态都是全量替换,所以每次都需要拷贝一份,非常麻烦!

export function usePartailState<T>(
  initState: T
): [T, (state: T extends object ? Partial<T> : T) => void] {
  const [state, setState] = useState<T>(initState);
  function setPartailState(newState: any) {
    if (typeof state === "object" && state !== null) {
      if (!isEqual(state, newState)) {
        if (Array.isArray(newState) && Array.isArray(state)) {
          newState = [...state, ...newState];
        } else {
          newState = { ...state, ...newState };
        }
        setState(newState);
      }
    } else {
      setState(newState);
    }
  }
  return [state, setPartailState];
}

4.useEffect

  • 在函数组件中使用的每一个useEffect,都会被MountEffect函数将effect接收的回调函数,收集到一个effect链表中
  • 每一次函数重新执行后(状态更新),都会调用updateEffect函数,取出并执行链表中的effect回调函数
  1. 4种形态
useEffect(() => {
    // useEffect(callback):没设置依赖
    // + 第一次渲染完毕后,执行callback,等价于 componentDidMount
    // + 在组件每一次更新完毕后,也会执行callback,等价于 componentDidUpdate
    console.log("@1", num);
});

useEffect(() => {
    // useEffect(callback,[]):设置了,但是无依赖
    //  + 只有第一次渲染完毕后,才会执行callback
    //  + 等价于 componentDidMount
    console.log("@2", num);
}, []);

useEffect(() => {
    // useEffect(callback,[依赖的状态(多个状态)]):
    //  + 第一次渲染完毕会执行callback
    //  + 当依赖的状态值(或者多个依赖状态中的一个)发生改变,也会触发callback执行
    //  + 但是依赖的状态如果没有变化,在组件更新的时候,callback是不会执行的
    console.log("@3", num);
}, [num]);

useEffect(() => {
    return () => {
      // 返回的小函数,会在组件释放的时候执行
      // 如果组件更新,会把上一次返回的小函数执行「可以“理解为”上一次渲染的组件释放了」
      console.log("@4", num);
    };
});
  1. useEffect接收的函数不能是async

因为加了async后,其函数就成了一个promise实例!而React需要的是可以被调用的普通函数!

// 这样写是不行的!
useEffect(async () => {
    let data = await queryData();
    console.log('成功:', data);
}, []); */

// 需要改成这样
useEffect(() => {
    queryData()
        .then(data => {
            console.log('成功:', data);
        });
}, []);

// 或者这样
const getXXX = async () => {
    let data = await queryData();
    console.log('成功:', data);
};
useEffect(() => {
    getXXX();
}, []);
  1. useEffect和useLayoutEffect的区别
  • useLayoutEffect会阻塞浏览器渲染真实DOM,优先执行Effect链表中的callback;
  • useEffect不会阻塞浏览器渲染真实DOM,在渲染真实DOM的同时,去执行Effect链表中的callback;
  • useLayoutEffect设置的callback要优先于useEffect去执行!!
  • 在两者设置的callback中,依然可以获取DOM元素「原因:真实DOM对象已经创建了,区别只是浏览器是否渲染」
  • 如果在callback函数中又修改了状态值「视图又要更新」
    • useEffect:浏览器肯定是把第一次的真实dom已经绘制了,再去渲染第二次真实DOM
    • useLayoutEffect:浏览器是把两次真实DOM的渲染,合并在一起渲染的
useLayoutEffect(() => {
    // 一个超大的循环,将阻塞渲染,多久渲染出来,取决于电脑性能多久能把这个循环执行完!
    for (let i = 0; i < 1000000000000; i++) {}
    console.log("useLayoutEffect"); //第一个输出
  }, [num]);
  useEffect(() => {
    console.log("useEffect"); //第二个输出
  }, [num]);

5.useRef

  1. 类组件获取ref的3种方式
class Demo extends React.Component {
  ref1 = React.createRef();
  ref2 = null;

  logRef = () => {
    console.log(ref1, ref2, this.refs.ref3);
  };

  render() {
    return (
      <div ref={ref1}>
        <div ref={(ref) => (this.ref2 = ref)}>
          <div ref="ref3"></div>
        </div>
      </div>
    );
  }
}
  1. 函数组件获取ref的3种方式
const Demo = function Demo() {
  let ref1 = useRef(null);
  let ref2 = React.createRef();
  let ref3;

  useEffect(() => {
    console.log(ref1,ref2,ref3);
  }, []);

  return (
    <div ref={ref1}>
      <div ref={ref2}>
        <div ref={(ref) => (ref3 = ref)}></div>
      </div>
    </div>
  );
};
  1. useRef和createRef的区别

useRef只会在组件第一次渲染时创建,后面每一次更新都是取的缓存中的;只能在函数组件中使用!

而createRef在函数组件中使用时,由于函数组件每次更新都会全部执行一次,所以每次都会重新创建一个新的ref对象;但是在类组件中使用时,类组件只更新的是render中的,不会重新new,所以也只执行一次;

所以:在函数组件中虽然可以使用createRef,但是不推荐使用!还是用为函数组量身定制的useRef最佳!

let prev1,prev2;
const Demo = function Demo() {
    let [num, setNum] = useState(0);
    let box1 = useRef(null), box2 = React.createRef();
    
    if (!prev1) {
        // 第一次DEMO执行,把第一次创建的REF对象赋值给变量
        prev1 = box1;
        prev2 = box2;
    } else {
        console.log(prev1 === box1); //true  
        console.log(prev2 === box2); //false 
    }

    return <div className="demo">
        <span className="num" ref={box1}>{num}</span>
        <span className="num" ref={box2}>哈哈哈</span>
        <Button type="primary" size="small"
            onClick={() => {
                setNum(num + 1);
            }}>
            更新视图
        </Button>
    </div>;
};

6.useImperativeHandle

写在类组件上面的ref,能够获取到类组件的实例;而函数组件没有实例,无法直接通过ref获取函数组件的ref;

React.forwardRef能对函数组件做ref转发,将ref对象传递给函数子组件

useImperativeHandle函数能将指定数据回传给父组件,搭配React.forwardRef就能实现ref的传递

const Child = React.forwardRef(function Child(props, ref) {
  let [text, setText] = useState("你好世界");
  const submit = () => {};

  useImperativeHandle(ref, () => {
    // 在这里返回的内容,都可以被父组件的REF对象获取到
    return {
      text,
      submit,
    };
  });

  return (
    <div className="child-box">
      <span>哈哈哈</span>
    </div>
  );
});

const Parent = () => {
  const childRef = useRef(null);

  useEffect(() => {
    console.log(childRef.current);
  }, []);

  return <Child ref={childRef} />;
};

7.useMemo

对数据做缓存!

具备“计算缓存”,在依赖的状态值没有发生改变,callback没有触发执行的时候,获取的是上一次计算出来的结果;和Vue中的计算属性非常的类似!!

有时候我们只改变了某一个状态,并不想让其它没变化的数据做不必要的更新!

const Demo = function Demo() {
    let [supNum, setSupNum] = useState(10),
        [oppNum, setOppNum] = useState(5),
        [x, setX] = useState(0);

    /* 
     let xxx = useMemo(callback,[dependencies])
       + 第一次渲染组件的时候,callback会执行
       + 后期只有依赖的状态值发生改变,callback才会再执行
       + 每一次会把callback执行的返回结果赋值给xxx
       + useMemo具备“计算缓存”,在依赖的状态值没有发生改变,
       + callback没有触发执行的时候,xxx获取的是上一次计算出来的结果
       + 和Vue中的计算属性非常的类似!!
     */
    let ratio = useMemo(() => {
        let total = supNum + oppNum,
            ratio = '--';
        if (total > 0) ratio = (supNum / total * 100).toFixed(2) + '%';
        return ratio;
    }, [supNum, oppNum]);

    return <div className="vote-box">
        <div className="main">
            <p>支持人数:{supNum}人</p>
            <p>反对人数:{oppNum}人</p>
            <p>支持比率:{ratio}</p>
            <p>x:{x}</p>
        </div>
        <div className="footer">
            <Button type="primary" onClick={() => setSupNum(supNum + 1)}>支持</Button>
            <Button type="primary" danger onClick={() => setOppNum(oppNum + 1)}>反对</Button>
            <Button onClick={() => setX(x + 1)}>干点别的事</Button>
        </div>
    </div>;
};

export default Demo;

8.useCallback

对函数做缓存!

函数一般都是常量不会发生变化,但是由于函数组件每次更新都会全部执行一遍,所以每次都会重新创建一个函数,而我们并不想让它重复创建新的函数!

const Demo = function Demo() {
    let [x, setX] = useState(0);

    // const handle = () => { };  //第一次:0x001  第二次:0x101  第三次:0x201 ...
    const handle = useCallback(() => { }, []); //第一次:0x001  第二次:0x001  第三次:0x001 ...

    return <div className="vote-box">
        <Child handle={handle} />
        <div className="main">
            <p>{x}</p>
        </div>
        <div className="footer">
            <Button type="primary" onClick={() => setX(x + 1)}>累加</Button>
        </div>
    </div>;
};

9.React.memo

对函数组件添加类似PureComponent的能力!

假如我们对函数组件传递一个不会发生变化的参数,例如基于useCallback的函数,在父组件更新时,子组件并没有发生状态变化,此时我们并不希望子组件更新;但是由于父组件重新执行,就一定会重新调用子组件!子组件就会产生不必要的更新!

// 类组件的PureComponent会添加一个shouldComponentUpdate周期函数,并对新老状态和props做钱比较!
class Child extends React.PureComponent {
    render() {
        console.log('Child Render'); // 父组件更新不会触发这里
        return <div>我是子组件</div>;
    }
}

// 函数组件使用memo包裹,也类似PureComponent
const Child1 = React.memo(function Child1(props) {
    console.log('Child Render1'); // 父组件更新不会触发这里
    return <div>我是子组件1</div>;
});

// 普通函数组件,不加memo,每一次都会触发更新
const Child2 = function Child2(props) {
    console.log('Child Render2'); // 每一次都有不必要的更新
    return <div>我是子组件2</div>;
};

const Demo = function Demo() {
    let [x, setX] = useState(0);
    const handle = useCallback(() => { }, []); 

    return <div className="vote-box">
        <Child handle={handle} />
        <Child1 handle={handle} />
        <Child2 handle={handle} />
        <div className="main">
            <p>{x}</p>
        </div>
        <div className="footer">
            <Button type="primary" onClick={() => setX(x + 1)}>累加</Button>
        </div>
    </div>;
};

10.useMemo和useCallback慎用原因

如果你的数据每次必然会发生变化,那么实际上用了反而会增加多余的步骤!并且可能会出现一些其它问题!

11.自定义hook

export const useDidMount = function useDidMount(callback) {
  useEffect(() => {
    if (typeof callback === "function") {
      callback();
    }
  }, []);
};