React 16.8 Hooks

一、介绍

使用过一段时间,特别是开发过App肯定清楚在组件使用时,会给不同的组件完成不同的state配置用来描述不同的工作任务。虽然简单控件容易编写页面,但是对于其状态逻辑的数据管理和跟踪却显得难以控制和复杂。
如:我们需要对一个简单的数据count参数在componentDidMountcomponentWillUnmount来进行不可预计的处理,更不需要说shouldComponentUpdate来进行数据更新检测、componentDidUpdate检测部分更新完成数据的二次封装任务。更有情况我们需要对一个指定的数据修改功能添加2个,3个维护数据进行管理。
这样由于生命周期的管理必要,和多组数据的共同管理,使得在一个极小的组件里维护显得庞大。
为了解决这个问题,Hooks允许根据相关的部分(例如设置订阅或获取数据)将一个组件拆分为较小的功能,而不是基于生命周期方法强制拆分。

当然,Hooks是向后兼容,因此没有必须学习或者将项目全部修改。

二、State Hook

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

简单使用

这是一个累加器。

import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity
} from 'react-native';

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

return (
<View style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{count}</Text>
<TouchableOpacity onPress={() => setCount(count+1)}><Text>下一个</Text></TouchableOpacity>
</View>
);
}
export default Ex;

看到这里返回了一个组件,而其中最注意的就是useState()。这是一个Hook,添加一个状态。React将会在重新渲染时保留这个状态,userState()函数返回一个当前的状态值(参数为初始化数值)和一个用于更新他的函数,他和this.setState()很像。
该语法为数组解构。创建的两个新变量为countsetCount。类似如下
var countStateValue = useState(0);
var first = countStateValue[0];
var setFirst = countStateValue[1];

初始化回调函数

也可以将初始化的工作放到回调函数中完成,类似class中的constructor构建,他只会在初始化的时候完成调用该回调函数。

const [count, setCount] = useState(() => {
console.log('----->初始化');
return 0;
})

这里就类似最简单的,没有与生命周期相关的Hook。

与class组件中的setState方法不同,useState不会自动合并更新对象。

三、Effect Hook

useEffect(didUpdate);

基本使用

useEffect()这里开始就与生命周期。他会影响到componentDidMountcomponentDidUpdatecomponentWillUnmount

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

useEffect(() => {
console.log('----->');
return () => {
console.log('----->over');
}
});

return (
<View style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{count}</Text>
<TouchableOpacity onPress={() => setCount(count+1)}><Text>+1</Text></TouchableOpacity>
</View>
);
}

与生命周期对比

看到使用useEffect函数第一个参数是一个函数,其内部的正常调用就是调用componentDidMountcomponentDidUpdate时会调用除return外的所有部分;调用componentWillUnmount函数时会调用return函数内部内容。

...
componentDidMount() {
console.log('----->');
}
componentWillUnmount() {
console.log('----->over');
console.log('----->');
}
componentDidUpdate() {
console.log('----->over');
console.log('----->');
}

其他参数

在开发优化时,根据判断来减少调用次数,这在开发中很常见。为了解决这个问题,useEffect提供了第二个参数来控制调用条件。

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

useEffect(() => {
console.log('----->');
return () => {
console.log('----->over');
}
}, []);
...

第二个参数必须是数组格式,在传递空数组时,由于我们没有给控制条件(空数组),所以在调用setCount更新state过程中componentDidUpdate函数会过滤掉,即只有componentDidMountcomponentWillUnmount有意义。

如果添加数组内容:

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

useEffect(() => {
console.log('----->');
return () => {
console.log('----->over');
}
}, [count]);
...

只有在数组内的数值发生变化时,才会使该useEffectHook在修改state时有意义,如果没有变化,则会继续过滤掉componentDidUpdate函数

自定义Effect Hooks

官方实例:
写一个判断朋友是否在线的功能,将这段状态抽出来。

这里判断某个id是否上线:

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

完成state状态封装后,我们就可以在两个组件中使用他:

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

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online':'Offline';
}

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

return (
<li style={{color: isOnline? 'green':'black'}}>
{props.friend.name}
</li>
);
}

这些组件的状态是完全独立的。Hooks是重用有状态逻辑而不是状态本身的一种方法。因此每个对Hooks的调用都具有完全隔离的状态。
这里使用的函数名称use开头表示了自定义Hook的意思,并且内部也调用了其他Hook。类似一种约定命名。

四、其他Hooks

有一些不太常用的内置Hooks可能会有用。

useContext

const value = useContext(MyContext);
该Hook接受一个React.createContext()返回的值。Context的作用就是对他所包含的组件树提供全局共享数据的一种技术规范。

const ThemeContext = React.createContext();

const ExContext = function () {
const theme = useContext(ThemeContext);

return (
<View style={{backgroundColor: theme}}>
<TouchableOpacity ><Text>Context</Text></TouchableOpacity>
</View>
);
}

//根节点
render() {
return (
<ThemeContext.Provider value='#00f'>
<View>
<ExContext/>
</View>
</ThemeContext.Provider>
);
}

必须将<ThemeContext.Provider>放置父组件位置才能传递内部存放数据。当这个数据更新时,将触发Hook来重新渲染。当这个渲染组件开销很大,可能需要看https://github.com/facebook/react/issues/15156#issuecomment-474590693来优化。
使用useContext后就会在这里得到父节点传递下来的数据内容。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
useState的高级Hook。看名字就知道他是一个Reducer函数,(state, action) => newState。与Redux具有相同的工作原理。
先来看看使用useReducer的实例:

