美文网首页react-native
react hooks 学习记录

react hooks 学习记录

作者: 北故城 | 来源:发表于2020-05-15 16:12 被阅读0次

    Hooks Api 索引

    基础 Api

    useState useEffect useContext
    返回一个 state,以及更新 state 的函数。 相当于componentDidMount、componentDidUpdate,componentWillUnmount 这三个函数的组合。 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

    额外Api

    useReducer useCallback useMemo useRef useImperativeHandle useLayoutEffect useDebugValue

    useState 官方文档例子

    import React, { useState } from 'react';
    
    function Example() {
      // 声明一个叫 "count" 的 state 变量
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    等价的class组件

    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }
    

    声明多个state

    function ExampleWithManyStates() {
      // 声明多个 state 变量
      const [age, setAge] = useState(42);
      const [fruit, setFruit] = useState('banana');
      const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
    

    提示:方括号有什么用?

    我们用方括号定义了一个 state 变量
    等号左边名字并不是 React API 的部分,你可以自己取名字
    这种 JavaScript 语法叫数组解构。它意味着我们同时创建了 fruitsetFruit 两个变量,fruit 的值为 useState 返回的第一个值,setFruit 是返回的第二个值

    const [fruit, setFruit] = useState('banana');
    var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
    var fruit = fruitStateVariable[0]; // 数组里的第一个值
    var setFruit = fruitStateVariable[1]; // 数组里的第二个值
    

    useState除了可以静态的赋值还可以useState(()=>init)
    useState返回的更新数据方法setXXX(currentState=>currentState+1)默认会传入当前的数据状态

    useEffect

    Effect Hook 可以让你在函数组件中执行副作用操作
    解释这个 Hook 之前先理解下什么是副作用。网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的。正常情况下,在Function Component的函数体中,是不建议写副作用代码的,否则容易出 bug。

    下面的Class Component例子中,副作用代码写在了componentDidMount和componentDidUpdate中:

    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
      }
    
      componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }
    

    可以看到componentDidMount和componentDidUpdate中的代码是一样的。而使用 Effect Hook 来改写就不会有这个问题:

    import React, { 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>
      );
    }
    //useEffect 没有传递依赖参数,所以每次数据更新都会调用useEffect
    

    useEffect会在每次 DOM 渲染后执行,不会阻塞页面渲染。它同时具备componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期函数的执行时机

    此外还有一些副作用需要组件卸载的时候做一些额外的清理工作的,例如订阅某个功能:

    class FriendStatus extends React.Component {
      constructor(props) {
        super(props);
        this.state = { isOnline: null };
        this.handleStatusChange = this.handleStatusChange.bind(this);
      }
    
      componentDidMount() {
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      handleStatusChange(status) {
        this.setState({
          isOnline: status.isOnline
        });
      }
    
      render() {
        if (this.state.isOnline === null) {
          return 'Loading...';
        }
        return this.state.isOnline ? 'Online' : 'Offline';
      }
    }
    

    componentDidMount订阅后,需要在componentWillUnmount取消订阅。使用 Effect Hook 来改写会是这个样子:

    import React, { useState, useEffect } from 'react';
    
    function FriendStatus(props) {
      const [isOnline, setIsOnline] = useState(null);
    
      useEffect(() => {
        function handleStatusChange(status) {
          setIsOnline(status.isOnline);
        }
    
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        
        // 返回一个函数来进行额外的清理工作:
        return function cleanup() {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });
    
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }
    

    useEffect的返回值是一个函数的时候,React 会在下一次执行这个副作用之前执行一遍清理工作,整个组件的生命周期流程可以这么理解:

    组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载

    上文提到useEffect会在每次渲染后执行,但有的情况下我们希望只有在 stateprops 改变的情况下才执行。如果是Class Component,我们会这么做:

    componentDidUpdate(prevProps, prevState) {
      if (prevState.count !== this.state.count) {
        document.title = `You clicked ${this.state.count} times`;
      }
    }
    

    使用 Hook 的时候,我们只需要传入第二个参数:

    useEffect(() => {
      document.title = `You clicked ${count} times`;
    }, [count]); // 只有在 count 改变的时候才执行 Effect
    

    第二个参数是一个数组,可以传多个值,一般会将 Effect 用到的所有 props 和 state 都传进去。

    当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数可以传一个空数组[],实现的效果有点类似componentDidMountcomponentWillUnmount的组合。

    useCallback useMemo

    在介绍一下这两个hooks的作用之前,我们先来回顾一下react中的性能优化。在hooks诞生之前,如果组件包含内部state,我们都是基于class的形式来创建组件。当时我们也知道,react中,性能的优化点在于:

    1.调用setState,就会触发组件的重新渲染,无论前后的state是否不同
    2.父组件更新,子组件也会自动的更新

    基于上面的两点,我们通常的解决方案是:使用immutable进行比较,在不相等的时候调用setState;在shouldComponentUpdate中判断前后的props和state,如果没有变化,则返回false来阻止更新。

    在hooks出来之后,我们能够使用function的形式来创建包含内部state的组件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此useMemo 和useCallback就是解决性能问题的杀手锏

    useCallbackuseMemo的参数跟useEffect一致,他们之间最大的区别有是useEffect会用于处理副作用,而前两个hooks不能。

    useMemouseCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量useCallback返回缓存的函数

    useMemo

    我们来看一个反例:

    import React from 'react';
     
     
    export default function WithoutMemo() {
        const [count, setCount] = useState(1);
        const [val, setValue] = useState('');
     
        function expensive() {
            console.log('compute');
            let sum = 0;
            for (let i = 0; i < count * 100; i++) {
                sum += i;
            }
            return sum;
        }
     
        return <div>
            <h4>{count}-{val}-{expensive()}</h4>
            <div>
                <button onClick={() => setCount(count + 1)}>+c1</button>
                <input value={val} onChange={event => setValue(event.target.value)}/>
            </div>
        </div>;
    }
    

    这里创建了两个state,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的昂贵计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo,只在count的值修改时,执行expensive计算:

    export default function WithMemo() {
        const [count, setCount] = useState(1);
        const [val, setValue] = useState('');
        const expensive = useMemo(() => {
            console.log('compute');
            let sum = 0;
            for (let i = 0; i < count * 100; i++) {
                sum += i;
            }
            return sum;
        }, [count]);
     
        return <div>
            <h4>{count}-{expensive}</h4>
            {val}
            <div>
                <button onClick={() => setCount(count + 1)}>+c1</button>
                <input value={val} onChange={event => setValue(event.target.value)}/>
            </div>
        </div>;
    }
    

    上面我们可以看到,使用useMemo来执行昂贵的计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。

    useCallback

    讲完了useMemo,接下来是useCallback。useCallback跟useMemo比较类似,但它返回的是缓存的函数。我们看一下最简单的用法

    const fnA = useCallback(fnB, [a])

    上面的useCallback会将我们传递给它的函数fnB返回,并且将这个结果缓存;当依赖a变更时,会返回新的函数。既然返回的是函数,我们无法很好的判断返回的函数是否变更,所以我们可以借助ES6新增的数据类型Set来判断,具体如下:

    import React, { useState, useCallback } from 'react';
     
    const set = new Set();
     
    export default function Callback() {
        const [count, setCount] = useState(1);
        const [val, setVal] = useState('');
     
        const callback = useCallback(() => {
            console.log(count);
        }, [count]);
        set.add(callback);
     
     
        return <div>
            <h4>{count}</h4>
            <h4>{set.size}</h4>
            <div>
                <button onClick={() => setCount(count + 1)}>+</button>
                <input value={val} onChange={event => setVal(event.target.value)}/>
            </div>
        </div>;
    }
    

    我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。

    知道useCallback有什么样的特点,那有什么作用呢?

    使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

    import React, { useState, useCallback, useEffect } from 'react';
    function Parent() {
        const [count, setCount] = useState(1);
        const [val, setVal] = useState('');
     
        const callback = useCallback(() => {
            return count;
        }, [count]);
        return <div>
            <h4>{count}</h4>
            <Child callback={callback}/>
            <div>
                <button onClick={() => setCount(count + 1)}>+</button>
                <input value={val} onChange={event => setVal(event.target.value)}/>
            </div>
        </div>;
    }
     
    function Child({ callback }) {
        const [count, setCount] = useState(() => callback());
        useEffect(() => {
            setCount(callback());
        }, [callback]);
        return <div>
            {count}
        </div>
    }
    

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

    有空继续整理

    相关文章

      网友评论

        本文标题:react hooks 学习记录

        本文链接:https://www.haomeiwen.com/subject/wuyznhtx.html