16.8出的新特性。前言里我就写道,我们渲染页面为什么要生成实例?或者说我们渲染页面的话生成实例是否必要?
在没有框架的时代,我们用js渲染个页面很简单啊,写个函数,然后函数里面生成个dom,往dom上加想要的东西,放到需要的位置,然后把这个函数执行就行了。里面有用实例吗?没有。
那么为什么会产生实例来渲染页面?
实例有什么特点? 实例是类的具现化产物,本质是对象,上面有一些属性而已。
那么如果我们渲染个有状态的页面,比如一个组件是显示状态还是不显示状态,这个状态提供给别的组件使用。这时候按以前写的话很可能要在全局做个属性,一个组件显示不显示去改变这个属性,另一个组件去读这个属性。
当时es6的class才出来,大家想着用起来,由于类的实例对于状态管理很方便,于是就有了类组件。像上一条那种情况,状态写在实例里就不会污染全局,而且便于管理,每个实例都是一个自己的空间。再加上完善的生命周期,看起来好像很不错。
但是后来,大家发现,渲染一个页面其实没必要弄一个实例出来,需要状态的话,闭包也是可以的。如果使用闭包,那么函数组件就能拥有状态,不然状态无法一直存在。我们渲染一个页面,使用实例就会耗性能,因为实例上除了状态还会带出别的东西,比如它的生成类,以及生成类的原型等等。但是通过闭包来解决这些问题就比较巧妙,函数组件可以有需要就渲染,用到什么状态什么生命周期就把什么放闭包里,这样更加灵活。其实闭包和实例本质上说并没太大区别,都是把很多个属性封装在某个地方,只是实例相对于闭包来说功能更多,更完整。当然,更多更完整并不意味着好用,反而可能意味着更难去操纵和管理。
hooks主要就是为了解决函数组件的状态而存在的。react从用fiber开始,就把每个组件变成一个fiber,而他们的状态就挂到hooks上,而hooks挂到fiber上。hooks是一个链表结构,每个节点都是用户写在组件的hook。实际usestate之类的状态使用闭包存起来,而说到闭包,就想到redux。没错,useState就是useReducer的语法糖,它最终会返回useReducer。所以react给每个组件的状态管理机,就很像是为每个组件创造了单独的store,并把修改store里属性的方法发出去。这里就不展开说了。
除了解决状态问题,hooks还有其他优势,比如写类组件实际我们写的是类,并不是实例,在调用的时候并不是用户在调用,所以用户根本不清楚this到底是谁,导致混乱。
另外就是它能很好的解耦。在写实例时候,如果有多个状态或者副作用,那么需要配合实例生命周期来写,导致状态组件耦合一起,但如果用hooks,可以很好的解耦,相同的依赖可以写在一起。
差不多就写这些,下面学习用法:
import React from 'react';
import {
useState } from 'react';
function App(props) {
return (
<div>
<Counter1></Counter1>
<Counter2></Counter2>
</div>
)
}
export default App;
class Counter1 extends React.Component {
state = {
number: 0 }
render() {
return (
<div>
{
this.state.number}
<button onClick={
() => this.setState({
number: this.state.number + 1 })}> ++</button>
</div>
)
}
}
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
return (
<div>
<div>
{
state.number}
<button onClick={
() => setState({
number: state.number + 1 })}> ++</button>
</div>
</div>
)
有些细节需要注意:
1、setState里面的对象会覆盖以前的state。比如初始值除了number还设了个name,但是setState的对象里不传name,就会没了。
2、异步逻辑需要注意状态。比如我在组件里使用setTimeout延迟+1,期间我又同步加一几次,但是异步时间到后触发时拿到的state是我还没同步加一的state。最终state又会改为异步执行后的结果。而不是加上同步执行后的结果。这个问题有2种解决方法。
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
let add = () => {
setState({
number: state.number + 1 })
}
let asyncAdd = () => {
setTimeout(() => {
setState((lastState) => ({
number: lastState.number + 1 }))
}, 4000);
}
return (
<div>
<div>
{
state.number}
<button onClick={
add}> ++</button>
<button onClick={
asyncAdd}>async</button>
</div>
</div>
)
}
传入的函数能接收到最新的state,使得其能拿到最新的值。
第二种方法 useRef解决:
function Counter2(props) {
let refNumber = useRef()
let [state, setState] = useState({
number: 0 })
let add = () => {
setState({
number: state.number + 1 })
refNumber.current = state.number + 1
}
let asyncAdd = () => {
setTimeout(() => {
setState({
number: refNumber.current + 1 })
}, 4000);
}
return (
<div>
<div>
{
state.number}
<button onClick={
add}> ++</button>
<button onClick={
asyncAdd}>async</button>
</div>
</div>
)
}
3、useState可以传入函数进行惰性初始化。
4、setState传和之前相同的对象,则组件不会更新。注意,这里说的同一个是指直接拿这个state传给它,而不是声明一个新变量或者解构后的值和原来相同。
5、react需要保证每次渲染钩子数量一样,所以钩子只能放函数里的外层。不能放到条件或者内嵌函数里面。
let previousFunc
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
let add = useCallback(() => {
setState({
number: state.number + 1 })
console.log('funcgo');
}, [])
console.log(previousFunc === add);
previousFunc = add
return (
<div>
<div>
{
state.number}
<button onClick={
add}> ++</button>
</div>
</div>
)
}
let add = useCallback(() => {
setState({
number: state.number + 1 })
console.log('funcgo');
}, [state.number])
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
let [state2, setState2] = useState({
number: 0 })
let add = useCallback(() => {
setState({
number: state.number + 1 })
}, [state.number])
let add2 = useCallback(() => {
setState2({
number: state2.number + 1 })
}, [state2.number])
let data = {
number: state.number + 1 }//模仿需要父组件数据进行处理
return (
<div>
<div>
{
state.number}
<button onClick={
add}> ++</button>
</div>
<div>
{
state2.number}
<button onClick={
add2}> ++22++</button>
</div>
<Child onButtonClick={
add} data={
data}></Child>
</div>
)
}
function Child(props) {
const {
onButtonClick, data } = props
console.log('child');
return (
<div>
<div>
{
data.number}
<button onClick={
onButtonClick}> ++</button>
</div>
</div>
)
}
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
let [state2, setState2] = useState({
number: 0 })
let add = useCallback(() => {
setState({
number: state.number + 1 })
}, [state.number])
let add2 = useCallback(() => {
setState2({
number: state2.number + 1 })
}, [state2.number])
//let data = { number: state.number + 1 }//模仿需要父组件数据进行处理
let data = useMemo(() => ({
number: state.number + 1 }), [state])
return (
<div>
<div>
{
state.number}
<button onClick={
add}> ++</button>
</div>
<div>
{
state2.number}
<button onClick={
add2}> ++22++</button>
</div>
<Memochild onButtonClick={
add} data={
data}></Memochild>
</div>
)
}
function Child(props) {
const {
onButtonClick, data } = props
console.log('child');
return (
<div>
<div>
{
data.number}
<button onClick={
onButtonClick}> ++</button>
</div>
</div>
)
}
let Memochild = memo(Child)
let data = useCallback(({
number: state.number + 1 }), [state])
const [state, dispatch] = useReducer(reducer, initialArg, init);
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return {
number: state.number + 1 }
case 'MINUS':
return {
number: state.number - 1 }
}
}
function initialFunc(initialArg) {
return {
number: initialArg }
}
function Counter2(props) {
let [state, dispatch] = useReducer(reducer, 0, initialFunc)
return (
<div>
<div>
{
state.number}
<button onClick={
() => dispatch({
type: 'ADD' })}> ++</button>
<button onClick={
() => dispatch({
type: 'MINUS' })}> --</button>
</div>
</div>
)
}
let Context = React.createContext()
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
return (
<Context.Provider value={
{
state, setState }}>
<Child></Child>
</Context.Provider>
)
}
function Child(props) {
let {
state, setState } = useContext(Context)
return (
<div>
{
state.number}
<button onClick={
() => setState({
number: state.number + 1 })}> ++</button>
</div>
)
}
class Counter1 extends React.Component {
state = {
number: 0 }
componentDidMount() {
document.title = this.state.number + ''
}
componentDidUpdate() {
document.title = this.state.number + ''
}
render() {
return (
<div>
{
this.state.number}
<button onClick={
() => this.setState({
number: this.state.number + 1 })}> ++</button>
</div>
)
}
}
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
useEffect(() => {
document.title = state.number + ''
})
return (
<div>
<p>
{
state.number}
</p>
<button onClick={
() => setState({
number: state.number + 1 })}>++++</button>
</div>
)
}
useEffect(() => {
if (!mount) {
mount = true
} else {
document.title = state.number + ''
}
})
function Counter2(props) {
let [state, setState] = useState({
number: 0 })
useEffect(() => {
let timer = setInterval(() => {
setState(prev => ({
number: prev.number + 1 }))
}, 1000);
return () => {
clearInterval(timer) }
})
return (
<div>
<p>
{
state.number}
</p>
<button onClick={
() => setState({
number: state.number + 1 })}>++++</button>
</div>
)
}
function Counter2(props) {
let inputRef = useRef()
let getFocus = () => {
inputRef.current.focus()
}
return (
<div>
<p>
<input type="text" ref={
inputRef}></input>
</p>
<button onClick={
getFocus}>++焦点++</button>
</div>
)
}
function Counter2(props) {
let inputRef = useRef()
let getFocus = () => {
inputRef.current.focus()
}
return (
<div><Child ref22={
inputRef}></Child>
<button onClick={
getFocus}>++焦点++</button>
</div>
)
}
function Child(props) {
return (
<div>
<p>
<input type="text" ref={
props.ref22}></input>
</p>
</div>
)
}
function Counter2(props) {
let inputRef = useRef()
let getFocus = () => {
inputRef.current.focus()
}
return (
<div><Forwarded ref={
inputRef}></Forwarded>
<button onClick={
getFocus}>++焦点++</button>
</div>
)
}
function Child(props, ref) {
return (
<div>
<p>
<input type="text" ref={
ref}></input>
</p>
</div>
)
}
let Forwarded = forwardRef(Child)
function Counter2(props) {
let inputRef = useRef()
let getFocus = () => {
inputRef.current.focus()//方法名对应handle的对象里方法
inputRef.current.value = 'yyyy'//由于ref拿到的不是dom所以没有value就不会成功,优点可以不报错。
}
return (
<div><Forwarded ref={
inputRef}></Forwarded>
<button onClick={
getFocus}>++焦点++</button>
</div>
)
}
function Child(props, ref) {
let newRef = useRef()
useImperativeHandle(ref, () => ({
focus() {
//父组件调用方法名,自己取
newRef.current.focus()//实际操作的方法
}
}))
return (
<div>
<p>
<input type="text" ref={
newRef}></input>
</p>
</div>
)
}
let Forwarded = forwardRef(Child)
function Counter2(props) {
let [color, setState] = useState('red')
useEffect(() => {
alert('useEffect' + color)
})
useLayoutEffect(() => {
alert('useLayoutEffect' + color);
})
return (
<div>
<div style={
{
backgroundColor: color }}>xxxxxxxxxxxxxxxx </div>
<button onClick={
() => setState('yellow')}>黄</button>
<button onClick={
() => setState('green')}>绿</button>
</div>
)
}
function useCounter() {
let [number, setNumber] = useState(0)
useEffect(() => {
setInterval(() => {
setNumber(prev => prev + Math.random())
}, 1000);
}, [])
return number
}
function Counter1(props) {
let number = useCounter()
return (
<div>
{
number}
</div>
)
}
function Counter2(props) {
let number = useCounter()
return (
<div>
{
number}
</div>
)
}
注意,这个自定义hook不用use开头会报错
这个自定义hooks可以用来包装useReducer做成logger中间件:
function useLogger(reducer, initialState, init) {
const [state, dispatch] = useReducer(reducer, initialState, init);
let dispatchWithLogger = (action) => {
console.log('old state', state);
dispatch(action);
}
useEffect(function () {
console.log('new state', state);
}, [state]);
return [state, dispatchWithLogger];
}
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return {
number: state.number + 1 }
case 'MINUS':
return {
number: state.number - 1 }
}
}
function usePromise() {
let [state, dispatch] = useReducer(reducer, {
number: 0 })
let mydispatch = (action) => {
action.then(res => {
dispatch(res)
})
}
return [state, mydispatch]
}
function Counter2(props) {
let [state, dispatch] = usePromise()
let promiseadd = () => {
dispatch(new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
type: 'ADD' })
}, 1000);
}))
}
return (
<div>
{
state.number}
<button onClick={
promiseadd}>promise+</button>
</div>
)
function useThunk() {
let [state, dispatch] = useReducer(reducer, {
number: 0 })
let mydispatch = (action) => {
if (typeof action === 'function') {
action(dispatch, () => state)
} else {
dispatch(action)
}
}
return [state, mydispatch]
}
function Counter2(props) {
let [state, dispatch] = useThunk()
let thunkadd = () => {
dispatch(function (thunkdispatch, getstate) {
setTimeout(() => {
thunkdispatch({
type: 'ADD' })
}, 1000);
})
}
return (
<div>
{
state.number}
<button onClick={
thunkadd}>thunkadd+</button>
</div>
)
}
function Counter1(props) {
const [users, loadMore] = useRequest('http://localhost:8000/api/users');
if (users === null) {
return <div>加载中....</div>
}
return (
<>
<ul>
{
users.map((item, index) => <li key={
index}>{
item.id}:{
item.name}</li>)
}
</ul>
<button onClick={
loadMore}>加载更多</button>
</>
)
}
function useRequest(url) {
let limit = 5
let [offset, setOffset] = useState(0)//偏移,从第几个开始加载
let [loading, setloading] = useState(false)//用来控制useEffect调用,保证点击按钮后执行。
let [data, setData] = useState([]);//数据存放
useEffect(() => {
async function reqfunc() {
setData(null)//用来显示加载中
let newData = await fetch(`${
url}?offset=${
offset}&limit=${
limit}`).then(res => res.json())
setData([...data, ...newData])//合并
setOffset(offset + newData.length)
}
reqfunc()
}, [loading])
function loadMore() {
setloading(!loading)
}
return [data, loadMore];
}
这里有争议的就是setData(null)之后为啥还能解构它。其实就是setData后是不能马上拿到新状态,拿的还是老状态,所以前面自定义hook第一个logger例子里马上取就取不到,通过useEffect才拿到修改后的状态。
还可以动态修改类名做成动画:
先写个css:
.circle{
width:50px;
height: 50px;
border-radius: 50%;
background-color: red;
transition: all 1s;
}
.circle-bigger{
width: 200px;
height: 200px;
}
.circle-smaller{
width: 10px;
height: 10px;
}
function useAnimation(initialClassName) {
const [className, setClassName] = useState(initialClassName);//修改类名
function bigger() {
setClassName(`${
initialClassName} ${
initialClassName}-bigger`);
}
function smaller() {
setClassName(`${
initialClassName} ${
initialClassName}-smaller`)
}
return [className, bigger, smaller];
}
function Counter1(props) {
const [className, bigger, smaller] = useAnimation('circle');
return (
<div>
<button onClick={
bigger}>bigger</button>
<button onClick={
smaller}>smaller</button>
<div className={
className}></div>
</div>
)
}