mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

react16.8 HOOKS #8

Open shenxuxiang opened 5 years ago

shenxuxiang commented 5 years ago

HOOKS

Hook是React 16.8中的新增功能。它们允许您在不编写类的情况下使用状态和其他React功能。HOOKS只能在函数组件中使用

memo

React.memo 是一个高阶的组件。它类似于React.PureComponent 也就是说如果组件的 props 没有改变,是不会被重新渲染的。


  function Foo (props) {
    ...
  }
  export default React.memo(Foo)

useState

类似于类组件中的state,不同的是 useState 接受一个任意类型的值 string, array, object, bool... 作为参数并返回一个数组,且 useState 只会在组件初始化的时候执行

  // 初始化的时候,age的值就是useState中参数的值
  const [ age, setAge ] = useState(20);
  const [ visible, setVisible ] = useState(props.visible);

数组中的第一个元素是状态值,组件在运行过程中会保留这个状态值,类似于this.state 数组中的第二个元素是改变这个状体值的函数,类似于this.setState()


  function Hooks(props) {
    const [ age, setAge ] = useState(20);
    const [ visible, setVisible ] = useState(props.visible);

    return (
      <div className="">
        <p>我的年龄是{age}岁</p>
        <button onClick={() => setAge(age + 1)}>点击</button>
        <p>{`${visible}`}</p>
      </div>
    );
  };

useEffect

这个类似类组件中的 componentDidMountcomponentDidUpdate。每次当函数组件挂载成功或者重新渲染完成后都会调用 useEffect 。 之所以说类似,是因为 useEffect 不完全同类组件中的 componentDidMountcomponentDidUpdate生命周期函数一样,useEffect 有延迟,在父组件didMount或didUpdate后,但在任何新渲染之前触发。useEffect可以在组件中使用多次。 useEffect 还可以返回一个函数,并在组件即将销毁时调用这个返回函数,没错,就是和类组件的 componentWillUnmount 一样。

useEffect延迟执行

  function Hooks(props) {
    const [ age, setAge ] = useState(20);
    // 当组件挂载成功后调用下面的函数
    // 当props.visible 改变了,那么会在组件重新渲染完成以后调用下面的函数
    // 当调用setAge,age发生改变,那么会在组件重新渲染完成以后调用下面的函数
    useEffect(() => {
      console.log(props);
      // 会在组件willUnmount时候调用
      return () => {...}
    });

    return (
      <div className="">
        <p>我的年龄是{age}岁</p>
        <button onClick={() => setAge(age + 1)}>点击</button>
        <p>{`${visible}`}</p>
      </div>
    );
  };

useEffect也可以接收一个数组作为第二个参数

类似于类组件中的 componentDidUpdate(prevProps, prevState) ,这个生命周期,那么如何使用呢?

  function Hooks(props) {
    const [ age, setAge ] = useState(20);
    const [ visible, setVisible ] = useState(props.visible);
    // 当函数调用时发现props.visible发生了变化,类似于类组件中的componentDidUpdate(prevProps, prevState)
    // 当prevProps.visible !== this.props.visible 那么就会执行useEffect的函数体
    useEffect(() => {
      console.log('visible is changed');
      setVisible(props.visible);
    }, [ props.visible ]);

    // 当函数调用时发现age发生了变化,类似于类组件中的componentDidUpdate(prevProps, prevState)
    // 当prevState.age !== this.state.age 那么就会执行useEffect的函数体
    useEffect(() => {
      console.log(age, 1111);
    }, [ age ]);

    return (
      <div className="">
        <p>我的年龄是{age}岁</p>
        <button onClick={() => setAge(age + 1)}>点击</button>
        <p>{`${visible}`}</p>
      </div>
    );
  };

如果参数中有多个元素 [ age, props.visible ] ,那么元素的关系是 age && props.visible ,通过比较后只要有一个元素发生变化,useEffect 就会执行。如果参数是一个空数组 [] ,那么这个时候 useEffect 就和类组件中的 componentDidMount 一样,只在组件刚刚挂载的时候调用一次。 useEffect 函数中return的函数,不受第二个参数的影响,仍在组件 WillUnmount 的时候调用。 不要在循环条件或嵌套函数中调用Hook。相反,始终在React函数的顶层使用Hooks。通过遵循此规则,您可以确保每次组件呈现时都以相同的顺序调用Hook。这就是React允许多个useState和useEffect调用之间正确保留Hook状态的原因。

useLayoutEffect

useEffect 使用原理相同,但是唯一的区别在于 useLayoutEffect 不会延迟触发,和类组件的 componentDidMountcomponentDidUpdate 这两个生命周期函数是同步的,其他没有区别。

customize hooks

