前端开发那些事儿

React Hooks 温故知新

2020-11-10  本文已影响0人  梦想成真213

概念

解决的问题

遵循的规则

Hooks API

下面开始使用一下经常用到的 Hooks。新建一个项目,用来写例子。

npx create-react-app react-hooks
cd react-hooks
yarn start

删掉src下多余的文件,只保留 index.js。

useState

1:对比类的写法和函数组件

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

class Counter1 extends React.Component {
  constructor() {
    super();
    this.state = {
      number: 0
    }
  }
  add = () => {
    const { number } = this.state
    this.setState({
      number: number + 1
    })
  }
  render() {
    const { number } = this.state
    return (
      <div className="counter">
        <p>counter: {number}</p>
        <button onClick={this.add}>add +</button>
      </div>
    )
  }
}

function Counter() {
  const [number, setNum] = useState(0)
  return (
    <div className="counter">
      <p>counter: {number}</p>
      <button onClick={() => setNum(number + 1)}>add +</button>
    </div>
  )
}

ReactDOM.render(
  // <Counter />,
  <Counter1 />,
  document.getElementById('root')
);

2.每次的渲染都是独立的闭包

function Counter2(){
  const [number,setNumber] = useState(0);
  function alertNumber(){
    setTimeout(()=>{
      alert(number);
    },3000);
  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}

这里 alert 出来的是点击时候的 number 值,如果一直点,并不是最新的值。

3.函数式更新

// 函数式更新
function Counter2(){
  const [number, setNum] = useState(0);
  const lazyAdd = () => {
    setTimeout(() => {
      setNum(number + 1)
    }, 3000) 
  }
  const lazyFunction = () => {
    setTimeout(() => {
      setNum(number => number + 1)
    }, 3000);
  }
  return (
    <div className="counter">
      <p>counter: {number}</p>
      <button onClick={() => setNum(number + 1)}>add +</button>
      <button onClick={lazyAdd}>lazy add</button>
      <button onClick={lazyFunction}>lazy function</button>
    </div>
  )
}

setState 更新状态的函数参数可以是一个函数,返回新状态:
setNum(number => number + 1)每次都返回最新的状态,然后再加1。

4.惰性初始 state

function Counter3(){
  const [userInfo, setUserInfo] = useState(() => {
    return {
      name: 'mxcz',
      age: 18
    }
  });
  return (
    <div className="counter">
      <p>{userInfo.name}: {userInfo.age}</p>
      <button onClick={() => setUserInfo({age: userInfo.age + 1})}>add +</button>
      <button onClick={() => setUserInfo({...userInfo, age: userInfo.age + 1})}>更新要写完整</button>
    </div>
  )
}

5.性能优化

5.1 Object.is()

function Counter4(){
  const [counter,setCounter] = useState({name:'计数器',number:0});
  console.log('render Counter')
  return (
      <>
          <p>{counter.name}:{counter.number}</p>
          <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
          <button onClick={()=>setCounter(counter)}>-</button>
      </>
  )
}

增加数值之后,在减,不会引起组件的重新渲染,因为Object.is(Object.is() 方法判断两个值是否为同一个值。) 比较算法表示state没有改变。

5.2 减少渲染次数

function Child({onButtonClick,data}){
  console.log('Child render');
  return (
    <button onClick={onButtonClick} >{data.number}</button>
  )
}
function App(){
  const [number,setNumber] = useState(0);
  const [name,setName] = useState('mxcz');
  const addClick = () => setNumber(number+1)
  const data = { number }
  return (
    <div>
      <input type="text" value={name} onChange={e=>setName(e.target.value)}/>
      <Child onButtonClick={addClick} data={data}/>
    </div>
  )
}

可以看到不优化的情况下,点击按钮和改变输入框的值都会引起子组件的重新渲染,但是子组件依赖的数据只有数字改变而已;


现在来改造一下,给子组件加上Child = memo(Child);返回一个记忆组件,此时再点击按钮和改变输入框,依然会重新渲染子组件,这里的原因是子组件调用了父组件传递来的会调函数,这个函数在父组件渲染时,都会重新建立新的函数引用,下面来验证一下:
let oldClick;
function App(){
  const [number,setNumber] = useState(0);
  const [name,setName] = useState('mxcz');
  const addClick = () => setNumber(number+1)
  console.log('oldClick === addClick', oldClick === addClick)
  oldClick = addClick
  const data = { number }
  return (
    <div>
      <input type="text" value={name} onChange={e=>setName(e.target.value)}/>
      <Child onButtonClick={addClick} data={data}/>
    </div>
  )
}

每次渲染都重新返回了false,表示每次都是新的函数,现在改造一下,const addClick = useCallback(()=>setNumber(number+1),[number]);

可以看到,给回调函数加上useCallback,点击按钮每次都是false,说明都是依赖的number改变了,函数是新的函数,而改变输入框的值,返回true,说明函数被缓存起来了,并没有重新创建函数。而此时chid组件依然被渲染了,因为data 改变了,现在将data用useMemo包起来const data = useMemo(()=>({number}),[number]);,再来运行一遍:

此时的子组件在输入框改变时并没有被重新渲染。现在子组件不用memo包装,可以看到子组件还是在输入框改变值的时候被重新渲染了。

例子的完整代码如下:
function Child({onButtonClick,data}){
  console.log('Child render');
  return (
    <button onClick={onButtonClick} >{data.number}</button>
  )
}
Child = memo(Child);
let oldClick;
function App(){
  const [number,setNumber] = useState(0);
  const [name,setName] = useState('mxcz');
  const addClick = useCallback(()=>setNumber(number+1),[number]);
  const data = useMemo(()=>({number}),[number]);
  // const addClick = () => setNumber(number+1)
  console.log('oldClick === addClick', oldClick === addClick)
  oldClick = addClick
  // const data = { number }
  return (
    <div>
      <input type="text" value={name} onChange={e=>setName(e.target.value)}/>
      <Child onButtonClick={addClick} data={data}/>
    </div>
  )
}

6.注意事项

function App2() {
  const [number, setNumber] = useState(0);
  const [visible, setVisible] = useState(false);
  if (number % 2 == 0) {
      useEffect(() => {
          setVisible(true);
      }, [number]);
  } else {
      useEffect(() => {
          setVisible(false);
      }, [number]);
  }
  return (
      <div>
          <p>{number}</p>
          <div>{visible && <div>visible</div>}</div>
          <button onClick={() => setNumber(number + 1)}>+</button>
      </div>
  )
}

可以看到报错了:
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
报错说:useEffect 在条件语句中被调用,在每次的组件渲染中,必须要以完全相同的顺序调用 React Hooks。条件不同,每次渲染的顺序不同,这就会乱了,应该是跟链表的结构相关吧,总之要遵循 React Hooks的使用原则。

useReducer

const initialState = 0;

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
  return {number: initialState};
}
function App3(){
  const [state, dispatch] = useReducer(reducer, initialState, init);
  return (
    <>
      Count: {state.number}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  )
}

