记录React-Hooks

1,072 阅读9分钟

黑白块原文链接 segmentfault.com/a/119000001…

Hooks应该是React中比较火的了

最近看了一段时间 React hooks 因为一直没有实际使用过 只是出了些小demo

这篇文章分两段:前半段是对不了解hooks的萌新准备的一个demo,后半段是稍微深入一点点的介绍和个人理解(一点点)

前半段

后半段

我以前是用vue的 先来一段vue-hook和react-hook(原文:www.cnblogs.com/rock-roll/p…

尤大对于Vue的hook和React的hook的对比中,总结的Vue Hook优势如下:

1. 整体上更符合JavaScript的直觉;(没体会过)

2. 不受调用顺序的限制,可以有条件地被调用;(React Hook受调用顺序限制,绝对不能错位)

3. 不会在后续更新时不断产生大量的内联函数而影响引擎优化或是导致GC压力;(见以下图示:

这笔者用两个项目中业务相同的界面做测试,区别是两个项目一个之前老的用Class组件做的,另一个是新的用函数组件配合Hook做的。

React Function Component & Hook Major GC:

React Class Component Major GC:

可以看出React Function Component & Hook Major GC的时候压力很大,相比之下GC过程明显长了很多,主线程被占用时间长。因为在render
或者re-render函数组件时,如果业务复杂会导致函数组件内部的内联函数太多,每次渲染的时候都会调用这个函数组件,每次低啊用都需要开
辟新的内存空间来存储该函数组件内部的除了ref以外的几乎所有东西。笔者这里有个组件,每次渲染会调用三个接口,都会导致重渲染,可以
看到Heap增加很大,这将导致下一次Major GC的时候过程很长:

而Class组件,每次重渲染,也是在最初基于这个Class实例化出来的对象上面进行处理,不会生成新的对象。

这个问题在PC端应用上可能影响小一些,在移动端具有复杂业务界面上或者对毫秒级优化有要求的应用上的影响会更明显一些。

)

4. 不需要总是使用 useCallback 来缓存传给子组件的回调以防止过度更新;(React Hook中,如果运行你的应用的客户端性能差,比如移动端,为了性能你确实需要这么做,但这会导致有很多公式化的代码出现)

5. 不需要担心传了错误的依赖数组给useEffect/useMemo/useCallback从而导致回调中使用了过期的值 —— Vue 的依赖追踪是全自动的。(React Hook必须保证正确,否则会出错,ESlint也会强制检查每个Hook的依赖是否正确)

之后继续:React-hooks

官方文档:React 16.8中的新增功能(hooks)。它们使您无需编写类即可使用状态和其他React功能。

从尤大的总结的几点来看,每一点都打到了使用React Hook的痛处上,特别当你有在React Hook上有实践体会时,你会很有感慨
Vue Hook还是值得期待的,反正我比较期待 哈哈
Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。

如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。

State hook

state hook的主要作用就是获取需要的 state 和 更新state的方法

使用方法

const [state, setState] = useState(initialState);

参数: initialState 可以直接是当前state的初始值,也可以是一个函数,函数的返回值将作为state的值,参数只会在组件的初始渲染中起作用

返回值:返回的是一个数组,一个是当前state的值,另一个是更新state的方法,这里面setState方法与class中的setState不同在于,此setState 不会合并state 中的值

如果需要定义多个state 只需要多次调用useState 方法就行。

Effect hook

useEffect方法是在每次渲染之后执行,可以理解为class写法中的 componentDidMount / componentDidUpdate(为了方便理解可以这么理解,但不完全一样)

useEffect(didUpdate); 参数:function,在每次渲染之后执行,在函数里可以编写更新dom ,添加订阅 等。

参数返回值: function(可以不返回) 如果 didUpdate函数中返回了一个函数,这个函数会在组件卸载前执行(每次渲染都会执行)需要清除上次订阅的内容可以再这里面写。

执行条件: useEffect 的第二个参数是一个数组,只有当数组中的的值发生改变的时候才会调用effect,如果执行在第一次挂载和卸载的时候调用,只需要传一个[]空数组

useContext

const value = useContext(MyContext);

获取context 的值,类似于clss 写法中的static contextType = MyContext ,当使用了useContext会在context 的值发生改变的时候重新render。

参数 接收对象是React.createContext 的返回值 返回值 context 里的内容

如果对context 不了解可以看 context 学习

自定义 hook

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook

第一段
这个demo主要有 组件化 子组件调用父组件 常用的事件

Main.js

import React, { Component } from 'react';
import './App.css';
import Form from './components/Form';
import Item from './components/Item';
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list:[{
        computed: false,
        val: '点击可划线'
      }]
    }
  }
  add = (val) => {
    this.setState({
      list: [{computed: false,val}].concat(this.state.list)
    });
  }
  render() {
    return (
      <div className="App">
        <Form add={this.add.bind(this)}/>
        <div className={'list'}>
          {
            // map遍历
            this.state.list.map((item, index) => {
              return <Item item={item} key={index} ></Item>
            })
          }
        </div>
      </div>
    );
  }
}
export default App;

