hooks api 详细demo

1,333 阅读7分钟

本文所有代码demo:https://stackblitz.com/edit/react-hooks-memo-gwv9c6?file=index.js

overview

How do React hooks really work? -- hooks closures

原文地址:https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/
setCount 返回一个函数 函数可以访问useState 内部的变量 => 闭包

const [count, setCount] = useState(0);
// simple useState useEffect  demo
const MyReact = (function() {
  let hooks = [],
    currentHook = 0 // array of hooks, and an iterator!
  return {
    render(Component) {
      const Comp = Component() // run effects
      Comp.render()
      currentHook = 0 // reset for next render
      return Comp
    },
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      const deps = hooks[currentHook] // type: array | undefined
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        callback()
        hooks[currentHook] = depArray
      }
      currentHook++ // done with this hook
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue // type: any
      const setStateHookIndex = currentHook ; // for setState closure
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()
// in usage
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  const [text, setText] = MyReact.useState('foo') // 2nd state hook!
  MyReact.useEffect(() => {
    console.log('effect', count, text)
  }, [count, text])
  return {
    click: () => setCount(count + 1),
    type: txt => setText(txt),
    noop: () => setCount(count),
    render: () => console.log('render', { count, text })
  }
}
let App
App = MyReact.render(Counter)
// effect 0 foo
// render {count: 0, text: 'foo'}
App.click()
App = MyReact.render(Counter)
// effect 1 foo
// render {count: 1, text: 'foo'}
App.type('bar')
App = MyReact.render(Counter)
// effect 1 bar
// render {count: 1, text: 'bar'}
App.noop()
App = MyReact.render(Counter)
// // no effect run
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}

useState

import React, { useState} from 'react';
import { render } from 'react-dom';
export default function App() {
  const [state, setState] = useState({});
  const onClick = () => setState({click:!state.click});
  return (
    <div>
      <p>You clicked count {count} times</p>
      <button onClick={onClick}>
        Click me count
      </button>
    </div>
  );
};
Tip

模拟class setState
You don’t have to use many state variables. State variables can hold objects and arrays just fine, so you can still group related data together. However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it。
我们可以在声明state的时候,使用数组或者对象。class里的setState是合并state,hooks里的setState是替换state。

useEffect

  • React 会记录下你传给 useEffect 的这个方法,然后在进行了 DOM 更新之后调用这个方法。
  • Hooks 使用了 JavaScript 的闭包(closures)。将 useEffect 放在一个组件内部,可以让我们在 effect 中,即可获得对 count state(或其它 props)的访问。
  • useEffect 在每次 render 之后都会调用。useEffect第二个参数是一个inputs数组。 他会在每次render之前比较依赖的inputs里的每一项。如果改变才会调用useEffect。这里类似 componentDidUpdate
不需要清理的 effects

使用场景:网络请求,手动更新 DOM,打印日志

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
需要清理的 effects
import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 明确在这个 effect 之后如何清理它
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
模拟生命周期 componentDidMount、componentDidUpdate 和 componentWillUnmount

import React, { useState,  useEffect } from 'react';
import { render } from 'react-dom';

export default function App() {
  
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => {
    console.log('similar componentDidmount count')
    document.title = `You clicked count ${count} times`;
  },[]);

  useEffect(() => {
  //  Optimizing Performance by Skipping Effects
    console.log('similar componentDidmount/componentDidUpdate count2')
    document.title = `You clicked count2 ${count2} times`;
  },[count2]);

  useEffect(() => {
    return ()=>{
      console.log('similar componentWillUnmount')
    }
  });

  return (
    <div>
      <p>You clicked count {count} times</p>
      <p>You clicked count2 {count2} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me count
      </button>
      <button onClick={() => setCount2(count2 + 1)}>
        Click me count2
      </button>
    </div>
  );
}
Tip

如果你熟悉 class 组件中的生命周期方法,你可以把 useEffect Hooks 视作 componentDidMount、componentDidUpdate 和 componentWillUnmount 的结合。
如果你只想运行一次effect 并清楚它, (componentDidMount and componentDidUpdate),你可以在第二个参数里传一个空数组。但是react官方建议不要将这成为习惯,因为这样会引起bug(可以参考 => 需要清理的 effects)。

