博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
浅谈React Hooks
阅读量:6911 次
发布时间:2019-06-27

本文共 6427 字,大约阅读时间需要 21 分钟。

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

缘由

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

  1. 难以复用stateful的代码,render props及HOC虽然解决了问题,但对组件的包裹改变了组件树的层级,存在冗余;
  2. 在ComponentDidMount、ComponentDidUpdate、ComponentWillUnmount等生命周期中做获取数据,订阅/取消事件,操作ref等相互之间无关联的操作,而把订阅/取消这种相关联的操作分开,降低了代码的可读性;
  3. 与其他语言中的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 = 0let isFirstRender = truefunction 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}

    }

    参考文章&拓展阅读

    转载地址:http://bqfcl.baihongyu.com/

    你可能感兴趣的文章
    FortiGate下视频会议等语音相关配置
    查看>>
    ERP按序打印问题
    查看>>
    文件的IO(输入/输出) 和 文件系统
    查看>>
    线程安全的小例子
    查看>>
    Android入门(八)广播
    查看>>
    Java锁--非公平锁
    查看>>
    java开始到熟悉61
    查看>>
    表示层设计模式:Intercepting Filter(截取筛选器)模式
    查看>>
    设计模式 之 《外观模式》
    查看>>
    淘宝初始化代码 - 解决浏览器的兼容问题
    查看>>
    xcode5修改APP名字
    查看>>
    Python 元组
    查看>>
    36.CSS3多列布局
    查看>>
    Spark的动态资源分配
    查看>>
    解决类似 /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found 的问题
    查看>>
    robotframwork的WEB功能测试(一)—切换window窗口
    查看>>
    云时代架构读后感12--架构师是怎样炼成的
    查看>>
    js事件中绑定另一事件导致事件多次执行
    查看>>
    堆与栈
    查看>>
    TCP聊天工具
    查看>>