2020年7月5号(react hook(一))


到目前为止学习和使用hook将近两个月的时间,在做项目的过程中也会遇到关于hook的很多坑,原因是对hook并没有很熟悉的掌握,所以今天打算总结一下关于hook的知识:v:

useState()

模拟实现
1.React.useState() 里都做了些什么:
2.将初始值赋给一个变量我们称之为 state
3.返回这个变量 state 以及改变这个 state 的回调函数我们称之为 setState
4.当 setState() 被调用时, state 被其传入的新值重新赋值,并且更新视图

function useState(initialState) {
  let _state = initialState;
  const setState = (newState) => {
    _state = newState;
    ReactDOM.render(<App />, rootElement);
  };
  return [_state, setState];
}

5.每次更新时,函数组件会被重新调用,也就是说 useState() 会被重新调用,为了使 state 的新值被记录(而不是一直被重新赋上 initialState),需要将其提到外部作用域声明

let _state;
function useState(initialState) {
   _state = _state === undefined ? initialState : _state;
   const setState = (newState) => {
     _state = newState;
     ReactDOM.render(<App />, rootElement);
   };
   return [_state, setState];
}

6.如果添加多个 useState(),将 _state 改成数组存储,让这个数组 _state 根据当前操作 useState() 的索引向内添加 state

let _state = [], _index = 0;
function useState(initialState) {
  let curIndex = _index; // 记录当前操作的索引
  _state[curIndex] = _state[curIndex] === undefined ? initialState : _state[curIndex];
  const setState = (newState) => {
    _state[curIndex] = newState;
    ReactDOM.render(<App />, rootElement);
    _index = 0; // 每更新一次都需要将_index归零,才不会不断重复增加_state
  }
  _index += 1; // 下一个操作的索引
  return [_state[curIndex], setState];
}

而实际上, React 并不是真的是这样实现的。上面提到的 _state 其实对应 React 的 memoizedState ,而 _index 实际上是利用了链表。
React 16.8.0 正式增加了 Hooks ,它为函数组件引入了 state 的能力,换句话说就是使函数组件拥有了 Class 组件的功能。
React.useState() 返回的第二个参数 setState 用于更新 state ,并且会触发新的渲染。同时,在后续新的渲染中 React.useState() 返回的第一个 state 值始终是最新的。
为了保证 memoizedState 的顺序与 React.useState() 正确对应,我们需要保证 Hooks 在最顶层调用,也就是不能在循环、条件或嵌套函数中调用。
React.useState() 通过 Object.is() 来判断 memoizedState 是否需要更新。

每个组件有个对应的fiber节点(可以理解为虚拟DOM),用于保存组件相关信息。
每次FunctionComponent render时,全局变量currentlyRenderingFiber都会被赋值为该FunctionComponent对应的fiber节点。
所以,hook内部其实是从currentlyRenderingFiber中获取状态信息的。currentlyRenderingFiber.memoizedState中保存一条hook对应数据的单向链表。
当FunctionComponent render时,每执行到一个hook,都会将指向currentlyRenderingFiber.memoizedState链表的指针向后移动一次,指向当前hook对应数据。
这也是为什么React要求hook的调用顺序不能改变(不能在条件语句中使用hook) —— 每次render时都是从一条固定顺序的链表中获取hook对应数据的。
我们知道,useState返回值数组第二个参数为改变state的方法。
在源码中,他被称为dispatchAction。
每当调用dispatchAction,都会创建一个代表一次更新的对象update,如果是多次调用dispatchAction,那么,update会形成一条环状链表。

useEffect()

useEffect出现闭包的常见情况

function WatchCount() {
    const [count, setCount] = useState(0)
    useEffect(function() {
        setInterval(function log() {
            console.log(`Count is: ${count}`)
        }, 2000)
    }, [])
    
    return (
      <div>
      {count}
      <button onClick={() => setCount(count + 1)}> 加1 </button>
      </div>
    )
}

当点击按钮是,可以在控制台看到,每两秒打印的count为0,因为在在第一渲染时,log()中闭包捕获count变量的值0。过后,即使count增加,log()中使用的仍然是初始化的值0log()中的闭包是一个过时的闭包

解决方法:让useEffect()知道log()中的闭包依赖于count:

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

  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id);
    }
  }, [count]); // 看这里,这行是重点,count变化后重新渲染useEffect

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}

设置依赖项后,一旦count更改,useEffect()就更新闭包。一定要永远对依赖项诚实

useCallback

