纵有疾风起
人生不言弃

useState原理

首先创建一个App组件,加入一个按钮和点击后显示的值num,在按钮上绑定click事件,每次点击,num++

function App() {  console.log('---app run again----')  const [num, setNum] = useState(0)  console.log('---render----')  console.log(`num:${num}`)  return (    <div className='App'>      <p>{num}</p>      <p>        <button          onClick={() => {            setNum(num + 1)            console.log(num)          }}        >          +1        </button>      </p>    </div>  )}
useState原理插图
image.png

在首次渲染的时候调用App() —> 运行render() —> 生成虚拟dom —> 作用于真实dom<br />用户点击button —> 调用App() —>调用setNum(n+1) —> 运行render() —> dom diff —> 作用于真实dom

每次调用App的时候,useState都会执行。<br />

useState原理插图1
image.png

state是异步的

在控制台中,我们可以看到,打印的num并不是页面上显示的结果,这是因为react中state的更新是异步的。当我们setState后,react并不会立即将值做出改变,而是将其暂时放入pedding队列中。react会合并多个state,然后只render 一次。

<a name=”4fH49″></a>

useState 实现

const newUseState = intialValue => {  let state = intialValue  console.log('newUseState run...')  const setState = newValue => {    state = newValue    reRender()  }  return [state, setState]}const reRender = () => {  ReactDOM.render(<App />, document.getElementById('root'))}

此时我们,我们在App中使用newUseState

  console.log('---app run again----')  const [num, setNum] = newUseState(0)  console.log('---render----')  console.log(`num:${num}`)

但是,发现什么用都没有,num 一直是0

useState原理插图2
image.png

这是由于每次App()调用后,num就被初始化为0,如果不想每次调用App后被初始化,可以在newUseState外边定义一个临时变量来存放set之后的值.

let _state = nullconst newUseState = intialValue => {  _state = _state === null ? intialValue : _state  console.log('newUseState run...')  const setState = newValue => {    _state = newValue    reRender()  }  return [_state, setState]}

此时,点击+1后,num就做出更新。

useState原理插图3
image.png

如果有两个 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 = 0const 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 (    <div className='App'>      <p>{num}</p>      <p>        <button          onClick={() => {            setNum(num + 1)            console.log(`num++`)            console.log(num)          }}        >          num+1        </button>      </p>      <p>{m}</p>      <p>        <button          onClick={() => {            setM(m + 1)            console.log(`m++`)            console.log(m)          }}        >          m+1        </button>      </p>    </div>  )}

但是,此时点击按钮不生效

useState原理插图4
image.png

是由于每次render运行的时候,index还保存着上次的值,导致数组变长。应该在render函数触发前将index的值变为0.

const reRender = () => {  index = 0  ReactDOM.render(<App />, document.getElementById('root'))}

此时,达到了我们想要的效果。

useState原理插图5
image.png

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 (    <div className='App'>      <p>{num}</p>      <p>        <button          onClick={() => {            setNum(num + 1)            console.log(`num++`)            console.log(num)          }}        >          num+1        </button>      </p>      <p>{m}</p>      <p>        <button          onClick={() => {            setM(m + 1)            console.log(`m++`)            console.log(m)          }}        >          m+1        </button>      </p>    </div>  )}

初始化的时候,m就为undefined

useState原理插图6
image.png

再次点击m+1就会报错

useState原理插图7
image.png

我们再次将newUseState 换成 React.useState

useState原理插图8
image.png

此时编辑器就会提示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 (    <>      <p>examples: {num}</p>      <p>        <button          onClick={() => {            setNum(num + 1)            console.log(`num++`)            console.log(num)          }}        >          example num+1        </button>      </p>    </>  )}

2.修改App的return

return (    <div className='App'>      <p>{num}</p>      <p>        <button          onClick={() => {            setNum(num + 1)            console.log(`num++`)            console.log(num)          }}        >          num+1        </button>      </p>      <p>{m}</p>      <p>        <button          onClick={() => {            setM(m + 1)            console.log(`m++`)            console.log(m)          }}        >          m+1        </button>      </p>      <div>        <Example />      </div>    </div>  )