useContext

//  子组件 调用
import React, { useState, useEffect,useContext } from 'react';
import { render } from 'react-dom';
import DataContext from './dataContext';
export default function App() {
  const data = useContext(DataContext);
  console.log(data) // {foo: "ProviderValue"};
  const [state, setState] = useState({});
  const onClick = () => setState({click:!state.click});
  return (
    <div>
      <button onClick={onClick}>
        Click me  
      </button>
      <DataContext.Consumer>
        {
          value => <div>{value.foo}</div> 
          //  value  {foo: "ProviderValue"}
        }
      </DataContext.Consumer>
    </div>
  );
};
//  父组件 提供
    <p>
        <DataContext.Provider value={ProviderValue}>
          <Context />
        </DataContext.Provider>
    </p>
//  提供数据
import React  from 'react';
const DataContext = React.createContext({});
export default DataContext;

Tip

useContext 不用再写嵌套的 Consumer 组件 回调函数 可以直接获取context的值
useContext 接收一个 由React.createContext创建的返回值 并返回当前 context 的value 。获取的是最近的 provider 提供的context。每当provider提供的context更新时,useContext会最后一次context value触发render

useCallback && useMemo

useCallback
const memoizedCallback = useCallback(
()=>{
    doSomething(a,b)
},
[a,b]);

返回一个 被记忆的(Memoized) 回调函数 接收一个回调函数和一个数组,useCallback 会返回一个 被记忆的回调函数,这个函数只有在 数组里的其中一个值变化的时候才变化。用于优化子组件,防止不必要的渲染的时候。类似于shouldComponentUpdate。

useCallback(fn, inputs) 等价于 useMemo(() => fn, inputs)
数组里的输入并不是被当作参数传给后面的回调函数。确切的说:每一个在回调函数里被引用的值也应该出现在后面的数组里。

useMemo
const memoizedValue = useMemo(()=> computedExpensiveValue(a,b),[a,b]);

返回一个 被记忆的(Memoized)值 useMemo 只有在输入的数组里的其中一个值改变的时候会重新计算被记忆的值。 这个优化的方法可以在每次渲染的时候避免昂贵的计算。

Tip

在React.Component里,如果props/state发生改变就会触发重新渲染(re-render),当父组件(parent components)重新渲染也会重新渲染 子组件(child components)。 在class Component里 ,shouldComponentUpdate 指定props改变更新components或使用 React.PureComponent。
请看下面的demo

const ChildComponent = React.memo(({ onClick, children }) => {
  console.log('clicked!')
  return <button onClick={onClick}>{children}</button>
})
                                                                   
const ParentComponent = () => {
  const [state, setState] = useState({ children: [1, 2, 3] })
  const { children } = state
  const onClick = () => setState({ children, foo: 'baz' })
  return (
    <div>
      {
        children.map(child => (
          <ChildComponent key={child} onClick={onClick} >
            { child }
          </ChildComponent>
        )) 
      }
    </div>
  )
}

ReactDOM.render(<ParentComponent />, document.getElementById('app'))

我们把button组件 用memo 记忆下来,在改变state的时候子组件不应该再次渲染。但实际上是,子组件被重新渲染里。 原因是:ParentComponent在重新渲染时,onClick被重新绑定一个新的尖头函数。在shallowEqual的strict equal情况下被判定是不相等造成memo失败。
改变一下 用useCallback就可以了

import React, { useState, useCallback, useMemo } from 'react';
import { render } from 'react-dom';


const ChildComponent = React.memo(({ onClick, children }) => {
  console.log('callback clicked!')
  return <button onClick={onClick}>{children}</button>
});
                                                                   
const ParentComponent = () => {
  const [state, setState] = useState({ children: [1, 2, 3] })
  const { children } = state
  const onClick = useCallback (
    ()=>setState({ children, foo: 'baz' }),
  [children]);
  return (
    <div> 
    <h1>callback demo</h1>
      {
        children.map(child => (
          <ChildComponent key={child} onClick={onClick} >
            { child }
          </ChildComponent>
        )) 
      }
    </div>
  )
}