useContext

const CounterContext = React.createContext();
function reducer2(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function Counter5(){
  let {state,dispatch} = useContext(CounterContext);
  return (
    <>
      <p>{state.number}</p>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  )
}
function App4(){
  const [state, dispatch] = useReducer(reducer2, {number:0});
  return (
      <CounterContext.Provider value={{state,dispatch}}>
          <Counter5 />
      </CounterContext.Provider>
  )
}

effect

1.修改document的标题,class的实现方式

class Title extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0
    };
  }
  componentDidMount() {
      document.title = `点击了${this.state.number}次`;
  }
  componentDidUpdate() {
      document.title = `点击了${this.state.number}次`;
  }
  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={() => this.setState({ number: this.state.number + 1 })}>
          +
        </button>
      </div>
    );
  }
}

在这个 class 中,需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。useEffect会在第一次渲染之后和每次更新之后都会执行。
下面是函数组件,使用useEffect的方式:

function Title2(){
  const [number,setNumber] = useState(0);
  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    document.title = `你点击了${number}次`;
  });
  return (
    <>
      <p>{number}</p>
      <button onClick={()=>setNumber(number+1)}>+</button>
    </>
  )
}

每次组件重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。

2.跳过 Effect 进行性能优化

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

3.清除副作用

function Counter7() {
  const [number, setNumber] = useState(0);
  useEffect(() => {
      console.log('开启一个新的定时器')
      const $timer = setInterval(() => {
          setNumber(number => number + 1);
      }, 1000);
      return () => {
          console.log('销毁老的定时器');
          clearInterval($timer);
      }
  });
  return (
      <p>{number}</p>
  )
}
function App5() {
  let [visible, setVisible] = useState(true);
  return (
      <div>
          {visible && <Counter7 />}
          <button onClick={() => setVisible(false)}>stop</button>
      </div>
  )
}

