因为工做的缘由我已经很长时间没接触过React了。前段时间圈子里都在讨论React Hooks,出于好奇也学习了一番,特此整理以加深理解。javascript
在web应用无所不能的9012年,组成应用的Components也愈来愈复杂,冗长而难以复用的代码给开发者们形成了不少麻烦。好比:html
主要介绍state hook,effect hook及custom hookjava
最基本的应用以下:react
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。web
再看看有多个“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 = 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执行的顺序。ide
import React, { useState, useEffect } from 'react'
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null)
// 基本写法
useEffect(() => {
document.title = 'Dom is ready'
})
// 须要取消操做的写法
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
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])
复制代码
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);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
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 = 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 <h1>Now: {count}, before: {prevCount}</h1>
}
复制代码