我们点击各自的num+1,互不干扰

useState原理插图9
image.png

useState的set方法每次set的都是不同的值(相当于set的分身)

我们创建一个+1 button还有一个log button

function App() {  const [num, setNum] = useState(0)  const log = () =>    setTimeout(() => {      console.log(`num:${num}`)    }, 2000)  return (    <div className='App'>      <p>{num}</p>      <p>        <button          onClick={() => {            setNum(num + 1)          }}        >          +1        </button>        <button onClick={log}>log now</button>      </p>    </div>  )}

当我们先点+1,然后再点log,此时num进行了+1操作,2秒后打出的num=1也是预期的结果

image.png

image.png

但是当我们先点击log,由于是延时2秒触发,我们点下2次+1,此时打出的num竟然是0

image.png

image.png

这是由于当num=0时,我们触发了log,但是它两秒后执行log(num=0).当我们先点+1,然后在点log时,我们两秒后触发的是log(num=1).set操作的相当于每次都是一个副本。

image.png

image.png

解决方法1

1.使用useRef贯穿整个周期
function App() {  const numRef = useRef(0)  const log = () =>    setTimeout(() => {      console.log(`num:${numRef.current}`)    }, 2000)  return (    <div className='App'>      <p>{numRef.current}</p>      <p>        <button          onClick={() => {            numRef.current++          }}        >          +1        </button>        <button onClick={log}>log now</button>      </p>    </div>  )}
image.png

image.png

此时无论先点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 (    <div className='App'>      <p>{numRef.current}</p>      <p>        <button          onClick={() => {            numRef.current++            forceUpdate(numRef.current)          }}        >          +1        </button>        <button onClick={log}>log now</button>      </p>    </div>  )}

我们创建一个forceUpdate方法,让其一开始传入为null,之后每次点击传入numRef.current,这时就可以强制render,达到我们的预期效果

image.png

image.png

之前,我们使用useRef创建了一个贯穿App组件的变量,并且通过创建一个无用的state,来达到强制更新组件的目的。但是这样做,并不是很好。

解决方法2:使用useContext创建贯穿不同组件的变量

首先创建两个子组件ChildA和ChildB

function ChildA() {  const { setTheme } = React.useContext(themeContext);  return (    <div>      <button onClick={() => setTheme("red")}>red</button>    </div>  );}function ChildB() {  const { setTheme } = React.useContext(themeContext);  return (    <div>      <button onClick={() => setTheme("blue")}>blue</button>    </div>  );}

改造下chilA和childB的父组件App

function App() {  const [theme, setTheme] = React.useState("red");  return (      <div className={`App ${theme}`}>        <p>{theme}</p>        <div>          <ChildA />        </div>        <div>          <ChildB />        </div>      </div>  );}

增加两个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;}
image.png

image.png

此时变成这样。接下来创建App的context,来传递给子组件ChildA和ChildB.

const themeContext = React.createContext(null);function App() {  const [theme, setTheme] = React.useState("red");  return (    <themeContext.Provider value={{ theme, setTheme }}>      <div className={`App ${theme}`}>        <p>{theme}</p>        <div>          <ChildA />        </div>        <div>          <ChildB />        </div>      </div>    </themeContext.Provider>  );}function ChildA() {  const { setTheme } = React.useContext(themeContext)  return (    <div>      <button        onClick={() =>          setTimeout(() => {            setTheme('red')          }, 2000)        }      >        red      </button>    </div>  )}function ChildB() {  const { setTheme } = React.useContext(themeContext)  return (    <div>      <button        onClick={() =>          setTimeout(() => {            setTheme('blue')          }, 2000)        }      >        blue      </button>    </div>  )}

此时,ChildA和ChildB中操作的theme都是通过Context传过来的,也就是它们修改的都是同一个值。

image.png

image.png

此时点击后也就能生效了。

image.png

文章转载于:https://www.jianshu.com/p/070d13814525

原著是一个有趣的人,若有侵权,请通知删除

未经允许不得转载:起风网 » useState原理
分享到: 生成海报

评论 抢沙发

评论前必须登录!

立即登录