产生误区的原因是useCallback的设计初衷并非解决组件内部函数多次创建的问题,而是减少子组件的不必要重复渲染。实际上在 React 体系下,优化思路主要有两种:

  • 1.减少重新 render 的次数。因为 React 最耗费性能的就是调和过程(reconciliation),只要不 render 就不会触发 reconciliation。
  • 2.减少计算量。
import React, { useState } from 'react';

function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = () => {
        setDataB(o => o + 1);
    }
    
    return <div>
        <A onClick={onClickA}>组件A:{dataA}</div>
        <B onClick={onClickB}>组件B:{dataB}</Expensive>
    </div>
}

b是一个渲染成本非常高的组件,但点击A组件也会导致B重新渲染,即使dataB并未发生改变。原因就是onClickB被重新定义,导致 React 在 diff 新旧组件时,判定组件发生了变化。这时候useCabllbackmemo就发挥了作用:

import React, { useState, memo, useCallback } from 'react';

function B({ onClick, name }) {
  console.log('B渲染');
  return <div onClick={onClick}>{name}</div>
}

const MemoB = memo(B);

function A({ onClick, name }) {
  console.log('A渲染');
  return <div onClick={onClick}>{name}</div>
}

export default function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = useCallback(() => {
        setDataB(o => o + 1);
    }, []);
    
    return <div>
        <A( onClick={onClickA} name={`组件A:${dataA}`}/>
        <B onClick={onClickB} name={`组件B:${dataB}`} />
    </div>
}

所以useCallback保证了onClickB不发生变化,此时点击A组件不会触发B组件的刷新,只有点击B组件才会触发。在实现减少不必要渲染的优化过程中

useMemo

之前在项目中也不很清楚useMemo的作用,只知道是缓存变量,导致感觉代码写了很多废话,也并没有达到减少代码,提升性能的作用

useMemo是针对一个函数,是否多次执行
useMemo主要用来解决使用React hooks产生的无用渲染的性能问题
在方法函数,由于不能使用shouldComponentUpdate处理性能问题,react hooks新增了useMemo

如果useMemo(fn, arr)第二个参数匹配,并且其值发生改变,才会多次执行执行,否则只执行一次,如果为空数组[],fn只执行一次

举例说明:

第一次进来时,控制台显示rich child,当无限点击按钮时,控制台不会打印rich child。但是当改变props.name为props.isChild时,每点击一次按钮,控制台就会打印一次rich child


export default () => {
	let [isChild, setChild] = useState(false);
 
	return (
		<div>
			<Child isChild={isChild} name="child" />
			<button onClick={() => setChild(!isChild)}>改变Child</button>
		</div>
	);
}
 
let Child = (props) => {
	let getRichChild = () => {
		console.log('rich child');
 
		return 'rich child';
	}
 
	let richChild = useMemo(() => {
		//执行相应的函数
		return getRichChild();
	}, [props.name]);
 
	return (
		<div>
			isChild: {props.isChild ? 'true' : 'false'}<br />
			{richChild}
		</div>
	);
}

useRef()和createRef()

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。

生成的ref拥有current属性,且如果没有显示改变current值,那么 ref的 current值 永远不会改变!无论组件被重渲染多少次!

usePrevious就是一个自己封装的custom-hook ,完全可以直接写在内部。

const exampleComponent = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, value);
  return <h1>{ref.current}</h1>
}

接着就可以分析一下生命周期中到底发生了什么:

  1. 第一次exampleComponent被执行,函数参数value为1,ref.current先是undefined (因为这时useEffect还没有被调用),然后根据return的JSX,渲染DOM,页面上被渲染出ref.current的值 -> undefined,接着 useEffect被调用,此时ref 的current值被赋值,是这次渲染的props -> value,也就是1。
  2. 第二次exampleComponent被再次调用,函数参数value变成了2,依旧是先根据return的JSX渲染DOM,还记得吗,第一次exampleComponent被调用的后期,ref.current变成了1,因为没有任何显示改变ref.current的操作,所以页面中渲染出 1 ,渲染完成以后,开始老步骤,执行useEffect,这一次的函数参数value是2,所以ref.current变成了2。
  3. 第三次就不用多说了吧,其实关键点在于理解react-hooks的生命周期,useEffect是在渲染DOM结束以后才会调用!

useRef 可以很好的解决闭包带来的不方便性.

值得注意的是,当 useRef 的内容发生变化时,它不会通知您。更改.current属性不会导致重新呈现。 因为他一直是一个引用 .

useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。


Author: wxy
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source wxy !
  TOC