render(<ParentComponent />, document.getElementById('callback'));

原因:在 class component中,我们可以作为props传递下去的callback是在class component的原型链上,是 member function。但是 function component是不存在instance,所以不能绑定callback。这就是useCallback/useMemo 的使用。在function component中 中产生一个不随父组件重新渲染而改动(mutate)的 callback。

//  useCallback包裹的cb和未被包裹的cb对比
import React, { useState, useCallback, useMemo } from 'react';
import { render } from 'react-dom';

const ParentComponent = () => {
  const [state, setState] = useState({ children: [1, 2, 3] });
  const { children } = state;
  //  点击changeState 会看到 memoizedCallback === unMemoizedonClick false
  const changeState = () => setState({ children, foo: 'baz' });
  const memoizedCallback = useCallback(changeState, []);
  const unMemoizedonClick = changeState;
  return (
    <div>
      <h1>callback demo</h1>
      <div>
        <div>
          <button onClick={changeState}> changeState </button>
        </div>
        unMemoizedonClick === memoizedCallback: {String(unMemoizedonClick === memoizedCallback)}
      </div>
    </div>
  )
}

render(<ParentComponent />, document.getElementById('memoizedCallback&&unMemoizedonClick'));
//  只有在dep变化时候 回调函数才会被调用
import React, {
  useEffect,
  useMemo,
  memo,
  useState,
  Fragment,
  useCallback
} from "react";
import ReactDOM from "react-dom";

import "antd/dist/antd.css";
import "./index.css";

const App = () => {
  const [state, setState] = useState({
    recieptVisible: 1,
    invoinceVisible: 1,
    noticeVisible: false,
    selectedRows: []
  });
  const { recieptVisible, invoinceVisible } = state;
  const cbfunc = () => {
    return setState({
      ...state,
      recieptVisible: recieptVisible + 1
    });
  };



  //  计算属性 get useMemo
  //  例如 算数的分子变化时候 结果变化 要进行大量计算的时候使用
  //  有点像 vue的 计算属性
  const memofunc = () => {
    return recieptVisible + invoinceVisible;
  };

  //  watch useCallback
  //  例如 modal的visible变化的时候  调用cb
  const Cb = useCallback(cbfunc, [recieptVisible]);
  const memoCb = useMemo(memofunc, [recieptVisible]);

  return (
    <div>
      <div>{recieptVisible}</div>

      <div>{memoCb}</div>
      <button
        type="primary"
        onClick={() =>
          setState({ ...state, recieptVisible: recieptVisible + 1 })
        }
      >
        Open recieptVisible
      </button>

      <button type="primary" onClick={Cb}>
        Cb
      </button>

      <div>{invoinceVisible}</div>
      <button
        type="primary"
        onClick={() =>
          setState({ ...state, invoinceVisible: invoinceVisible + 1 })
        }
      >
        Open invoinceVisible
      </button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("container"));

useRef

const refContainer = useRef(initialValue);

如果有initialValue 返回 初始化时候 {current:initialValue}
组件render之后 返回 所指 当前 组件实例 {current:el}


import React, { useRef } from 'react';
import { render } from 'react-dom';

export default function App() { 
  const inputEl = useRef(null);
 const onButtonClick = () => {
    console.log(inputEl,'inputEl') // {current:el}
  };
  return (
    <>
      <input ref={inputEl} type='text' />
      <button onClick={onButtonClick}>button</button> 
    </>
  );
}
Tip

原来 ref = {(node) => this.el = node} 把node放在回调函数里取出来更直接

useImperativeHandle

useLayoutEffect

如果要进行dom操作,配合ref就应该使用useLayoutEffect ,他会阻塞页面渲染。

tip

在一些不常见的情况下你也许需要他们同步调用(比如计算元素尺寸),我们提供了一个单独的 useLayoutEffect 来达成这样的效果。它的 API 和 useEffect 是相同的

useDebugValue

参考