From.jsx

import './Form.css';
import React, {useState} from 'react';
 function Form(props) {
  // 基本挂钩 
  // 在函数组件里声明state属性
  // const [ value, setValue ] = useState(value的初始值);
  const [value, setValue] = useState('');
  const clickHandle = () => {
    if ( value.length > 0 ) {
      // 调用父组件
      props.add(value);
    }
    // 把input内容清空
    setValue('');
  }
  return (
    <div className="wrapper">
      <input
       value={value} 
       //  在元素值改变时  
       onChange={(ev) =>{setValue(ev.target.value)}} 
       className="input"
       placeholder={".........别空"}
      />
      {/* 点击时-->增功能 */}
      <button className={'btn'} onClick={clickHandle}>
        增
      </button>
    </div>
  );
}
export default Form;

Item.jsx

import React, {useState} from 'react';
import './Item.css';
function Item(props) {
  // const [computed, setCompoted] = useState(computed的初始值);
  const [computed, setCompoted] = useState(props.item.computed);
  const clickHandle = () => {
    setCompoted(!computed);
  };
  return (
    // 点击事件
    <div className="item" onClick={clickHandle}>
      {
        // 三目运算符
        !computed ? 
        <p >{props.item.val}</p> 
        : 
        <s >{props.item.val}</s>
      }
    </div>
  );
}
export default Item;
第二段
这里有一段是从一篇帖子拿的 现在历史记录没找到(有点太杂) 后续会添加 提前抱歉

如何在组件加载时发起异步任务

这类需求非常常见,典型的例子是在列表组件加载时发送请求到后端,获取列表后展现。

发送请求也属于 React 定义的副作用之一,因此应当使用 useEffect 来编写。基本语法我就不再过多说明,代码如下:

import React, { useState, useEffect } from 'react';
const SOME_API = '/api/get/value';
export const MyComponent: React.FC<{}> = () => {
const [loading, setLoading] = useState(true);
const [value, setValue] = useState(0);
useEffect(() => {
    (async () => {
        const res = await fetch(SOME_API);
        const data = await res.json();
        setValue(data.value);
        setLoading(false);
    })();
}, []);
return (
    <>
    {loading ? (
        <h2>Loading...</h2>
    ) : (
        <h2>value is {value}</h2>
    )}
    </>
);
}

如上是一个基础的带 Loading 功能的组件,会发送异步请求到后端获取一个值并显示到页面上。如果以示例的标准来说已经足够,但要实际运用到项目中,还不得不考虑几个问题。

如果在响应回来之前组件被销毁了会怎样?

React 会报一个 Warning

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in 
your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification
大意是说在一个组件卸载了之后不应该再修改它的状态。虽然不影响运行,但作为完美主义者代表的程序员群体是无法容忍这种情况发生的,那么如何解决呢?
问题的核心在于,在组件卸载后依然调用了 setValue(data.value) 和 setLoading(false) 来更改状态。因此一个简单的办法是标记一下组件有没有被卸载,可以利用 useEffect 的返回值。
// 省略组件其他内容,只列出 diff
useEffect(() => {
let isUnmounted = false;
(async () => {
    const res = await fetch(SOME_API);
    const data = await res.json();
    if (!isUnmounted) {
        setValue(data.value);
        setLoading(false);
    }
})();
return () => {
    isUnmounted = true;
}
}, []);

这样可以顺利避免这个 Warning。

有没有更加优雅的解法?

上述做法是在收到响应时进行判断,即无论如何需要等响应完成,略显被动。一个更加主动的方式是探知到卸载时直接中断请求,自然也不必再等待响应了。这种主动方案需要用到 AbortController。

AbortController 是一个浏览器的实验接口,它可以返回一个信号量(singal),从而中止发送的请求。这个接口的兼容性不错,除了 IE 之外全都兼容(如 Chrome, Edge, FF 和绝大部分移动浏览器,包括 Safari)。

useEffect(() => {
let isUnmounted = false;
const abortController = new AbortController(); // 创建
(async () => {
    const res = await fetch(SOME_API, {
        singal: abortController.singal, // 当做信号量传入
    });
    const data = await res.json();
    if (!isUnmounted) {
        setValue(data.value);
        setLoading(false);
    }
})();

return () => {
    isUnmounted = true;
    abortController.abort(); // 在组件卸载时中断
}
}, []);

singal 的实现依赖于实际发送请求使用的方法,如上述例子的 fetch 方法接受 singal 属性。如果使用的是 axios,它的内部已经包含了 axios.CancelToken,可以直接使用,例子在这里。

不要忘记依赖、不要打乱Hook的顺序

先说Hook的顺序,在很多文章中,都有介绍Hook的基本实现或模拟实现原理,这里不再多讲,有兴趣可以自行查看。总结来说就是,Hook实现的时候依赖于调用索引,当某个Hook在某一次渲染时因条件不满足而未能被调用,就会造成调用索引的错位,进而导致结果出错。这是和Hook的实现方式有关的原因,只要记住Hook不能书写在 if 等条件判断语句内部即可。