文章首发于3月29日。html
在react的今天和明天系列文章中,react开发人员介绍了class组件存在的三个问题。react
在class组件中经过HOC和render props中来实现组件的逻辑复用。这会带来一个问题,当咱们拆分出不少细小的组件再将它们组合到一块儿,若是在chrome打开react的扩展,会发现组件层级很是深,增长了react的计算负担。这个问题称为“包裹地狱(wrapper hell)”。git
当咱们试图解决第一个问题的时候,咱们会将更多的逻辑放在单个的组件中,致使组件日益庞大。这就是第二个问题。github
js中的class其实是function的语法糖。在使用过程当中,它隐藏了function的实现细节(static, prototype等)。而且当咱们写一个函数组件的时候,若是要添加状态,那么就必须将它转换为class组件,这会写不少样板代码。不只如此,对于机器来讲,压缩后的class的组件中的全部的方法名都是没有通过压缩的。而且也没法tree shaking。这是class组件的第三个问题。算法
Dan Abramov
认为这不是三个独立的问题,而是一个问题的三个部分。为了解决这个问题,因而出现了hooks。chrome
hooks是另外一种书写组件的方式,在hooks中只有函数而没有class。经过hooks提供的一系列API几乎能够彻底覆盖class组件中的状况(为何说是几乎?在下面差别部分会提到)。hooks是更加简洁优雅的,逻辑分离的。json
v16.8。hooks是react16.8版本新添加的功能。若是要使用hooks,须要确保react版本升级到16.8.0及以上,同时保证react-dom和react的版本保持一致。redux
百分之百向后兼容数组
react没有计划移除class组件。hooks是一种可选的写法,你依然能够选择不用而使用class(用过了你会喜欢上它)。浏览器
hooks中使用的依旧是class中的概念。
hooks有下列API。全部的示例demo都在这里。codesandbox.io/s/o968n1q62… ,能够打开并直接看到效果。
接下来会解释这些API并附有详细的demo。
useState是hooks中最基础的一个API。其使用方式相似于class中的this.setState,不过又有所不一样。下面是demo中的示例。
function Counter() {
const [count, setCount] = useState(4);
return (
<>
<p>
count: {count}, random: {Math.random()}
</p>
<button onClick={() => setCount(count < 5 ? count + 1 : count)}>
点击这里加1
</button>
<button onClick={() => setCount(count - 1)}>点击这里减1</button>
</>
);
}
复制代码
useState(4)
用于建立一个state变量,并传入初始值。返回值是一个数组,经过解构写法拿到返回的值。count是一个可变的值(只能经过setCount改变),它的初始值是useState传入的参数4。单击按钮的时候,经过setCount去改变它的值。从而从新渲染该组件。
它与class组件中的setState的差别有如下几点:
另外,若是useState的参数值须要复杂的计算才能获得,那么能够传入给useState一个函数,它仅在首次渲染时执行该复杂的计算函数。useState(() => expensiveCalc())
。
因为useEffect和useLayoutEffect具备相同的使用方式,就放到一块儿来介绍。 正如其名,useEffect用来处理反作用。反作用包括DOM的改变、订阅、定时器、日志等。反作用不容许放到函数体中。
useEffect/useLayoutEffect
能够取代componentDidMount、componentDidUpdate、componentWillUnmount三个声明周期。因此它是很是强大一个hook,在使用方面很灵活,也不是那么容易理解。
useEffect
接受两个参数,其中第二个参数是可选的,不过通常状况下都须要传入第二个参数。
useEffect(() => {
// some side effect
// ...
// return a clean up function
return () => {
}
}, [])
复制代码
第一个参数是一个函数,当组件首次渲染或者其依赖的状态改变时它会执行。该函数的返回值是可选的,能够不写,若是要写的话,必须是一个函数,用于清除上一个状态。
第二个参数是可选的,它是一个数组。数组中能够传入状态值(经过useState产生的值),当状态值改变的时候首先会执行return函数,用于清理上一个状态,而后useEffect中的函数就会再次执行。
// demo--useLayoutEffect
useEffect(() => {
if (value.length > 10) {
setValue(value.substring(0, 10));
}
setLengths(value.length);
},[value]);
复制代码
当value发生变化的时候,effect再次执行。改变length状态。
useLayoutEffect的语法和useEffect同样。不一样点在于:
在useLayoutEffect的demo中,尝试将useEffect改变成useLayoutEffect,而后在输入框输入第十一个字符,能够看到明显差异。在大多数状况下你应该使用useEffect。由于useEffect是异步的,不会堵塞主线程渲染。
在16.3版本中,引入React.createRef,来代替字符串ref。一样,在hooks中,useRef也能够取代createRef。能够查看demo中的useRef。
function xxx() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.value = 'hello';
}, [])
return (
<input type="text" ref={inputRef} />
)
}
复制代码
useRef接受一个初始值,返回一个可变的ref对象,ref.current指向初始化的值。它能够指向别的值。
另外,因为是函数组件,this再也不指向这个组件,因此若是要达到class组件中实例变量的效果,也能够经过useRef来实现。
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
inputRef.current.value = 'hello';
}, 1000);
return () => {
clearInterval(timerRef.current);
};
}, []);
复制代码
这里的timerRef.current至关于class中的实例变量。
在React中,若是要将上层的属性传递到下层,通常来讲须要一层一层的传递。好比A->B-C->D。Context是一种数据传递机制,用于跨层级传递数据。好比在D组件能够直接使用A组件的数据。能够查看demo中的useContext。
useContext是Context.Consumer(16.3)以及static contextType(16.6)的一种简写。
上层组件定义Context.Provider,并传入value属性。在子组件中经过useContext(Context)能够获取value属性。
// parent.js
const parentContext = createContext();
function Parent(props) {
const countArr = useState({
count1: 0,
count2: 1
});
return (
<parentContext.Provider value={countArr}>
{props.children}
</parentContext.Provider>
);
}
function Child() {
const countArr = useContext(parentContext);
const [countObj, setCountObj] = countArr;
return (
<>
<div>
count1: {countObj.count1} count2: {countObj.count2}
</div>
</>
);
}
<Parent><Child /></Parent>
复制代码
能够看到,这里传递属性不是经过props的,而是经过Context的。须要注意的是,在父组件定义了parentContext,须要将其导出,由于子组件使用useContext的参数就是parentContext。
另外,由于没有static的限制,同一个组件中可使用多个useContext。也就是说,可使用多个上层组件传递来的数据。
提到reducer,首先想到的应该是redux中的reducer。useReducer这个hook与redux中的reducer有所类似又有所不一样。能够查看demo中的useReducer。
定义一个reducer的方式和redux中是同样的:
const initialState = {
count: 0
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD': {
return {
count: state.count + 1
};
}
case 'MINUS': {
return {
count: state.count - 1
};
}
}
};
function Counter1() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<div>count: {state.count}</div>
<button onClick={() => { dispatch({ type: 'ADD' }); }}>+</button>
<button onClick={() => { dispatch({ type: 'MINUS' }); }}>-</button>
</>
);
}
复制代码
useReducer接受连个参数,一个是reducer,一个是initialState。返回一组值,分别是state和dispatch。经过dispatch触发一个动做,进而去更新状态。若是你使用过redux,那么理解起来没有任何困难。
另外,与redux中的reducer有所不一样的是,useReducer中的reducer是独立的。若是有多个组件使用到了同一个reducer,那么它们之间的状态是独立的。相较于redux的全局共享状态,它还依赖于react-redux提供的Provider组件。
因此是否是忽然想到了第四点中的Context,它提供了Provider。若是能配合useReducer,就能够实现全局状态共享了?确实如此!具体的代码能够查看demo中的context-reducer。
还有一点必须须要提到的是,因为性能缘由,react-redux没有推出官方的useRedux。 具体缘由能够看这篇文章:juejin.im/post/5c7c8d… 。在最近的7.0的beta版本的发布说明中,react-redux团队宣布将在7.x版本中推出useRedux API。
在class组件中,若是父组件须要改变子组件的状态,有两种方式。一种是就是经过改变父组件state,该state做为props传给子组件,从而改变子组件的状态。另外一种就是经过操做子组件的ref了。传递ref的方式主要有两种,createRef和forwardRef,具体的就再也不细说。useImperativeHandle这个hook就是ref的另外一种写法。之前是在父组件中拿到子组件元素的ref,直接操做ref表明的元素节点,至关因而直接操做子元素的dom元素。如今经过这个hook能够在子组件中暴露一些API供父组件调用,而父组件是不能直接操做子组件的dom元素的。迪米特法则就是这样描述的:一个类对它所调用的类的细节知道的越少越好。具体代码能够查看demo中的useImperativeHandle。
function Child(props) {
const inputRef = useRef(null);
useImperativeHandle(props.myref, () => ({
focus() {
inputRef.current.focus();
},
setValue(value) {
inputRef.current.value = value;
}
}));
return (
<>
<input type="text" ref={inputRef} />
</>
);
}
复制代码
不过在实际开发中,你应该尽量经过传递props来改变子组件,经过ref来改变子组件是一种不推荐的方案。
useCallback用来缓存一个函数。在函数式组件中可能有这样一种状况,父组件调用子组件,并将一个函数传递给子组件,假设子组件是使用了memo的(若是属性值没有变化,那么将不会从新渲染)。具体代码能够查看demo中的useCallback。
function Parent() {
//...
const handleChange = () => { // ... }
return (
<>
<p>count: {count}</p>
<Child onChange={handleChange}/>
</>
)
}
复制代码
当父组件中的count状态改变时,这时候父组件从新渲染,子组件尽管没有任何改变,但因为onChange这个属性是一个新的函数,它仍是从新渲染了。这是咱们不指望的行为。在class组件中咱们每每经过onChange={this.handleChange}
将函数传递给子组件。那么下次父组件渲染时,this.handleChange是没有变化的。若是子组件是memo的,那么子组件将不会从新渲染。
因此uesCallback就是为了解决这样一个问题,它缓存一个函数,并接受一系列依赖项,返回一个函数。若是依赖项没有变化,那么返回的函数不会变化。
function Parent() {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const [result, setResult] = useState(count);
useEffect(() => {
setInterval(() => {
// setCount(prevCount => prevCount + 1);
setCount2(prevCount => prevCount + 1);
}, 1000);
}, []);
const handleChange = useCallback(() => {
setResult(count + 1);
}, [count]);
// const handleChange = () => { setResult(count + 1) };
return (
<>
<Counter count={count} />
<Counter count={count2} />
<Child onChange={handleChange} />
<p>result: {result}</p>
</>
);
}
复制代码
这里的handleChange函数是被缓存了的,除非count发生变化,它才会发生变化。经过demo打开控制台,发现随着count2的改变,子组件是不会从新渲染的。
useMemo用来缓存一个复杂的计算值。 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。若是经过一个输入获得一个值须要通过复杂的计算,那么下次一样的输入再进行一遍一样复杂的计算是没有必要的。这正是useMemo存在的意义。具体代码能够查看demo中的useMemo。
function Parent() {
const [count, setCount] = useState(10);
const [count2, setCount2] = useState(10);
console.time('calc');
const result = useMemo(() => computeExpensiveValue(40), [count]);
// const result = computeExpensiveValue(count);
console.timeEnd('calc');
return (
<>
<p>result: {result}</p>
<div>
<input type="number" disabled value={count} />
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<br />
<input type="number" disabled value={count2} />
<button onClick={() => setCount2(count2 + 1)}>+</button>
<button onClick={() => setCount2(count2 - 1)}>-</button>
</div>
</>
);
}
复制代码
useMemo接受一个函数,该函数涉及到复杂的计算,并接受一系列依赖项,返回一个计算后的值。若是依赖项没有变化,那么返回的值不会变化。这里useMemo依赖于count的变化。在页面中分别尝试改变count和count2的值,观察控制台输出的时间。改变count的时候,函数进行了重进计算,打印出的值比较大。改变count2的时候,直接使用缓存的值,打印出的值很小。
须要注意的是,react文档中说明,useMemo只是做为一种暗示,当依赖值变化时,并不必定能保证每一次都不计算。
一般来讲你不须要它。它只会存在于自定义的hooks中用来标志一个自定义的hooks。当在chrome中打开react扩展的时候,若是一个组件使用到了自定义的hooks,而且该hooks使用到了useDebugValue,那么该组件下方会显示useDebugValue传入的参数。
function useUserInfo() {
// ...
useDebugValue('use-user-info');
return userInfo;
}
复制代码
除了官方提供的hooks之外,咱们也能够定义本身的hooks。编写自定义的hooks是很是简单的。你能够像和编写一个正常的组件同样,区别在于它返回的是数据(或者不返回),而不是jsx。使用自定义hooks须要遵循两点。demo中编写了一个自定义的hooks(/components/custom-hooks/use-user-info.js)。
const useUserInfo = (username = 'yuwanlin') => {
const fetchRef = useRef(null);
const [userInfo, setUserInfo] = useState({});
const handleData = data => {
setUserInfo(data);
};
useEffect(() => {
const fetchData = username =>
fetch(`${prefix}${username}`)
.then(res => res.json())
.then(data => {
console.log('fetch success');
handleData(data);
});
fetchRef.current = debounce(fetchData, 1000);
}, []);
useEffect(
() => {
fetchRef.current(username);
},
[username]
);
// useDebugValue('use-user-info');
return userInfo;
};
复制代码
打开use-user-info demo能够看到页面中出现了用户的信息。
enzyme目前尚不支持测试hooks。react官方推出了测试hooks的方案。打开demo,在右边选项卡中。从Browser切换到Tests,能够看到经过了测试。全部位于__test__文件夹下的.test.js结尾的文件都会被当成测试文件。
hooks目前不支持getSnapshotBeforeUpdate和componentDidCatch/getDerivedStateFromError生命周期,之后会加上。
因为每一个函数就是一个组件,那么整个函数体就至关于class组件中的render函数。每次状态改变,都要从新执行函数。下面是一些常见的问题:
在class组件中咱们经过实例变量来保存上一个状态。在函数组件中,能够经过ref来取代实例变量。usePrevious的实现以下:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
复制代码
经过React.memo。
不,在现代浏览器中,与类相比,闭包的原始性能没有显著差别,除了在极端状况下。 此外,考虑到Hooks的设计在如下几个方面更有效:
更多常见的问题,能够参照:reactjs.org/docs/hooks-…
代码中使用hooks的时候,最好配合eslint插件eslint-plugin-react-hooks。
// Your ESLint configuration
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}
复制代码
class组件存在三个问题,逻辑复用、组件庞大、难以理解的class。hooks的存在就是为了解决这三个问题的。而且在一个函数组件中,你能够屡次使用相同的hooks。好比:
function Example() {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => { // ... }, []);
useEffect(() => { // ... }, []);
return (
// ...
)
}
复制代码
将不一样的逻辑放到不一样的hooks,逻辑更加清晰。hooks是class的另外一种写法,在react16.8引入,react并不打算放弃class。