由于工作的原因我已经很长时间没接触过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 (<div><p>You have clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click</button></div>)
}

调用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 (<div><p>name: {name}</p><p>age: {age}</p></div>)
}

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

const stateArr = []
const setterArr = []
let cursor = 0
let 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 (<li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>)
}

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 (<><input ref={inputEl} type="text" /><button onClick={onButtonClick}>Focus the input</button></>)
}

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

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 = idreturn () => {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.currentreturn <h1>Now: {count}, before: {prevCount}</h1>
}

参考文章&拓展阅读

  • React Hooks官方文档
  • React hooks: not magic, just arrays
  • Why Isn’t X a Hook?
  • Making Sense of React Hooks

浅谈React Hooks相关推荐

  1. 浅谈 React 生命周期

    浅谈 React 生命周期 浅谈 React 生命周期 旧版的生命周期 新版的生命周期 详解各个生命周期函数 constructor getDerivedStateFromProps render c ...

  2. 【转】浅谈React、Flux 与 Redux

    本文转自<浅谈React.Flux 与 Redux>,转载请注明出处. React React 是一个 View 层的框架,用来渲染视图,它主要做几件事情: 组件化 利用 props 形成 ...

  3. 浅谈react和vue

    浅谈React 和 Vue 相同之处: React 和 Vue 有许多相似之处,它们都有: ·       使用 Virtual DOM ·       提供了响应式 (Reactive) 和组件化 ...

  4. 浅谈 React Fiber

    2016 年都已经透露出来的概念,这都 9102 年了,我才开始写 Fiber 的文章,表示惭愧呀.不过现在好的是关于 Fiber 的资料已经很丰富了,在写文章的时候参考资料比较多,比较容易深刻的理解 ...

  5. 浅谈React虚拟DOM

    为什么要使用虚拟DOM 因为浏览器的DOM渲染是非常消耗性能的,很低效,我们使用虚拟DOM是为了提高DOM的渲染性能: 什么是虚拟DOM 虚拟DOM就是把真实的DOM树通过createElement转 ...

  6. Java防止Xss注入json_浅谈 React 中的 XSS 攻击

    作者:陈吉 转发链接:https://mp.weixin.qq.com/s/HweEFh78WXLawyQr_Vsl5g 前言 前端一般会面临 XSS 这样的安全风险,但随着 React 等现代前端框 ...

  7. 【转载】浅谈React编程思想

    React是Facebook推出的面向视图层开发的一个框架,用于解决大型应用,包括如何很好地管理DOM结构,是构建大型,快速Web app的首选方式. React使用JavaScript来构建用户界面 ...

  8. 浅谈react hook ( ref)

    import React ,{useRef,useState,PureComponent} from "react"; import ReactDOM from "rea ...

  9. 浅谈React和VDom关系

    组件化 组件的封装 组件的复用 组件的封装 视图 数据 视图和数据之间的变化逻辑 import React, {Component} from 'react';export default class ...

  10. 浅谈React与jQuery的思维差异

    为什么越来越多的互联网公司都在转向React.js去开发前端组件,除了性能因素外,很大一部分原因是因为用jQuery去写很复杂的DOM操作,后期代码会变得越来越难维护.现在大部分的 Web APP 都 ...

最新文章

  1. poj1330Nearest Common Ancestors 1470 Closest Common Ancestors(LCA算法)
  2. 最新开源Faster-LIO:快速激光IMU里程计
  3. GridView中TextBox 有内容,选中当前行CheckBox
  4. Linux Kernel aarch64 Crypto原理和框架介绍
  5. C#interface定义及使用浅析
  6. Mybatis CRUD注解Annotation的使用
  7. php bigpipe实现,如何通过php 实现BigPipe分块输出
  8. 【Linux系统编程学习】 GDB调试器的简单使用
  9. android 锁屏 home,android 锁屏界面禁用长按home 和menu(recent apps)
  10. 记录ionic 最小化应用时所遇的问题
  11. java将date类型转成yyyymmdd_java中的Date怎么转换成YYYYMMDD形式?
  12. Nginx的UDP健康检查
  13. JavaScript模拟call和apply的实现
  14. 负债人有尊严吗?我觉得真的没有
  15. jqAutoComplete 和 knockout
  16. tensorflow架构_TensorFlow半监督对象检测架构
  17. 安卓开发板烧写程序与安装软件的区别_总结一下各大开发板android烧写方式
  18. lena.raw图片文件下载及打开方式
  19. 第14章Stata因变量受限回归分析
  20. Intellij idea破解2017

热门文章

  1. position之属性
  2. CentOS 7.0yum安装MySQL
  3. js中的数据转换、整数、小数保存、四舍五入
  4. php页面传递参数值几种方法总结
  5. 短视频图像处理 OpenGL ES 实践
  6. 多语言样式容器内容超出父级宽度不换行显示
  7. android-Handler
  8. Mysql查询按照某字段指定顺序排序
  9. Linq,企业类库,EXECL生成,Execl chart的一些基本操作记录.(一)
  10. XLN Audio Addictive Trigger for Mac(智能鼓音替换工具)