一、前言
- react hooks 是2018年,facebook react官方团队提出的一个react 新特性。
1. 1 引入hooks
- react特点: 组件化和虚拟Dom
- 函数组件:1. 无法存储任何状态 2. 没有自己的生命周期
- 类组件:1. 可以通过setState设置一个或多个状态 2.但是复用时候会有一定成本
- facebook react 推崇函数式编程
- 提出react hooks方案,使函数组件能够拓展一下能力
二、useState
2.1 例子
import React, { useState } from 'react';
function Example(props) {
// 声明一个叫做 “count” 的 state 变量
const [count, setCount] = useState(()=>0);
return (
<div>
<p>You clicked {count} times</p
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2.2 useState是如何实现的呢
2.2.1 从this.setState()说起
-
当我们在组件中调用
setState
的时候,发生了些什么?- React根据下一个状态重新渲染组件,同时更新DOM以匹配返回的元素。
- 更新DOM像是React DOM的职责所在。但是我们调用的是
this.setState()
,而没有调用任何来自React DOM的东西。 而且我们组件的父类React.Component
也是在React本身定义的。 - 所以存在于
React.Component
内部的setState()
是如何更新DOM的呢?
-
实际上从React 0.14将代码拆分成多个包以来,
react
包只暴露一些定义组件的API。绝大多数React的实现都存在于“渲染器(renderers)”中。react-dom
、react-dom/server
、react-native
、react-test-renderer
、react-art
都是常见的渲染器- 这就是为什么不管我们的目标平台是什么,
react
包都是可用的。从react
包中导出的一切,比如React.Component
、React.createElement
、React.Children
或者是hooks,都是独立于目标平台的。无论是运行React DOM,还是 React DOM Server,或是 React Native,组件都可以使用同样的方式导入和使用。
-
所以我们要做的就是使
React.Component
中的setState()
与正确的渲染器“对话”。- **每个渲染器都在已创建的类上设置了一个特殊的字段。**这个字段叫做
updater
。这是React DOM、React DOM Server 或 React Native在我们创建完类的实例之后会立即设置的东西:
- **每个渲染器都在已创建的类上设置了一个特殊的字段。**这个字段叫做
// React DOM 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// React DOM Server 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// React Native 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
setState
所做的一切就是委托渲染器创建这个组件的实例:
// 适当简化的代码
setState(partialState, callback) {
// 使用`updater`字段回应渲染器!
this.updater.enqueueSetState(this, partialState, callback);
}
-
这就是this.setState()尽管定义在React包中,却能够更新DOM的原因。它读取由React DOM设置的
this.updater
,让React DOM安排并处理更新。 -
setState()
它除了将调用转发给当前的渲染器外,什么也没做。 -
hooks useState()也是如此。
-
https://overreacted.io/zh-hans/how-does-setstate-know-what-to-do/
2.2.2 useState实现
- Hooks使用了一个“dispatcher”对象,代替了
updater
字段。 - 当调用
React.useState()
、React.useEffect()
、 或者其他内置的Hook时,这些调用被转发给了当前的dispatcher。
// React内部(简化)
const React = {
// 真实属性隐藏的比较深!!!
__currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
},
// ...
};
- 各个渲染器会在渲染你的组件之前设置dispatcher:
// React DOM 内部
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;let result;
try {
result = YourComponent(props);
} finally {
// 恢复原状 React.__currentDispatcher = prevDispatcher;}
updater
字段和__currentDispatcher
对象都是称为依赖注入的通用编程原则的形式。在这两种情况下,渲染器将诸如setState
之类的功能的实现“注入”到通用的React包中,以使组件更具声明性。
三、useEffect
3.1 例子
useEffect(
() => {
// 默认情况下,每次渲染后都会调用
console.log('render!');
// 在末尾处返回一个函数,等于实现 componentWillUnmount, React 在该组件卸载前调用该方法,也可返回一个箭头函数
return function cleanup() => {
console.log('unmounting...')
};
},
[deps]
);
3.2 capture value
- 在没有依赖的情况下会捕获初次进来时候那个状态值,今后会一直使用这个状态,哪怕外部状态已经发生变化。
- 所以当内部使用任何状态或变量,一定要添加依赖。
// 每500毫秒加一
useEffect(
() => {
const timer = setInterval(()=>{
console.log(count)
setCount(count+1)
},500);
return () => clearInterval(timer)
},
[count]
);
3.3 不允许在条件语句中添加
-
hooks有条规则是不要在循环、条件或嵌套函数中调用hook。
-
这是因为Hooks 的内部实现其实是链表。当调用
useState
的时候,我们将指针移到下一项。当我们退出组件的“调用树”时,会缓存该结果的列表直到下次渲染开始。// 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。 let memoizedState = []; // hooks 存放在这个数组 let cursor = 0; // 当前 memoizedState 下标 function useState(initialValue) { memoizedState[cursor] = memoizedState[cursor] || initialValue; const currentCursor = cursor; function setState(newState) { memoizedState[currentCursor] = newState; render(); } return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1 } // 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。 function useEffect(callback, depArray) { const hasNoDeps = !depArray; const deps = memoizedState[cursor]; const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true; if (hasNoDeps || hasChangedDeps) { callback(); memoizedState[cursor] = depArray; } cursor++; }
-
-
在后面也不行。当所有的hooks当作一个链表重组在一起时,如果有if语句,可能会让组件提前结束,hooks就添加不了在链表里面。所有的hooks 不允许在条件语句中,后添加。这样有让组件提前结束的风险,这样在状态变更的时候错乱。
例如:
if(count>5){
return null
}
useEffect(
() => {
console.log(count)
}
);
- 数组也许能更好解释其原理:
// 伪代码
let hooks, i;
function useState() {
i++;
if (hooks[i]) {
// 再次渲染时
return hooks[i];
}
// 第一次渲染
hooks.push(...);
}
// 准备渲染
i = -1;
hooks = fiber.hooks || [];
// 调用组件
YourComponent();
// 缓存 Hooks 的状态
fiber.hooks = hooks;
在处理指向一组数组的游标,如果更改渲染中调用的顺序,则游标将不会与数据匹配,那么use调用将不会指向正确的数据或处理程序。
3.4 useLayoutEffect
-
这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题
-
useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。
四、useRef
function TextInputWithFocusButton() {
const inputRef = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
用途:
- 挂载在某个组件或Dom上,然后他能拿到真实的DOM去绑定一些事件,但是它并没有覆盖所有的原生DOM事件,比如鼠标滚动之类,这些得通过DOM自己去绑定。
- 去破除useEffect的capture value造成的影响,相当于组件内部持久存在的一个常量,这个常量是一个object对象,他有个current值,这个current值是可以随时指定的,在组件任何其他地方都可以访问。
五、useMemo与useCallback
5.1 Memo
5.1.1 例子1
- 如果我们child渲染的是一个固定东西,每次父组件更新,子组件也要更新,我们需要优化的就是这样一个问题。
const Child =()=>{
console.log('object')
return(
<div>hello</div>
)
}
const Parent =() =>{
const[count,setCount]=useState(0)
return(
<div>
<div>count:{count}</div>
{/* eslint-disable-next-line react/button-has-type */}
<button onClick={()=>{
setCount(count+1)
}}> +1</button>
<Child/>
</div>
)
}
5.1.2 例子2
import React, { memo,useState } from 'react';
import './App.scss';
const Child =memo(()=>{
console.log('object')
return(
<div>hello</div>
)
})
const Parent =() =>{
const[count,setCount]=useState(0)
return(
<div>
<div>count:{count}</div>
{/* eslint-disable-next-line react/button-has-type */}
<button onClick={()=>{
setCount(count+1)
}}> +1</button>
<Child/>
</div>
)
}
5.1.3 例子3
import React, { memo,useState } from 'react';
import './App.scss';
const Child =memo(()=>{
const date=new Date();
return(
<div>time: {date.getHours()}:{date.getMinutes()}:{date.getSeconds()}</div>
)
},(prep,next) => {
return prev.count === next.count;
})
const Parent =() =>{
const[count,setCount]=useState(0)
const[clickTimeCount,setClickTimeCount]=useState(0);
return(
<div>
<div>count:{count}</div>
{/* eslint-disable-next-line react/button-has-type */}
<button onClick={()=>{
setCount(count+1)
}}> +1</button>
{/* eslint-disable-next-line react/button-has-type */}
<button onClick={()=>{
setClickTimeCount(clickTimeCount+1)
}}>GET CURRENT TIME</button>
<Child count={clickTimeCount}/>
</div>
)
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tz6bwMLh-1608541435755)(/Users/jinronghe/Desktop/截屏2020-12-13 下午2.49.38.png)]
- React.memo还有第二个参数,是个函数,两个参数分别接收的是前一次的props和后一次的props,如果返回false的话,函数会去更新,返回true的话就不需要更新,
5.2 useMemon
5.2.1 用法1
const Parent =() =>{
const[count,setCount]=useState(0)
const[clickTimeCount,setClickTimeCount]=useState(0);
const timeOption = useMemo(()=>{
return {
clickTimeCount
}
},[clickTimeCount])
return(
<div>
<div>count:{count}</div>
{/* eslint-disable-next-line react/button-has-type */}
<button onClick={()=>{
setCount(count+1)
}}> +1</button>
{/* eslint-disable-next-line react/button-has-type */}
<button onClick={()=>{
setClickTimeCount(clickTimeCount+1)
}}>GET CURRENT TIME</button>
<Child count={timeOption}/>
</div>
)
}
5.2.2 用法2
- usememo除了这个用法,他是用来缓存一些变量,比如timeoption。
- 这个变量在不需要变化的时候,我们直接读取缓存。
好处:
- 可以优化子组件。
- 是通过一些比较复杂的技术获取当前值的时候不需要在父组件每次更新的时候计算,只需要在某些依赖项变化的时候重新计算。
5.3 useCallback
5.3.1 例子
const Child =memo(props => {
console.log(props)
return(
<>
<input type="text" onChange={props.onChange}/>
</>
)
})
const Parent =() =>{
const[count,setCount]=useState(0)
const[text,setText]=useState("");
const handeleOnChange = e => {
setText(e.target.value)
}
return(
<div>
<div>count:{count}</div>
<div>text: {text}</div>
<button onClick={()=>{
setCount(count+1)
}}> +1</button>
<Child onChange ={handeleOnChange}/>
</div>
)
}
- useCallback的用法与usememo、useEffect用法基本是一样的,在函数上使用回调。他接收两个参数,一个是一个函数,第二个是个依赖数组.
const handeleOnChange =useCallback(e=> {
setText(e.target.value)
},[])
对付子组件重复更新问题,可以通过下面视角进行考虑。
- 通过react.memo避免没有必要的更新。
- 在用了react.memo之后,发现如handeleOnChange,或某个属性,在没有变化时还是更新,这是由于父组件的刷新导致拿到的值是重新计算的,并且把计算过的值重新给了child导致,这些单一的值或者单一的回调函数进行优化、缓存可以通过useCallback与useMemo。
六、自定义Hooks
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-azv1czku-1608541435757)(/Users/jinronghe/Desktop/截屏2020-12-20 上午12.44.44.png)]
const Parent =() =>{
const [width,setWidth]=useState('0px')
const [height,setHeight]=useState('0px')
useEffect(()=>{
setWidth(`${document.documentElement.clientWidth}px`)
setHeight(`${document.documentElement.clientHeight}px`)
},[])
useEffect(()=>{
const handleResize = () =>{
setWidth(`${document.documentElement.clientWidth}px`)
setHeight(`${document.documentElement.clientHeight}px`)
}
window.addEventListener('resize',handleResize,false);
return()=>{
window.removeEventListener('resize',handleResize,false);
}
},[])
return(
<div>
size:{width} * {height}
</div>
)
}
import {useState, useEffect} from 'react';
export const uesWindowSize = () => {
const [width,setWidth]=useState('0px')
const [height,setHeight]=useState('0px')
useEffect(()=>{
setWidth(`${document.documentElement.clientWidth}px`)
setHeight(`${document.documentElement.clientHeight}px`)
},[])
useEffect(()=>{
const handleResize = () =>{
setWidth(`${document.documentElement.clientWidth}px`)
setHeight(`${document.documentElement.clientHeight}px`)
}
window.addEventListener('resize',handleResize,false);
return()=>{
window.removeEventListener('resize',handleResize,false);
}
},[])
return [width,height];
}
const Parent =() =>{
const [width,height] = useWindowSize()
return(
<div>
size:{width} * {height}
</div>
)
}
参考资料
https://react.docschina.org/
https://overreacted.io/