资讯专栏INFORMATION COLUMN

浅谈React Hooks

yearsj / 1511人阅读

摘要:另外也不利于组件的,及。所以在使用时,尽量将相关联的,会共同变化的值放入一个。有同学可能会想,每次后都会执行,这样会不会对性能造成影响。另外必须以开头来命名,这样工具才能正确检测其是否符合规范。

由于工作的原因我已经很长时间没接触过React了。前段时间圈子里都在讨论React Hooks,出于好奇也学习了一番,特此整理以加深理解。
缘由

在web应用无所不能的9012年,组成应用的Components也越来越复杂,冗长而难以复用的代码给开发者们造成了很多麻烦。比如:

难以复用stateful的代码,render props及HOC虽然解决了问题,但对组件的包裹改变了组件树的层级,存在冗余;

在ComponentDidMount、ComponentDidUpdate、ComponentWillUnmount等生命周期中做获取数据,订阅/取消事件,操作ref等相互之间无关联的操作,而把订阅/取消这种相关联的操作分开,降低了代码的可读性;

与其他语言中的class概念差异较大,需要对事件处理函数做bind操作,令人困扰。另外class也不利于组件的AOT compile,minify及hot loading。

在这种背景下,React在16.8.0引入了React Hooks。

特性

主要介绍state hook,effect hook及custom hook

State Hook

最基本的应用如下:

import React, { useState } from "react"

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

  return (
    

You have clicked {count} times

) }

调用useState,传入初始值,通过数组的结构赋值得到独立的local state count,及setCount。count可以理解为class component中的state,可见这里的state不局限于对象,可以为number,string,当然也可以是一个对象。而setCount可以理解为class component中的setState,不同的是setState会merge新老state,而hook中的set函数会直接替换,这就意味着如果state是对象时,每次set应该传入所有属性,而不能像class component那样仅传入变化的值。所以在使用useState时,尽量将相关联的,会共同变化的值放入一个object。

再看看有多个“local state”的情况:

import React, { useState } from "react"

function person() {
  const [name, setName] = useState("simon")
  const [age, setAge] = useState(24)

  return (
    

name: {name}

age: {age}

) }

我们知道当函数执行完毕,函数作用域内的变量都会销毁,hooks中的state在component首次render后被React保留下来了。那么在下一次render时,React如何将这些保留的state与component中的local state对应起来呢。这里给出一个简单版本的实现:

const stateArr = []
const setterArr = []
let cursor = 0
let isFirstRender = true

function createStateSetter(cursor) {
  return state => {
    stateArr[cursor] = state
  }
}

function useState(initState) {
  if (isFirstRender) {
    stateArr.push(initState)
    setterArr.push(createStateSetter(cursor))

    isFirstRender = false
  }

  const state = stateArr[cursor]
  const setter = setterArr[cursor]

  cursor++

  return [state, setter]
}

可以看出React需要保证多个hooks在component每次render的时候的执行顺序都保持一致,否则就会出现错误。这也是React hooks rule中必须在top level使用hooks的由来——条件,遍历等语句都有可能会改变hooks执行的顺序。

Effect Hook
import React, { useState, useEffect } from "react"

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

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

  // 基本写法
  useEffect(() => {
    document.title = "Dom is ready"
  })

  // 需要取消操作的写法
  useEffect(() => {
    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的函数(effect)中做了一些"side effect",在class component中我们通常会在componentDidMount,componentDidUpdate中去做这些事情。另外在class component中,需要在componentDidMount中订阅,在componentWillUnmount中取消订阅,这样将一件事拆成两件事做,不仅可读性低,还容易产生bug:

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";
  }
}

如上代码,如果props中的friend.id发生变化,则会导致订阅和取消的id不一致,如需解决需要在componentDidUpdate中先取消订阅旧的再订阅新的,代码非常冗余。而useEffect hook在这一点上是浑然天成的。另外effect函数在每次render时都是新创建的,这其实是有意而为之,因为这样才能取得最新的state值。

有同学可能会想,每次render后都会执行effect,这样会不会对性能造成影响。其实effect是在页面渲染完成之后执行的,不会阻塞,而在effect中执行的操作往往不要求同步完成,除了少数如要获取宽度或高度,这种情况需要使用其他的hook(useLayoutEffect),此处不做详解。即使这样,React也提供了控制的方法,及useEffect的第二个参数————一个数组,如果数组中的值不发生变化的话就跳过effect的执行:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  }
}, [props.friend.id])
Custom Hook
A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.

