網(wǎng)上各種言論說(shuō) React 上手比 Vue 難,可能難就難不能深刻理解 JSX,或者對(duì) ES6 的一些特性理解得不夠深刻,導(dǎo)致覺(jué)得有些點(diǎn)難以理解,然后說(shuō) React 比較難上手,還反人類(lèi)啥的,所以我打算寫(xiě)兩篇文章來(lái)講新手學(xué)習(xí) React 的時(shí)候容易迷惑的點(diǎn)寫(xiě)出來(lái)。
為什么要引入 React
在寫(xiě) React 的時(shí)候,你可能會(huì)寫(xiě)類(lèi)似這樣的代碼:
import React from 'react'
function A() {
// ...other code
return <h1>前端桃園</h1>
}
你肯定疑惑過(guò),下面的代碼都沒(méi)有用到 React,為什么要引入 React 呢?
如果你把 import React from ‘react’
刪掉,還會(huì)報(bào)下面這樣的錯(cuò)誤:
那么究竟是哪里用到了這個(gè) React,導(dǎo)致我們引入 React 會(huì)報(bào)錯(cuò)呢,不懂這個(gè)原因,那么就是 JSX 沒(méi)有搞得太明白。
你可以講上面的代碼(忽略導(dǎo)入語(yǔ)句)放到在線 babel 里進(jìn)行轉(zhuǎn)化一下,發(fā)現(xiàn) babel 會(huì)把上面的代碼轉(zhuǎn)化成:
function A() {
// ...other code
return React.createElement("h1", null, "前端桃園");
}
因?yàn)閺谋举|(zhì)上講,JSX 只是為 React.createElement(component, props, ...children)
函數(shù)提供的語(yǔ)法糖。
為什么要用 className 而不用 class
- React 一開(kāi)始的理念是想與瀏覽器的 DOM API 保持一直而不是 HTML,因?yàn)?JSX 是 JS 的擴(kuò)展,而不是用來(lái)代替 HTML 的,這樣會(huì)和元素的創(chuàng)建更為接近。在元素上設(shè)置
class
需要使用className
這個(gè) API:
const element = document.createElement("div")
element.className = "hello"
- 瀏覽器問(wèn)題,ES5 之前,在對(duì)象中不能使用保留字。以下代碼在 IE8 中將會(huì)拋出錯(cuò)誤:
const element = {
attributes: {
class: "hello"
}
}
- 解構(gòu)問(wèn)題,當(dāng)你在解構(gòu)屬性的時(shí)候,如果分配一個(gè)
class
變量會(huì)出問(wèn)題:
const { class } = { class: 'foo' } // Uncaught SyntaxError: Unexpected token }
const { className } = { className: 'foo' }
const { class: className } = { class: 'foo' }
其他討論可見(jiàn):有趣的話題,為什么jsx用className而不是class
為什么屬性要用小駝峰
因?yàn)?JSX 語(yǔ)法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用camelCase
(小駝峰命名)來(lái)定義屬性的名稱(chēng),而不使用 HTML 屬性名稱(chēng)的命名約定。
來(lái)自 JSX 簡(jiǎn)介
為什么 constructor 里要調(diào)用 super 和傳遞 props
這是官網(wǎng)的一段代碼,具體見(jiàn):狀態(tài)(State) 和 生命周期
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
而且有這么一段話,不僅讓我們調(diào)用 super
還要把 props
傳遞進(jìn)去,但是沒(méi)有告訴我們?yōu)槭裁匆@么做。
不知道你有沒(méi)有疑惑過(guò)為什么要調(diào)用 super
和傳遞 props
,接下來(lái)我們來(lái)解開(kāi)謎題吧。
為什么要調(diào)用 super
其實(shí)這不是 React 的限制,這是 JavaScript 的限制,在構(gòu)造函數(shù)里如果要調(diào)用 this,那么提前就要調(diào)用 super,在 React 里,我們常常會(huì)在構(gòu)造函數(shù)里初始化 state,this.state = xxx
,所以需要調(diào)用 super。
為什么要傳遞 props
你可能以為必須給 super
傳入 props
,否則 React.Component
就沒(méi)法初始化this.props
:
class Component {
constructor(props) {
this.props = props;
// ...
}
}
不過(guò),如果你不小心漏傳了 props
,直接調(diào)用了 super()
,你仍然可以在 render
和其他方法中訪問(wèn) this.props
(不信的話可以試試嘛)。
為啥這樣也行?因?yàn)?strong>React 會(huì)在構(gòu)造函數(shù)被調(diào)用之后,會(huì)把 props 賦值給剛剛創(chuàng)建的實(shí)例對(duì)象:
const instance = new YourComponent(props);
instance.props = props;
props
不傳也能用,是有原因的。
但這意味著你在使用 React 時(shí),可以用 super()
代替 super(props)
了么?
那還是不行的,不然官網(wǎng)也不會(huì)建議你調(diào)用 props 了,雖然 React 會(huì)在構(gòu)造函數(shù)運(yùn)行之后,為 this.props
賦值,但在 super()
調(diào)用之后與構(gòu)造函數(shù)結(jié)束之前,this.props
仍然是沒(méi)法用的。
// Inside React
class Component {
constructor(props) {
this.props = props;
// ...
}
}
// Inside your code
class Button extends React.Component {
constructor(props) {
super(); // 忘了傳入 props
console.log(props); // {}
console.log(this.props); // undefined
}
// ...
}
要是構(gòu)造函數(shù)中調(diào)用了某個(gè)訪問(wèn) props
的方法,那這個(gè) bug 就更難定位了。因此我強(qiáng)烈建議始終使用super(props),即使這不是必須的:
class Button extends React.Component {
constructor(props) {
super(props); // We passed props
console.log(props); // {}
console.log(this.props); // {}
}
// ...
}
上面的代碼確保 this.props
始終是有值的。
如果你想避免以上的問(wèn)題,你可以通過(guò)class 屬性提案 來(lái)簡(jiǎn)化代碼:
class Clock extends React.Component {
state = {date: new Date()};
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
更詳細(xì)的內(nèi)容可見(jiàn)Dan 的博客
為什么組件用大寫(xiě)開(kāi)頭
前面以及說(shuō)過(guò)了,JSX 是 React.createElement(component, props, …children)
提供的語(yǔ)法糖,component 的類(lèi)型是:string/ReactClass type
,我們具體看一下在什么情況下會(huì)用到 string 類(lèi)型,什么情況下用到 ReactClass type 類(lèi)型
- string 類(lèi)型react會(huì)覺(jué)得他是一個(gè)原生dom節(jié)點(diǎn)
- ReactClass type 類(lèi)型 自定義組件
例如(string):在 jsx 中我們寫(xiě)一個(gè)
<div></div>
轉(zhuǎn)換為js的時(shí)候就變成了
React.createElement("div", null)
例如(ReactClass type):在jsx中我們寫(xiě)一個(gè)
function MyDiv() {
return (<div><div>)
}
轉(zhuǎn)換為js的時(shí)候就變成了
function MyDiv() {
return React.createElement("div", null);
}
React.createElement(MyDiv, null);
上邊的例子中如果將MyDiv中的首字母小寫(xiě),如下
function myDiv() {
return (<div><div>)
}
轉(zhuǎn)換為 js 的時(shí)候就變成了
function MyDiv() {
return React.createElement("div", null);
}
React.createElement("myDiv", null);
由于找不到 myDiv 這個(gè) dom,所以就會(huì)報(bào)錯(cuò)。
為什么調(diào)用方法要 bind this
前提知識(shí):深刻的理解 JavaScript 中的 this
相信剛寫(xiě) React 的時(shí)候,很多朋友可能會(huì)寫(xiě)類(lèi)似這樣的代碼:
class Foo extends React.Component {
handleClick () {
this.setState({ xxx: aaa })
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
)
}
}
發(fā)現(xiàn)會(huì)報(bào) this
是 undefined
的錯(cuò),然后可能對(duì)事件處理比較疑惑,然后去看官網(wǎng)的事件處理有下面一段話:
你必須謹(jǐn)慎對(duì)待 JSX 回調(diào)函數(shù)中的
this
,在 JavaScript 中,class 的方法默認(rèn)不會(huì)綁定this
。如果你忘記綁定this.handleClick
并把它傳入了onClick
,當(dāng)你調(diào)用這個(gè)函數(shù)的時(shí)候this
的值為undefined
。
這并不是 React 特有的行為;這其實(shí)與 JavaScript 函數(shù)工作原理有關(guān)。通常情況下,如果你沒(méi)有在方法后面添加()
,例如onClick={this.handleClick}
,你應(yīng)該為這個(gè)方法綁定this
。
然后你看了官網(wǎng)的例子和建議之后,知道需要為事件處理函數(shù)綁定 this
就能解決,想下面這樣:
class Foo extends React.Component {
handleClick () {
this.setState({ xxx: aaa })
}
render() {
return (
<button onClick={this.handleClick.bind(this)}>
Click me
</button>
)
}
}
但是可能你沒(méi)有去思考過(guò)為什么需要 bind this?如果你不能理解的話,還是 js 的基礎(chǔ)沒(méi)有打好。
React 是如何處理事件的?
咱們先來(lái)了解一下 React 是如何處理事件的。
React 的事件是合成事件, 內(nèi)部原理非常復(fù)雜,我這里只把關(guān)鍵性,可以用來(lái)解答這個(gè)問(wèn)題的原理部分進(jìn)行介紹即可(后面應(yīng)該會(huì)寫(xiě)一篇 react 的事件原理,敬請(qǐng)期待)。
上篇文章已經(jīng)說(shuō)過(guò),jsx 實(shí)際上是 React.createElement(component, props, …children)
函數(shù)提供的語(yǔ)法糖,那么這段 jsx 代碼:
<button onClick={this.handleClick}>
Click me
</button>
會(huì)被轉(zhuǎn)化為:
React.createElement("button", {
onClick: this.handleClick
}, "Click me")
了解了上面的,然后簡(jiǎn)單的理解 react 如何處理事件的,React 在組件加載(mount
)和更新(update
)時(shí),將事件通過(guò) addEventListener
統(tǒng)一注冊(cè)到 document
上,然后會(huì)有一個(gè)事件池存儲(chǔ)了所有的事件,當(dāng)事件觸發(fā)的時(shí)候,通過(guò) dispatchEvent
進(jìn)行事件分發(fā)。
所以你可以簡(jiǎn)單的理解為,最終 this.handleClick
會(huì)作為一個(gè)回調(diào)函數(shù)調(diào)用。
理解了這個(gè),然后再來(lái)看看回調(diào)函數(shù)為什么就會(huì)丟失 this
。
this 簡(jiǎn)單回顧
在函數(shù)內(nèi)部,
this
的值取決于函數(shù)被調(diào)用的方式。
如果你不能理解上面那句話,那么你可能需要停下來(lái)閱讀文章,去查一下相關(guān)資料,否則你可能看不懂下面的,如果你懶的話,就看為你準(zhǔn)備好的 MDN 吧。
通過(guò)上面對(duì)事件處理的介紹,來(lái)模擬一下在類(lèi)組件的 render 函數(shù)中, 有點(diǎn)類(lèi)似于做了這樣的操作:
class Foo {
sayThis () {
console.log(this); // 這里的 `this` 指向誰(shuí)?
}
exec (cb) {
cb();
}
render () {
this.exec(this.sayThis);
}
}
var foo = new Foo();
foo.render(); // 輸出結(jié)果是什么?
你會(huì)發(fā)現(xiàn)最終結(jié)果輸出的是 undefined
,如果你不理解為什么輸出的是 undefined
,那么還是上面說(shuō)的,需要去深刻的理解 this 的原理。如果你能理解輸出的是undefined
,那么我覺(jué)得你就可以理解為什么需要 bind this 了。
那么你可能會(huì)問(wèn):為什么React沒(méi)有自動(dòng)的把 bind 集成到 render 方法中呢?在 exec 調(diào)用回調(diào)的時(shí)候綁定進(jìn)去,像這樣:
class Foo {
sayThis () {
console.log(this); // 這里的 `this` 指向誰(shuí)?
}
exec (cb) {
cb().bind(this);
}
render () {
this.exec(this.sayThis);
}
}
var foo = new Foo();
foo.render(); // 輸出結(jié)果是什么?
因?yàn)?render 多次調(diào)用每次都要 bind 會(huì)影響性能,所以官方建議你自己在 constructor 中手動(dòng) bind 達(dá)到性能優(yōu)化。
四種事件處理對(duì)比
對(duì)于事件處理的寫(xiě)法也有好幾種,咱們來(lái)進(jìn)行對(duì)比一下:
1. 直接 bind this 型
就是像文章開(kāi)始的那樣,直接在事件那里 bind this
class Foo extends React.Component {
handleClick () {
this.setState({ xxx: aaa })
}
render() {
return (
<button onClick={this.handleClick.bind(this)}>
Click me
</button>
)
}
}
優(yōu)點(diǎn):寫(xiě)起來(lái)順手,一口氣就能把這個(gè)邏輯寫(xiě)完,不用移動(dòng)光標(biāo)到其他地方。
缺點(diǎn):性能不太好,這種方式跟 react 內(nèi)部幫你 bind 一樣的,每次 render 都會(huì)進(jìn)行 bind,而且如果有兩個(gè)元素的事件處理函數(shù)式同一個(gè),也還是要進(jìn)行 bind,這樣會(huì)多寫(xiě)點(diǎn)代碼,而且進(jìn)行兩次 bind,性能不是太好。(其實(shí)這點(diǎn)性能往往不會(huì)是性能瓶頸的地方,如果你覺(jué)得順手,這樣寫(xiě)完全沒(méi)問(wèn)題)
2. constuctor 手動(dòng) bind 型
class Foo extends React.Component {
constuctor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick () {
this.setState({ xxx: aaa })
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
)
}
}
優(yōu)點(diǎn):相比于第一種性能更好,因?yàn)闃?gòu)造函數(shù)只執(zhí)行一次,那么只會(huì) bind 一次,而且如果有多個(gè)元素都需要調(diào)用這個(gè)函數(shù),也不需要重復(fù) bind,基本上解決了第一種的兩個(gè)缺點(diǎn)。
缺點(diǎn):沒(méi)有明顯缺點(diǎn),硬要說(shuō)的話就是太丑了,然后不順手(我覺(jué)得丑,你覺(jué)得不丑就這么寫(xiě)就行了)。
3. 箭頭函數(shù)型
class Foo extends React.Component {
handleClick () {
this.setState({ xxx: aaa })
}
render() {
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
)
}
}
優(yōu)點(diǎn):順手,好看。
缺點(diǎn):每次 render 都會(huì)重復(fù)創(chuàng)建函數(shù),性能會(huì)差一點(diǎn)。
4. public class fields 型
這種 class fields
還處于實(shí)驗(yàn)階段,據(jù)我所知目前還沒(méi)有被納入標(biāo)準(zhǔn),具體可見(jiàn)這里。
class Foo extends React.Component {
handleClick = () => {
this.setState({ xxx: aaa })
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
)
}
}
優(yōu)點(diǎn):好看,性能好。
缺點(diǎn):沒(méi)有明顯缺點(diǎn),如果硬要說(shuō)可能就是要多裝一個(gè) babel 插件來(lái)支持這種語(yǔ)法。
總結(jié)
我平時(shí)用的就這四種寫(xiě)法,我這邊從代碼的美觀性、性能以及是否順手方便對(duì)各種寫(xiě)法做了簡(jiǎn)單的對(duì)比。其實(shí)每種方法在項(xiàng)目里用都是沒(méi)什么問(wèn)題的,性能方面基本上可以忽略,對(duì)于美觀性和順手比較主觀,所以總體來(lái)說(shuō)就是看大家的偏好咯,如果硬要推薦的話,我還是比較推薦第四種寫(xiě)法,美觀而且不影響性能。
為什么要 setState,而不是直接 this.state.xx = oo
這個(gè)問(wèn)題是我們公司后端寫(xiě) React 的時(shí)候提出的問(wèn)題,為啥不能直接修改 state,要 setState 一下。我在想,從 vue 轉(zhuǎn)到 React 可能也會(huì)有這種疑問(wèn),因?yàn)?vue 修改狀態(tài)都是直接改的。
如果我們了解 setState 的原理的話,可能就能解答這個(gè)問(wèn)題了,setState 做的事情不僅僅只是修改了 this.state
的值,另外最重要的是它會(huì)觸發(fā) React 的更新機(jī)制,會(huì)進(jìn)行 diff ,然后將 patch 部分更新到真實(shí) dom 里。
如果你直接 this.state.xx == oo
的話,state 的值確實(shí)會(huì)改,但是改了不會(huì)觸發(fā) UI 的更新,那就不是數(shù)據(jù)驅(qū)動(dòng)了。
那為什么 Vue 直接修改 data 可以觸發(fā) UI 的更新呢?因?yàn)?Vue 在創(chuàng)建 UI 的時(shí)候會(huì)把這些 data 給收集起來(lái),并且在這些 data 的訪問(wèn)器屬性 setter 進(jìn)行了重寫(xiě),在這個(gè)重寫(xiě)的方法里會(huì)去觸發(fā) UI 的更新。如果你想更多的了解 vue 的原理,可以去購(gòu)買(mǎi)染陌大佬的剖析 Vue.js 內(nèi)部運(yùn)行機(jī)制。
不明白訪問(wèn)器屬性的可以看這篇文章:深入理解JS里的對(duì)象
setState 是同步還是異步相關(guān)問(wèn)題
1. setState 是同步還是異步?
我的回答是執(zhí)行過(guò)程代碼同步的,只是合成事件和鉤子函數(shù)的調(diào)用順序在更新之前,導(dǎo)致在合成事件和鉤子函數(shù)中沒(méi)法立馬拿到更新后的值,形式了所謂的“異步”,所以表現(xiàn)出來(lái)有時(shí)是同步,有時(shí)是“異步”。
2. 何時(shí)是同步,何時(shí)是異步呢?
只在合成事件和鉤子函數(shù)中是“異步”的,在原生事件和 setTimeout/setInterval等原生 API 中都是同步的。簡(jiǎn)單的可以理解為被 React 控制的函數(shù)里面就會(huì)表現(xiàn)出“異步”,反之表現(xiàn)為同步。
3. 那為什么會(huì)出現(xiàn)異步的情況呢?
為了做性能優(yōu)化,將 state 的更新延緩到最后批量合并再去渲染對(duì)于應(yīng)用的性能優(yōu)化是有極大好處的,如果每次的狀態(tài)改變都去重新渲染真實(shí) dom,那么它將帶來(lái)巨大的性能消耗。
4. 那如何在表現(xiàn)出異步的函數(shù)里可以準(zhǔn)確拿到更新后的 state 呢?
通過(guò)第二個(gè)參數(shù) setState(partialState, callback)
中的 callback 拿到更新后的結(jié)果。
或者可以通過(guò)給 setState 傳遞函數(shù)來(lái)表現(xiàn)出同步的情況:
this.setState((state) => {
return { val: newVal }
})
5. 那表現(xiàn)出異步的原理是怎么樣的呢?
直接講源碼肯定篇幅不夠,可以看這篇文章:你真的理解setState嗎?。
我這里還是用最簡(jiǎn)單的語(yǔ)言讓你理解:在 React 的 setState 函數(shù)實(shí)現(xiàn)中,會(huì)根據(jù) isBatchingUpdates(默認(rèn)是 false) 變量判斷是否直接更新 this.state 還是放到隊(duì)列中稍后更新。然后有一個(gè) batchedUpdate 函數(shù),可以修改 isBatchingUpdates 為 true,當(dāng) React 調(diào)用事件處理函數(shù)之前,或者生命周期函數(shù)之前就會(huì)調(diào)用 batchedUpdate 函數(shù),這樣的話,setState 就不會(huì)同步更新 this.state,而是放到更新隊(duì)列里面后續(xù)更新。
這樣你就可以理解為什么原生事件和 setTimeout/setinterval 里面調(diào)用 this.state 會(huì)同步更新了吧,因?yàn)橥ㄟ^(guò)這些函數(shù)調(diào)用的 React 沒(méi)辦法去調(diào)用 batchedUpdate 函數(shù)將 isBatchingUpdates 設(shè)置為 true,那么這個(gè)時(shí)候 setState 的時(shí)候默認(rèn)就是 false,那么就會(huì)同步更新。
最后
setState 是 React 非常重要的一個(gè)方法,值得大家好好去研究一下他的原理。
以上就是W3Cschool編程獅
關(guān)于新手學(xué)習(xí) react 迷惑的點(diǎn)(也可以復(fù)習(xí),建議收藏!)的相關(guān)介紹了,希望對(duì)大家有所幫助。