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>
  )
}
image.png

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

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


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


image.png

这是由于每次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就做出更新。


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 = 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 (
    <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>
  )
}

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


image.png

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

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

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

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

image.png

再次点击m+1就会报错


image.png

我们再次将newUseState 换成 React.useState

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,互不干扰


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
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容