ReactHooks


Hooks

Hooks 让函数式组件拥有了 生命周期、状态管理、逻辑复用等,

所有作为组件应当具备的能力,并避开了class式的写法。

什么是hooks

Hooks直译为钩子,通常指,系统运行到某一时期时,会调用被注册到该时机的回调函数

如: window.onloadaddEventListener注册的回调函数

hooks规范

use + 大写字母的单词就是hooks,只能在react函数组件中调用hook,

只能在最顶层调用hooks,不能在 条件语句 / 嵌套函数 调用

为什么需要hooks

大佬的reactHooks源码解析,存一下回头看

React官网-为什么我们创造了Hooks

useState

给函数组件添加 重复渲染时可读取的 内部state

参数: 初始state
返回值: 当前状态 和 一个用于更新它的函数。(类似this.setState,但不会合并新旧state)

import React from './react';

function App(){
    // 初始化的时候number就是0,后续更新渲染,useState返回的就是setNumber改变后的值
    const [number,setNumber] = React.useState(0); // 重新渲染的时候返回的number不一样了
    let handleClick = () => setNumber(number+1) // setNumber调用之后会重新渲染,重新渲染的时候
    return (
        <div>
            <p>{number}</p>
            <button onClick={handleClick}>+</button>
        </div>
    )
}

简单实现

(实际上是用了fiber,本质上是链表,非数组储存,且更复杂)

一个 全局数组hookStates 一个 全局下标hookIndex 初始为0.

初始化时,

整个应用的 useState 随着组件初始化被依次调用,

每个useState 将数据都放在 hookIndex 对应的 hookStates[hookIndex]中, 最后 hookIndex++.

也就是说 hookStates[0] 对应第一个 useState, hookStates[1] 对应第二个 useState,以此类推.

并且,每个 useState 记录下 currentIndex = hookIndex, 也就是自己数据存放的位置,

当,某个 useStatesetState 被调用时就修改 hookStates[currentIndex] 的数据,并触发更新.

更新时,

再次将 全局变量 hookIndex 置为 0,

这样 第一个 useState 返回 hookStates[hookIndex],并最后 hookIndex++.

而 第二个 useState 执行时,又能再次读到 1 位置的数据,并返回.

这也是为什么react 不允许 条件调用useState,必须保证每次useState顺序相同

useState只会覆盖原值,不会合并原值

let hookStates = []; // 一个全局对象存放了所有函数组件的 useState
let hookIndex = 0;
let scheduleUpdate;
function render(vdom, container) {
    mount(vdom,container);
    scheduleUpdate = ()=>{
        hookIndex = 0; // 把索引置为0,再次渲染,按照渲染顺序,每个函数组件又会从useState拿到自己的最新state
        compareTwoVdom(container,vdom,vdom); // 同一个vdom进行比较,但是其子节点state已不一样
    }
}
export function useState(initialState){
    // 初次渲染的时候,返回初始值,和 setState。再次渲染函数组件时 还会调用useState,返回的就是 最新state
    hookStates[hookIndex] = hookStates[hookIndex]||initialState;
    let currentIndex = hookIndex;
    function setState(newState){
        let newState = typeof action === 'function' ? action(oldState) : action;
        hookStates[currentIndex] = newState;
        scheduleUpdate();
    }
    return [hookStates[hookIndex++],setState];
    // 每存放完一个state,hookIndex+1,按渲染顺序放好 每个函数组件的state
    // 更新渲染的时候每个函数组件都会 再次依次按顺序调用 useState,hookIndex按老顺序++
    // 每个函数组件都会拿到自己那份 最新的hookStates[hookIndex]值 并返回
}

useState返回的函数setState调用更新 compareTwoVdom,然后 updateChildren,会比较每个child的props是否有变更

而 假设你的state是一个对象,修改的又只是对象下的属性的话, 在 updateChildren 内会认为该对象没有变化,而可能不更新UI

修改对象下的某个属性,是不会被react监控到并更新渲染UI的

// false
const onClick = ()=>{
    user.name='marry';
    setUser(user);
}

这里是怕对象没改变导致UI不更新,而下面两个是怕对象改变导致UI更新

useCallback + useMemo

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,依赖项改变时返回新对象。
把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

import React from 'react';