自定义Hook是一个JavaScript函数,其名称以“use” 开头,可以调用其他Hook。构建自己的Hook可以将组件逻辑提取到可重用的函数中 ,确保只在自定义Hook的顶层无条件地调用其他Hook。与React组件不同,自定义Hook不需要具有特定签名。我们可以决定它作为参数需要什么,以及它应该返回什么(如果有的话)

  // useVisibleStatus是一个自定义的钩子,我们在函数中调用的useEffect
  function useVisibleStatus(isShow) {
    const [ visible, setVisible ] = useState(isShow);
    useEffect(() => {
      setVisible(isShow);
    }, [ isShow ]);
    return visible;
  };

  function Hooks(props) {
    const [ count ] = useState(props.count);
    const visible = useVisibleStatus(props.visible);

    return (
      <div className="">
        <h2>{count}</h2>
        <button onClick={() => setCount(count + 1)}>点击</button>
        <h2>{`${visible}  ${props.count}`}</h2>
      </div>
    );
  };

我们也可以将一些复杂或者重复的逻辑提取提取到自定义的hook函数中,从而简化我们的代码。其实自定义hook和函数组件没有多大区别。

useReducer

useState 复杂的状态逻辑涉及多个子值或下一个状态取决于前一个状态时,通常useReducer更可取。useReducer还可以让您优化触发深度更新的组件的性能

  const initialState = {count: 0};

  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return {count: state.count + 1};
      case 'decrement':
        return {count: state.count - 1};
      default:
        throw new Error();
    }
  }

  function Counter({initialState}) {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
      <>
        Count: {state.count}
        <button onClick={() => dispatch({type: 'increment'})}>+</button>
        <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      </>
    );
  }

我们看看 useReducer 具体的实现(和自定义hook没有差异):

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

useImperativeHandle

可以通过 useImperativeHandle ,给ref上绑定一些自定的事件,前提是我们必须使用 forwardRef ,注意所有的事件都是绑定在ref的 current 属性上。 看下面的例子

// hook.js
function Hooks(props, ref) {
  const [ count, setCount ] = useState(props.count);
  useImperativeHandle(ref, () => ({
    // 自定义一些事件
    click: () => {
      setCount(count + 1);
    },
  }));

  return (
    <div className="">
      <h2>{count}</h2>
      <button  onClick={() => setCount(count + 1)}>点击</button>
    </div>
  );
};
export default React.forwardRef(Hooks);

// Application.js
export default class App extends PureComponent {
  componentDidMount() {
    this.ref = React.createRef();
  }

  return (
    <div
      onClick={() => this.ref.current.click()}
    >
      // ...
      <Hooks ref={this.ref} count={this.state.count} visible={this.state.visible}/>
      // ...
    </div>
  );
}

或者

  function FancyInput(props, ref) {
    // 获取真是DOM节点
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
      // 自定义一些事件
      focus: () => {
        // 在DOM节点执行一些操作都可以
        inputRef.current.focus();
      }
    }));
    return <input ref={inputRef} />;
  }
  FancyInput = forwardRef(FancyInput);

useRef

useRef 返回一个可变的ref对象,其 .current 属性值为初始化传递的参数(initialValue)。返回的对象将持续整个组件的生命周期。和class组件中的实例属性很像

  const ref = usRef(20);
  console.log(ref.current) // 20
  // 可以重新赋值
  ref.current = 200;

当然最常见的就是访问一个元素节点

  function TextInputWithFocusButton() {
    const inputEl = useRef(null);
    const onButtonClick = () => {
      // `current` points to the mounted text input element
      inputEl.current.focus();
    };
    return (
      <>
        <input ref={inputEl} type="text" />
        <button onClick={onButtonClick}>Focus the input</button>
      </>
    );
  }

useMemo

使用的场景:函数组件中,我们定义了一些方法,但是我们并不希望每次组件更新的时候都重新执行一次个函数,而是变成有条件的触发,那么这个时候我们就可以使用useMemo。有个地方需要注意点那就是,useMemo是在useLayoutEffect之前执行,这和类组件中的 componentWillMountcomponentWillUpdate 类似。可以查看的我们demo

  // 组件初始化的时候会调用Func,类似 componentWillMount
  // 当数组中的元素的值发生改变,那么就会调用Func,这个条件 `a && b` 的关系
  useMemo(() => Func(a, b), [a, b]);

在看这个带返回值的

  function Hooks(props) {
    const [ count, setCount ] = useState(props.count);
    useLayoutEffect(() => {
      console.log('useLayoutEffect 后执行');
      setCount(props.count);
    }, [ props.count ]);

    const dom = useMemo(() => {
      console.log('useMemo 优先执行');
      return <h2>{count * 10}</h2>;
    }, [count]);

    return (
      <div className="">
        <h2>{count}</h2>
        <button onClick={() => setCount(count + 1)}>点击</button>
        {dom}
      </div>
    );
  };

useCallback

useCallback 的使用和 useMemo 是一样的,且 useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

这是我的demo