const ExReducer = function () {
const initialState = {count: 0};
const [state, dispatch] = useReducer(reducer, initialState);

return (
<View style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{state.count}</Text>
<TouchableOpacity onPress={() => dispatch({type: 't1'})}><Text>+1</Text></TouchableOpacity>
</View>
);
}
const reducer = (state, action) => {
switch(action.type) {
case 't1':
return {count: state.count+1};
break;
case 't2':
return {count: state.count-1};
break;
}
}

这样就完全模拟出useState时完成的功能状态。

延迟初始化

添加第三个传递参数完成延迟初始状态。

const ExReducer = function () {
const initialState = {count: 0};
const [state, dispatch] = useReducer(reducer, initialState, init);

return (
<View style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{state.count}</Text>
<TouchableOpacity onPress={() => dispatch({type: 't1'})}><Text>+1</Text></TouchableOpacity>
<TouchableOpacity onPress={() => dispatch({type: 'reset', payload: initialState})}><Text>reset</Text></TouchableOpacity>
</View>
);
}
const init = (initialState) => {
return {count: initialState.count};
}
const reducer = (state, action) => {
switch(action.type) {
case 't1':
return {count: state.count+1};
break;
case 't2':
return {count: state.count-1};
break;
case 'reset':
return init(action.payload);
break;
}
}

这里添加了一个init初始化函数参数,可以在这里添加需要初始化的功能(这个初始化也会在复位的时候重复调用)。这也很方便某个操作的重置状态。
React使用Object.is比较算法,判断Hook中返回与当前状态相同的值,则React将不会进行数据返回。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个memoized数值。

memo大致

memoize是一种JavaScript技术,通过缓存数据的参数特征与其对应结果,来提升数据重复调用的速度。核心思想就是通过在每个指定功能函数中添加一个数组,来存储刚刚说的入参特征与其对应结果,来达到提升效率的方法。

memoize = (fn) => {
return function() {
var args = Array.prototype.slice.call(arguments);
fn.cache = fn.cache || {};
return fn.cache[args]? fn.cache[args] : (fn.cache[args] = fn.apply(this, args));
}
}

当我们将存储层级提升到Function原型中,就可以在每个函数内部找到memoize函数,并完成调用。
Function.prototype.memoize = function() {
var self = this
return function () {
var args = Array.prototype.slice.call(arguments)
self.cache = self.cache || {};
return self.cache[args] ? self.cache[args] : (self.cache[args] = self(args))
}
}

回到Hook

相似的,useMemo思路与memo相似。他也会保存指定数据的最近数值根据对比判断是否需要对页面进行重新渲染的过程。
先看一个反例:

const ExMemo = function() {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0);

function sum() {
console.log('----->至查看count值');
return count + 1;
}

return (
<View style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{sum()}</Text>
<TouchableOpacity onPress={() => {
setCount(count+1);
}}><Text>Btn1</Text></TouchableOpacity>
<TouchableOpacity onPress={() => {
setValue(value+1);
}}><Text>Btn2</Text></TouchableOpacity>
</View>
)
}

这里有两个状态countvalue,并添加两个对应的按钮来分别给这两个数据做累加处理,而sum函数只会对count进行一次查询处理。这个过程,无论是修改count还是value都会引起函数sum的调用,但是这个函数的关心点只有count。这就引出了多余的计算问题。

useMemo则是解决这个问题的方式。

const ExMemo = function() {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0);

const sumValue = useMemo(() => {
console.log('----->至查看count值');
return count + 1;
}, [count]);

return (
<View style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{sumValue}</Text>
<TouchableOpacity onPress={() => {
setCount(count+1);
}}><Text>Btn1</Text></TouchableOpacity>
<TouchableOpacity onPress={() => {
setValue(value+1);
}}><Text>Btn2</Text></TouchableOpacity>
</View>
)
}

这里就可以看到,当修改value参数时,这个调用函数不会被执行,而是通过缓存提升速度。

useCallback

const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
这里和useMemo的思想类似。但这里缓存的是函数。

useRef

const refContainer = useRef(initialValue);
返回一个可变的ref对象。通过.current对象来保存。
ref本是用来获取组件实例对象或Dom对象。

const ExRef = function() {
const [count, setCount] = useState(0);
const thisRef = useRef();
const doubleCount = useMemo(() => {
return 2*count;
}, [count]);
useEffect(() => {
thisRef.current = setInterval(() => {
setCount(count => count+1);
}, 1000);
}, []);
useEffect(() => {
if (count>10)
clearInterval(thisRef.current);
})

return (
<View ref={thisRef} style={{justifyContent: 'flex-start', flexDirection: 'column', alignItems: 'center'}}>
<Text>{count}-{doubleCount}</Text>
</View>
)
}

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])
用于自定义暴露给父组件的ref属性。需要配个forwardRef一起使用。

先看看子组件:

var ExImperativeHandle = function(props, ref) {
const textRef = useRef();
useImperativeHandle(ref, () => ({
value: textRef.current
}));
return (
<View>
<Text ref={textRef}>DDD</Text>
</View>
);
}
ExImperativeHandle = forwardRef(ExImperativeHandle);
export {
ExImperativeHandle
};

看到这里首先准备了Ref对象,将这个对象里面的数据暴露给父组件使用。这个过程就使用了useImperativeHandle,完成后还需要前面提到的forwardRef

useLayoutEffect

和前面的useEffect相同。但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useDebugValue

用于在React DevTools中显示自定义Hooks的标签。会在自定义Hoos标签中调试