let  Child = ({data,handleClick})=>{
  console.log('Child render');
  return (
     <button onClick={handleClick}>{data.number}</button>
  )
}
Child = React.memo(Child);

function App(){
  const[name,setName] = React.useState('lzy');
  const[number,setNumber]=React.useState(0);

  let data = React.useMemo(()=>({number}),[number]); // []数组内的数据变动时,才重新调用函数,返回 新对象
  let handleClick = React.useCallback(()=> setNumber(number+1),[number]);// []数组内的数据变动时,返回 新函数
    //   let data = { data }
    //   let handleClick = () => setNumber(number + 1)
    // 如果按照注释里的写法,输入框的值,修改时,再次渲染函数组件,又会生成新的data,新的handleClick
    // react 在 updateChildren 的时候,就判定Child的组件的props发生了修改,这样就会重新渲染Child组件
    // 而事实上,name属性的修改,并没有涉及Child组件的props

    // 而 用上了 useMemo,useCallback 就可以保证 设定的依赖值没变时,返回的还是 第一次创建的对象.
  return (
    <div>
      <input type="text" value={name} onChange={event=>setName(event.target.value)}/>
      <Child data={data} handleClick={handleClick}/>
    </div>
  )
}

原理
注意,dependencies 是潜比较

export  function useMemo(factory,dependencies){
    if(hookStates[hookIndex]){ // 如果以前有,就进行比较
        let [lastMemo,lastDeps] = hookStates[hookIndex];
        let same = dependencies.every((item,index)=>item === lastDeps[index]);
        if(same){ // 数组中设定的依赖值每个都没变,就返回以前的对象
            hookIndex++;
            return lastMemo;
        }else{ // 有依赖项变了,就返回新的
            let newMemo = factory();
            hookStates[hookIndex++]=[newMemo,dependencies];
            return newMemo;
        }
    } else { // 如果没有,就保存,并返回初始值
      let newMemo = factory();
      hookStates[hookIndex++]=[newMemo,dependencies];
      return newMemo;
    }
}

useReducer

useState继承自useReducer,

接收一个 reducer:(state, action) => newState,返回[state, dispatch]

在调用dispatch(action)传入action,会传给 reducer,reducer 根据action 处理 oldState

useState不同,非直接覆盖oldState,

可通过reducer事先设定处理方式,可获取到oldState,并根据action处理更多种更复杂的情况,

function reducer(state={number:0}, action) {
  switch (action.type) {
    case 'ADD':
      return {number: state.number + 1};
    case 'MINUS':
      return {number: state.number - 1};
    default:
      return state;
  }
}

function Counter(){
    const [state, dispatch] = React.useReducer(reducer,{number:0});
    return (
        <div>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'ADD'})}>+</button>
          <button onClick={() => dispatch({type: 'MINUS'})}>-</button>
        </div>
    )
}

简单实现原理

export function useReducer(reducer, initialState) {
    hookStates[hookIndex] = hookStates[hookIndex] || initialState;
    let currentIndex = hookIndex;
    function dispatch(action) {
        //1.获取老状态
        let oldState = hookStates[currentIndex];
        //如果有reducer就使用reducer计算新状态
        if (reducer) {
            let newState = reducer(oldState, action); // 给reducer传入action
            hookStates[currentIndex] = newState;
        } else {
            //判断action是不是函数,如果是传入老状态,计算新状态
            let newState = typeof action === 'function' ? action(oldState) : action;
            hookStates[currentIndex] = newState;
        }
        scheduleUpdate();
    }
    return [hookStates[hookIndex++], dispatch];
}

useContext

useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

接收一个context对象,读取订阅 context,需要在上层组件树中使用 <MyContext.Provider value={}> 来为下层组件提供 context

const CounterContext = React.createContext();

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {number: state.number + 1};
    case 'minus':
      return {number: state.number - 1};
    default:
      return state;
  }
}
function Counter(){
  let {state,dispatch} = React.useContext(CounterContext);
  return (
      <div>
        <p>{state.number}</p>
        <button onClick={() => dispatch({type: 'add'})}>+</button>
        <button onClick={() => dispatch({type: 'minus'})}>-</button>
      </div>
  )
}
function App(){
    const [state, dispatch] = React.useReducer(reducer, {number:0});
    return (
        <CounterContext.Provider value={{state,dispatch}}>
          <Counter/>
        </CounterContext.Provider>
    )
}

