一、Hooks介绍和使用
1.基本认识
class组件的缺点:
- 相同业务逻辑分散到各个方法中,逻辑混乱(比如说在didmount的时候发送ajax获取数据,有可能在didupdate更新的时候还需要发送ajax获取数据,同样获取数据分散到didmount和didupdate两个方法中)
- 复用组件逻辑复杂(HOC、Render Prop)
函数组件没有组件实例,没有生命周期,没有state和setState,只能接受props,为了增强功能需要Hooks
2.Hooks规范
命名规范:
- 所有Hooks都用use开头,如useXxx
- 自定义Hook也要以use开头
- 非Hooks的地方,尽量不要用useXxx写法
使用规范:
- 用于React的函数组件和自定义hook
- 只能用于顶层代码,不能用于循环、判断条件中(可以通过eslint-plugin-react-hooks规范代码,create-react-app脚手架自身带有这个插件)
函数组件(纯函数),执行完就销毁,所以无论是初始化(render)还是组件更新(re-render),都会重新执行一次函数,获取最新的组件
render:初始化state的值,添加effect函数(useEffect)
re-render:恢复初始化的state的值,但不会重新设置新的值,要修改只能通过setState修改;替换effect函数(useEffect),effect内部的函数也会重新定义
以useState为例子,如果有多个useState是按顺序读取的,所以不能把useState放到条件判断中,不然如果条件判断不通过,useState读取的顺序会混乱,其他Hooks也是按顺序执行的
class组件有组件实例,执行完也不会销毁
二、State Hook(useState)
默认函数组件没有state,是一个纯函数,执行完即销毁,不能存储state,需要State Hook
1、基本用法
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
console.log('App运行')
const [n, setN] = React.useState(0);
console.log(`n:${n}`)
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
-
首次渲染,调用App函数,得到虚拟DOM,在页面创建真实DOM(调用App函数会运行useState(0))
-
用户点击button,调用setN,再次调用App函数
-
调用App函数,得到虚拟DOM,通过DOM Diff,更新真实DOM(调用App函数会运行useState(0))
-
执行setN的时候,n不会马上变,会把n+1存入到数据x,并且会导致App函数重新执行,每次useState(0)的时候,从数据x读取n的最新值
-
每个组件都会有自己的数据x,可以认定为state
2、实现一个简单的useState
2.1原始的myState
import React from 'react';
import ReactDOM from 'react-dom';
const root = document.getElementById('root')
let _state
const myState = (initialValue)=>{
_state = _state === undefined?initialValue:_state
const setState = newValue =>{
_state = newValue
render()
}
return [_state,setState]
}
const render = ()=>{
ReactDOM.render(<App/>,root)
}
const App = props =>{
console.log('App运行了');
const [n,setN] = myState(0)
console.log(`n,${n}`)
const addN = ()=>{
setN(n+1)
}
return(
<div>{n}
<button onClick={addN}>+1</button>
</div>)
}
export default App;
- 存在的问题:如果一个组件里面用了两个myState,则_state会冲突
- 将_state设置为数组
2.2改进版的myState
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state = [];
let index = 0;
function myUseState(initialValue) {
const currentIndex = index;//引入currentIndex,这样一来对index的+1操作不会影响后面的操作
index += 1;
_state[currentIndex] = _state[currentIndex] || initialValue;
const setState = (newState) => {
_state[currentIndex] = newState;
render();
};
return [_state[currentIndex], setState];
}
const render = () => {
index = 0;//App不断被调用,这样index会一直+1,要在改变数据更新UI前把index重置,再重新调用App
ReactDOM.render(<App />, rootElement);
};
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
return (
<div className="App">
<p>m:{m}</p>
<p>
<button onClick={() => setM(m + 1)}>+1</button>
</p>
<p>n:{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
-
useState不要写在写在循环和条件中
-
改进之处:还要给每个组件创建_state和index,并且还得放在组件对应的虚拟节点上
3、注意事项
3.1不可局部更新,要用...操作符或者Object.asign来实现合并
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
setUser({
...user, //...操作符实现合并
name: 'Jack'
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
3.2setState推荐用函数
如果要对setState进行多种操作,推荐写成函数
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0)
const onClick = ()=>{
setN(n+1)
setN(n+1) // 会发现 n 不能加 2
// setN(i=>i+1) //推荐写成函数形式
// setN(i=>i+1)
}
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+2</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
三、useRef
作用:
-
是获取DOM节点
-
如果需要一个值在render的时候保持不变,可以使用useRef
//初始化:
const count = useRef(0)
//读取:
count.current
注意:count.current值变化,不会自动render(Vue3可以实现ref变化自动render)
解决:监听ref,当ref.current变化的时候调用setX
一个例子:
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const [n, setN] = React.useState(0);
const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
- 点击+1再点log,不存在bug
- 如果点log再点+1,则打印n的值为旧值0
原因:
点击log的时候会三秒后打印n的值,此时n的值为0,然后再点+1,会产生新的值n+1
如果想要一个贯穿始终的状态,可以用useRef
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0); //{current:0},本身变化并不能触发更新
const update = React.useState()[1];//通过设置update来更新,update相当于setState
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={() => ((nRef.current += 1), update(nRef.current))}>
+1
</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
四、useContext
作用:父子组件间通信,可以跨层传递值
import React, { useContext } from 'react'
// 主题颜色
const themes = {
light: {
foreground: '#000',
background: '#eee'
},
dark: {
foreground: '#fff',
background: '#222'
}
}
// 创建 Context
const ThemeContext = React.createContext(themes.light) // 初始值
function ThemeButton() {
//子组件通过useContext(xxx)来读取
const theme = useContext(ThemeContext)
return <button style={{ background: theme.background, color: theme.foreground }}>
hello world
</button>
}
function Toolbar() {
return <div>
<ThemeButton></ThemeButton>
</div>
}
function App() {
//父组件用xxx.Provider包裹起来
return <ThemeContext.Provider value={themes.dark}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
}
export default App