首先创建一个App组件,加入一个按钮和点击后显示的值num,在按钮上绑定click事件,每次点击,num++
function App() {
console.log('---app run again----')
const [num, setNum] = useState(0)
console.log('---render----')
console.log(`num:${num}`)
return (
{num}
)
}
在首次渲染的时候调用App() ---> 运行render() ---> 生成虚拟dom ---> 作用于真实dom
用户点击button ---> 调用App() --->调用setNum(n+1) ---> 运行render() ---> dom diff ---> 作用于真实dom
每次调用App的时候,useState都会执行。
state是异步的
在控制台中,我们可以看到,打印的num并不是页面上显示的结果,这是因为react中state的更新是异步的。当我们setState后,react并不会立即将值做出改变,而是将其暂时放入pedding队列中。react会合并多个state,然后只render 一次。
useState 实现
const newUseState = intialValue => {
let state = intialValue
console.log('newUseState run...')
const setState = newValue => {
state = newValue
reRender()
}
return [state, setState]
}
const reRender = () => {
ReactDOM.render( , document.getElementById('root'))
}
此时我们,我们在App中使用newUseState
console.log('---app run again----')
const [num, setNum] = newUseState(0)
console.log('---render----')
console.log(`num:${num}`)
但是,发现什么用都没有,num 一直是0
这是由于每次App()调用后,num就被初始化为0,如果不想每次调用App后被初始化,可以在newUseState外边定义一个临时变量来存放set之后的值.
let _state = null
const newUseState = intialValue => {
_state = _state === null ? intialValue : _state
console.log('newUseState run...')
const setState = newValue => {
_state = newValue
reRender()
}
return [_state, setState]
}
此时,点击+1后,num就做出更新。
如果有两个 newUseState
const [num, setNum] = newUseState(0)
const [m, setM] = newUseState(20)
此时外部变量_state 存放的num,会被后面的maxNum覆盖,变为 20.
改变newUseState类型
1. 使_state为对象
let _state = { num:0, m:20 }
但是使用newUseState(0)的时候,我们无法知道赋值给的是num还是maxNum
2. 使_state为数组
let _state = [0, 20]
此时,我们的newUseState也要进行修改
let _state = []
let index = 0
const newUseState = intialValue => {
const currentIndex = index
_state[currentIndex] =
_state[currentIndex] === undefined ? intialValue : _state[currentIndex]
console.log('newUseState run...')
const setState = newValue => {
_state[currentIndex] = newValue
console.log('---after-set----')
console.log(_state)
reRender()
}
index++
return [_state[currentIndex], setState]
}
我们把m也放到页面上
function App() {
console.log('---app run again----')
const [num, setNum] = newUseState(0)
const [m, setM] = newUseState(20)
console.log('---render----')
console.log(`num:${num}`)
return (
{num}
{m}
)
}
但是,此时点击按钮不生效
是由于每次render运行的时候,index还保存着上次的值,导致数组变长。应该在render函数触发前将index的值变为0.
const reRender = () => {
index = 0
ReactDOM.render( , document.getElementById('root'))
}
此时,达到了我们想要的效果。
newUseState 使用数组的'缺陷'
之前,我们使用数组和外部变量index,实现了多个newUseState,使得组件中能够使用多个state。但是实际上还有一些不是那么方便的地方。
1. 只能按顺序调用
在第一次渲染的时候,第一个值是num,第二个值是m,那么当App()再次被调用的时候,下一次,还得保持这个顺序,否则就会出错。先把之前App的代码微做修改
function App() {
console.log('---app run again----')
const [num, setNum] = newUseState(0)
let m, setM
if (num % 2 === 0) {
;[m, setM] = newUseState(20)
}
console.log('---render----')
console.log(`num:${num}`)
console.log(`m:${m}`)
return (
{num}
{m}
)
}
初始化的时候,m就为undefined
再次点击m+1就会报错
我们再次将newUseState 换成 React.useState
此时编辑器就会提示useState被有条件的调用,hooks必须按照完全一样的顺序渲染。
2.App使用了useState,其他组件用什么
react为每个组件创建了memorisedState和index,并且将其放在对应的虚拟dom上,这样,假如App()有m,Example()也可以拥有m,不会重复。
1.创建Example组件,包含和App同样的m
function Example() {
const [num, setNum] = useState(0)
return (
<>
examples: {num}
>
)
}
2.修改App的return
return (
{num}
{m}
)
我们点击各自的num+1,互不干扰
useState的set方法每次set的都是不同的值(相当于set的分身)
我们创建一个+1 button还有一个log button
function App() {
const [num, setNum] = useState(0)
const log = () =>
setTimeout(() => {
console.log(`num:${num}`)
}, 2000)
return (
{num}
)
}
当我们先点+1,然后再点log,此时num进行了+1操作,2秒后打出的num=1也是预期的结果
但是当我们先点击log,由于是延时2秒触发,我们点下2次+1,此时打出的num竟然是0
这是由于当num=0时,我们触发了log,但是它两秒后执行log(num=0).当我们先点+1,然后在点log时,我们两秒后触发的是log(num=1).set操作的相当于每次都是一个副本。
解决方法1
1.使用useRef贯穿整个周期
function App() {
const numRef = useRef(0)
const log = () =>
setTimeout(() => {
console.log(`num:${numRef.current}`)
}, 2000)
return (
{numRef.current}
)
}
此时无论先点log还是先点+1,都能得到我们预期的结果。但是此时,页面上的num仍然是0,因为useRef不会触发render函数。react更倾向于函数式,它希望每次操作的并不是同一个值,这点有别于vue。
2.强制更新
function App() {
const numRef = useRef(0)
const log = () =>
setTimeout(() => {
console.log(`num:${numRef.current}`)
}, 2000)
const forceUpdate = useState(null)[1]
return (
{numRef.current}
)
}
我们创建一个forceUpdate方法,让其一开始传入为null,之后每次点击传入numRef.current,这时就可以强制render,达到我们的预期效果
之前,我们使用useRef创建了一个贯穿App组件的变量,并且通过创建一个无用的state,来达到强制更新组件的目的。但是这样做,并不是很好。
解决方法2:使用useContext创建贯穿不同组件的变量
首先创建两个子组件ChildA和ChildB
function ChildA() {
const { setTheme } = React.useContext(themeContext);
return (
);
}
function ChildB() {
const { setTheme } = React.useContext(themeContext);
return (
);
}
改造下chilA和childB的父组件App
function App() {
const [theme, setTheme] = React.useState("red");
return (
{theme}
);
}
增加两个css类
.red button {
background: red;
color: white;
width: 100px;
line-height: 40px;
height: 40px;
border-radius: 4px;
}
.blue button {
background: blue;
color: white;
width: 100px;
line-height: 40px;
height: 40px;
border-radius: 4px;
}
此时变成这样。接下来创建App的context,来传递给子组件ChildA和ChildB.
const themeContext = React.createContext(null);
function App() {
const [theme, setTheme] = React.useState("red");
return (
{theme}
);
}
function ChildA() {
const { setTheme } = React.useContext(themeContext)
return (
)
}
function ChildB() {
const { setTheme } = React.useContext(themeContext)
return (
)
}
此时,ChildA和ChildB中操作的theme都是通过Context传过来的,也就是它们修改的都是同一个值。
此时点击后也就能生效了。