简单实现

function useContext(context){
    return context._currentValue;
}

useEffect

useEffect 操作副作用,其接收的函数会在组件渲染完成后执行.

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志

以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性.

function Counter() {
    const [number, setNumber] = React.useState(0);
    React.useEffect(() => {
        console.log('开启一个新的定时器')
        const $timer = setInterval(() => {
            setNumber(number => number + 1);
        }, 1000);
        return () => {
            console.log('销毁老的定时器');
            clearInterval($timer);
        }
    });
    return (
        <p>{number}</p>
    )
}

简单实现
注意,dependencies 是浅比较

export function useEffect(callback,dependencies){
    let currentIndex = hookIndex;
    if(hookStates[hookIndex]){
        let [destroy,lastDeps] = hookStates[hookIndex];
        let same = dependencies&&dependencies.every((item,index)=>item === lastDeps[index]);
        if(same){
            hookIndex++;
        }else{
            destroy&&destroy();
            setTimeout(()=>{
                hookStates[currentIndex]=[callback(),dependencies];
            });
            hookIndex++;
        }
    }else{
        setTimeout(()=>{
            hookStates[currentIndex]=[callback(),dependencies];
        });
        hookIndex++;
    }
}

useLayoutEffect + useRef

useEffect 函数会放入宏任务队列,

useLayoutEffect 函数会放入微任务队列,

浏览器绘制属于宏任务,useLayoutEffect会在能拿到DOM,但浏览器未开始绘制时执行

使用

const Animate = ()=>{
    const ref = React.useRef();
    React.useLayoutEffect(() => {
      ref.current.style.transform = `translate(500px)`;//TODO
      ref.current.style.transition = `all 500ms`;
    });
    let style = {
      width: '100px',
      height: '100px',
      borderRadius: '50%',
      backgroundColor: 'red'
    }
    return (
      <div style={style} ref={ref}></div>
    )
}

useRef只是创建了一个对象,然后将该对象保存在了链表里以后能拿到而已

reactDOM 在执行渲染时会给这个对象绑定上该组件对应的真实DOM.

export function useLayoutEffect(callback,dependencies){
    let currentIndex = hookIndex;
    if(hookStates[hookIndex]){ 
        let [destroy,lastDeps] = hookStates[hookIndex];
        let same = dependencies&&dependencies.every((item,index)=>item === lastDeps[index]);
        if(same){
            hookIndex++;
        }else{
            destroy&&destroy();
            queueMicrotask(()=>{
                hookStates[currentIndex]=[callback(),dependencies];
            });
            hookIndex++
        }
    }else{ // 如果原本没有,就存起来
        queueMicrotask(()=>{ // 存的时候,按微任务的顺序存,拿的时候也按微任务的顺序拿.
            hookStates[currentIndex]=[callback(),dependencies];
        });
        hookIndex++;
    }
}
export function useRef(initialState) {
    hookStates[hookIndex] =  hookStates[hookIndex] || { current: initialState };
    return hookStates[hookIndex++];
}

forwardRef + useImperativeHandle

useRef: 获取原生DOM,或组件
forwardRef: 转发父组件的ref,子组件须接受props和ref作为参数,子组件可将ref挂在到自身某个dom元素上
useImperativeHandle:在函数式组件中,用于定义暴露给父组件的ref方法。

function Child(props, ref) {
    const inputRef = React.useRef();
    React.useImperativeHandle(ref, () => (
        {
            focus() {
                inputRef.current.focus();
            }
        }
    ));
    return (
        <input type="text" ref={inputRef} />
    )
}
const ForwardChild = React.forwardRef(Child);
function Parent() {
    let [number, setNumber] = React.useState(0);
    const inputRef = React.useRef();
    function getFocus() {
        console.log(inputRef.current);
        inputRef.current.value = 'focus'; // 代表输入框DOM
        inputRef.current.focus();
    }
    return (
        <div>
            <ForwardChild ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
            <p>{number}</p>
            <button onClick={() => {
                debugger
                setNumber( number + 1)
            }}>+</button>
        </div>
    )
}

延迟场景产生的闭包问题

函数在useEffect内定义,且用到了组件内的变量,然后延迟被调用,

此种情况下,产生了闭包,变量在当前宏任务中的修改,不会再函数执行时体现。

回头补例子。


文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录