useRef

function Parent() {
  let [number, setNumber] = useState(0);
  return (
      <>
          <Child2 />
          <button onClick={() => setNumber({ number: number + 1 })}>+</button>
      </>
  )
}
let input;
function Child2() {
  const inputRef = useRef();
  console.log('input===inputRef', input === inputRef);
  input = inputRef;
  function getFocus() {
      inputRef.current.focus();
  }
  return (
      <>
          <input type="text" ref={inputRef} />
          <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

forwardRef

function Child3(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child3 = forwardRef(Child3);
function Parent2(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child3 ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

useImperativeHandle

function Child4(props,ref){
  const inputRef = useRef();
  useImperativeHandle(ref,()=>(
    {
      focus(){
        inputRef.current.focus();
      }
    }
  ));
  return (
    <input type="text" ref={inputRef}/>
  )
}
Child4 = forwardRef(Child4);
function Parent3(){
  let [number,setNumber] = useState(0);
  const inputRef = useRef();
  function getFocus(){
    console.log(inputRef.current);
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
    <>
      <Child4 ref={inputRef}/>
      <button onClick={()=>setNumber({number:number+1})}>+</button>
      <button onClick={getFocus}>获得焦点</button>
    </>
  )
}

这样父组件中只可以操作子组件暴露给父组件的方法。

useLayoutEffect

function LayoutEffect() {
  const [color, setColor] = useState('red');
  useLayoutEffect(() => {
      console.log(color);
  });
  useEffect(() => {
      console.log('color', color);
  });
  return (
      <>
          <div id="myDiv" style={{ background: color }}>颜色</div>
          <button onClick={() => setColor('red')}>红</button>
          <button onClick={() => setColor('yellow')}>黄</button>
          <button onClick={() => setColor('blue')}>蓝</button>
      </>
  );
}

自定义 Hook

1.自定义一个计数器

function useNumber(initNumber){
  const [number, setNumber] = useState(initNumber || 0)
  useEffect(() => {
    const $timer = setInterval(() => {
      setNumber(number => number + 1)
    }, 1000)
    return () => {
      clearInterval($timer)
    }
  }, [number])
  return number
}

function App6(){
  const number = useNumber(4)
  return(
    <p>{ number }</p>
  )
}

2. 日志中间件

const initState = 0;

function countReducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { number: state.number + 1 };
        case 'decrement':
            return { number: state.number - 1 };
        default:
            throw new Error();
    }
}
function initFun(initState) {
    return { number: initState };
}
function useLogger(reducer, initialState, init) {
    const [state, dispatch] = useReducer(reducer, initialState, init);
    let dispatchWithLogger = (action) => {
        console.log('老状态', state);
        dispatch(action);
    }
    useEffect(function () {
        console.log('新状态', state);
    }, [state]);
    return [state, dispatchWithLogger];
}
function Counter8() {
    const [state, dispatch] = useLogger(countReducer, initState, initFun);
    return (
        <>
            Count: {state.number}
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </>
    )
}

路由hook

import { BrowserRouter as Router, Route, Switch, useParams, useLocation, useHistory } from "react-router-dom";
function Post() {
   let { title } = useParams();
   const location = useLocation();
   let history = useHistory();
   return <div>
              {title}<hr />{JSON.stringify(location)}
             <button type="button" onClick={() => history.goBack()}>回去</button>
          </div>;
}
ReactDOM.render(
<Router>
    <div>
      <Switch>
        <Route path="/post/:title" component={Post} />
      </Switch>
    </div>
  </Router>,
document.getElementById("root")
);
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, useRouteMatch } from 'react-router-dom';
function NotFound() {
  return <div>Not Found</div>
}
function Post(props) {
  return (
    <div>{props.match.params.title}</div>
  )
}
function App() {
  let match = useRouteMatch({
    path: '/post/:title',
    strict: true,
    sensitive: true
  })
  console.log(match);
  return (
    <div>
      {match ? <Post match={match} /> : <NotFound />}
    </div>
  )
}

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

官方文档:https://react.docschina.org/docs/hooks-intro.html

上一篇 下一篇

猜你喜欢

热点阅读