Custom Hook的使命是解决stateful logic复用的问题,如上面例子中的FriendStatus,在一个聊天应用中可能多个组件都需要知道好友的在线状态,将FriendStatus抽象成这样的hook:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

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

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id)

  if (isOnline === null) {
    return "Loading..."
  }

  return isOnline ? "Online" : "Offline"
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id)

  return (
    
  • {props.friend.name}
  • ) }

    FriendStatus和FriendListItem中的isOnline是独立的,因custom hook复用的是stateful logic,而不是state本身。另外custom hook必须以use开头来命名,这样linter工具才能正确检测其是否符合规范。

    除了以上三种hook,React还提供了useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue内置hook,它们的用途可以参考官方文档,这里我想多带带讲讲useRef。
    顾名思义,这个hook应该跟ref相关的:

    function TextInputWithFocusButton() {
      const inputEl = useRef(null)
      const onButtonClick = () => {
        inputEl.current.focus()
      }
      return (
        <>
          
          
        
      )
    }

    来看看官方文档上的说明:

    useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime    of the component.

    这句话告诉我们在组件的整个生命周期里,inputEl.current都是存在的,这扩展了useRef本身的用途,可以使用useRef维护类似于class component中实例属性的变量:

    function Timer() {
      const intervalRef = useRef()
    
      useEffect(() => {
        const id = setInterval(() => {
          // ...
        })
        intervalRef.current = id
        return () => {
          clearInterval(intervalRef.current)
        }
      })
    
      // ...
    }

    这在class component中是理所当然的,但不要忘记Timer仅仅是一个函数,函数执行完毕后函数作用域内的变量将会销毁,所以这里需要使用useRef来保持这个timerId。类似的useRef还可以用来获取preState:

    function Counter() {
      const [count, setCount] = useState(0)
    
      const prevCountRef = useRef()
      useEffect(() => {
        prevCountRef.current = count    // 由于useEffect中的函数是在render完成之后异步执行的,所以在每次render时prevCountRef.current的值为上一次的count值
      })
      const prevCount = prevCountRef.current
    
      return 

    Now: {count}, before: {prevCount}

    }
    参考文章&拓展阅读

    React Hooks官方文档

    React hooks: not magic, just arrays

    Why Isn’t X a Hook?

    Making Sense of React Hooks

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/102216.html

    相关文章

    • 浅谈reacthooks闭包陷阱

        本文不会过多讲解基础知识,更多说的是在使用useRef如何能摆脱 这个 闭包陷阱 ?  react hooks 的闭包陷阱 基本每个开发员都有遇见,这是很令人抓狂的。  (以下react示范demo,均为react 16.8.3 版本)  列一个具体的场景:  functionApp(){   const[count,setCount]=useState(1);   useEffect(()=...

      3403771864 评论0 收藏0
    • 浅谈webpack之plugin,webpack-manifest-plugin源码解读

      摘要:注册方法之后,当执行了当前的,那么挂载正在当前上的方法就会被执行。比如在开始编译之前,就能触发钩子,就用到了当前的。上面都是前置知识,下面通过解读一个源码来巩固下。先看一段简单的源码。,是众多的的一个,官网的解释是编译创建之后,执行插件。 通过解读webpack-manifest-plugin,了解下plugin机制 先简单说一下这个插件的功能,生成一份资源清单的json文件,如下 s...

      william 评论0 收藏0
    • 浅谈ReactFiber

      摘要:什么是每一个都有一个对应的,记录这个节点的各种状态,是一链表的结构的串联起来。 1. 什么是fiber 每一个ReactElement都有一个对应的fiber, 记录这个节点的各种状态, fiber是一链表的结构的串联起来。showImg(https://segmentfault.com/img/bVbqVZR?w=540&h=708); 2. Fiber的组成 export type...

      yibinnn 评论0 收藏0
    • 精读《React Hooks

      摘要:更容易将组件的与状态分离。也就是只提供状态处理方法,不会持久化状态。大体思路是利用共享一份数据,作为的数据源。精读带来的约定函数必须以命名开头,因为这样才方便做检查,防止用判断包裹语句。前端精读帮你筛选靠谱的内容。 1 引言 React Hooks 是 React 16.7.0-alpha 版本推出的新特性,想尝试的同学安装此版本即可。 React Hooks 要解决的问题是状态共享,...

      kohoh_ 评论0 收藏0
    • 新上课程推荐:《React Hooks 案例详解(React 进阶必备)》

      摘要:课程制作和案例制作都经过精心编排。对于开发者意义重大,希望对有需要的开发者有所帮助。是从提案转为正式加入的新特性。并不需要用继承,而是推荐用嵌套。大型项目中模块化与功能解耦困难。从而更加易于复用和独立测试。但使用会减少这种几率。 showImg(https://segmentfault.com/img/bVbpNRZ?w=1920&h=1080); 讲师简介 曾任职中软军队事业部,参与...

      Lin_YT 评论0 收藏0

    发表评论

    0条评论

    yearsj

    |高级讲师

    TA的文章

    阅读更多
    最新活动
    阅读需要支付1元查看
    <