React 是一個用於構建用戶界面的 JavaScript 庫。數據改變時 React 能有效地更新並正確地渲染組件。javascript
咱們寫 React 就是建立擁有各自狀態的組件,再由這些組件構成更加複雜的 UI 界面。java
第一個組件:react
import React from 'react';
class HelloReact extends React.Component {
render() {
return (
<div> Hello {this.props.name} </div>
);
}
}
export default HelloReact;
複製代碼
組件擁有動態渲染 name 的功能,name 字段是使用該組件的地方傳入的。git
<HelloReact name={"第一個組件"}/>
複製代碼
所以咱們就能夠正確渲染出一個DOM結構github
接觸 React 想必 JSX 應該會是你接觸的第一個新概念,由於你多是第一次在 js 文件中直接編寫「HTML」。編程
return (
<div> Hello {this.props.name} </div>
);
複製代碼
JSX 是一個 JavaScript 的語法擴展,它能夠生成 React 「元素」。數組
它的寫法徹底等價於:瀏覽器
import React from 'react';
class HelloJSX extends React.Component {
render() {
return React.createElement("div", null, "Hello ", this.props.name);
}
}
export default HelloJSX;
複製代碼
其實 React 最終須要的就是 React.createElement("div", null, "Hello ", this.props.name)
經過它,React 能夠建立一系列數據結構來表示 React 中的元素,最終再把它轉換成真實的 DOM 插入到頁面中。緩存
所以 JSX 的結構與真實的 DOM 結構只能說是類似,它們之間還須要 React 去作一系列轉換。
JSX 自己在 React 項目中只是語法糖,不能直接被瀏覽器識別的。須要通過編譯,那麼這個編譯後的就是它的本質了。
JSX 在 React 項目中被編譯成了 React 元素,就是一個樹狀的數據結構。也就是咱們常說的虛擬 DOM。
既然 Reac t是經過組件的組合來實現複雜工程的構建。那麼組件之間的第一個問題就是它們之間如何通訊。
經過 props 向子組件傳遞數據,而且全部 React 組件都必須像純函數同樣保護它們的 props 不被更改。
import React from 'react';
class Child extends React.Component {
render() {
const { list } = this.props
return <ul>{list.map((item, index) => { return <li key={item.id}> <span>{item.title}</span> </li> })}</ul>
}
}
class Parent extends React.Component{
constructor(props) {
super(props)
this.state = {
list: [
{
id: 'id-1',
title: '標題1'
},
{
id: 'id-2',
title: '標題2'
},
{
id: 'id-3',
title: '標題3'
}
]
}
}
render() {
return <Child list={this.state.list} /> } } export default Parent; 複製代碼
代碼解釋:
<Child list={this.state.list} />
const { list } = this.props
這個就是父組件向子組件傳遞數據。
思考一個問題,當咱們的組件層級嵌套很深,例如 Parent 組件須要向 Child 的 Child 也就是孫子組件,甚至乎更加深的層級去傳遞一些數據該如何作呢?能夠經過context去實現。
Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。
咱們經常用它來傳遞應用級別的配置數據,例如APP的主題、用戶喜愛之類的。
咱們來實現一個切換主題的簡單場景:
import React from 'react'
// 建立 Context 填入默認值(任何一個 js 變量)
const ThemeContext = React.createContext('light') // {1}
// 函數式組件可使用 Consumer
function ThemeLink (props) {
return <ThemeContext.Consumer> // {3}
{ value => <p>link's theme is {value}</p> }
</ThemeContext.Consumer>
}
class ThemedButton extends React.Component {
render() {
const theme = this.context // React 會往上找到最近的 theme Provider,而後使用它的值。
return <div>
<p>button's theme is {theme}</p>
</div>
}
}
ThemedButton.contextType = ThemeContext // 指定 contextType 讀取當前的 theme context。
// 中間的組件不再必指明往下傳遞 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
<ThemeLink />
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: 'light'
}
}
render() {
return <ThemeContext.Provider value={this.state.theme}> // {2}
<Toolbar />
<hr/>
<button onClick={this.changeTheme}>change theme</button>
</ThemeContext.Provider>
}
changeTheme = () => {
this.setState({
theme: this.state.theme === 'light' ? 'dark' : 'light'
})
}
}
export default App
複製代碼
代碼解釋:
const ThemeContext = React.createContext('light')
建立一個context,返回一個ThemeContext 對象<ThemeContext.Provider value={this.state.theme}></ThemeContext.Provider>
ThemeContext 的提供者<ThemeContext.Consumer></ThemeContext.Consumer>
ThemeContext 的消費者export function createContext( defaultValue, calculateChangedBits ){
const context = {
$$typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
_currentValue: defaultValue,
_currentValue2: defaultValue,
Provider:null,
Consumer:null ,
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
return context;
}
複製代碼
這是通過刪減的源碼,調用 createContext
函數返回的就是一個context對象,咱們使用的 ThemeContext.Provider
也就是該對象返回的其中一個屬性。
經過上面咱們已經知道組件之間是如何進行通訊的,那麼組件如何維護自身的狀態呢?就是 state
了,由於它的一些特性,也讓它成爲了面試必考。
import React from 'react'
class StateDemo extends React.Component{
constructor(props) {
super(props);
this.state = {
count: 0
}
}
add = ()=>{
this.setState({
count: this.state.count + 1
})
}
render() {
return <div> <p>{this.state.count}</p> <button onClick={this.add}>增長</button> </div>
}
}
export default StateDemo
複製代碼
代碼解釋:
這就是組件的 state,以及如何改變 state,關於它的常見面試題有:
setState
只有一次生效?componentDidMount() {
console.log('SetState調用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 0
console.log('SetState調用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 0
}
複製代碼
兩次打印都是0,沒有當即更新
add = ()=>{
console.log('合成事件中調用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 0
console.log('合成事件中調用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 0
}
複製代碼
componentDidMount() {
setTimeout(()=>{
console.log('SetState調用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 1
console.log('SetState調用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 2
},0);
}
複製代碼
咱們使用 setTimeout 異步函數包裝下,發現,setState 同步執行了。
componentDidMount(){
document.body.addEventListener('click', this.bodyClickHandler); // 在生命週期中綁定原生事件
}
bodyClickHandler = ()=>{
console.log('原生事件中調用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 1
console.log('原生事件中調用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 2
}
複製代碼
在原生事件中執行 setState 也同步執行了。
異步狀況:
緣由:
在React
的生命週期和合成事件中,React
仍然處於他的更新機制中,這時isBatchingUpdates
爲 true
。 這時不管調用多少次setState
,都會不會執行更新,而是將要更新的state
存入_pendingStateQueue
,將要更新的組件存入dirtyComponent
。
當上一次更新機制執行完畢,以生命週期爲例,全部組件,即最頂層組件didmount
後會將 isBatchingUpdates
設置爲 false
。這時將執行以前累積的setState
同步狀況:
緣由:
由執行機制看,setState
自己並非異步的,而是當調用setState
時,若是React
正處於更新過程,當前更新會被暫存,等上一次更新執行後再執行,這個過程給人一種異步的假象。
在生命週期,根據JS的異步機制,會將異步函數先暫存,等全部同步代碼執行完畢後再執行,這時上一次更新過程已經執行完畢,isBatchingUpdates
被設置爲 false
,根據上面的流程,這時再調用setState
便可當即執行更新,拿到更新結果。
簡單理解:當isBatchingUpdates
爲 true
時,加入異步隊列,下一輪執行;當isBatchingUpdates
爲 false
時,不加入異步隊列,當即執行。能夠理解成相似 JavaScript EventLoop
機制。
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
複製代碼
這句話的大概意思是,isBatchingUpdates
變量爲 false
時當即執行performSyncWork方法。從該方法名的意思是「執行同步任務」。
Batch模式下React不會馬上修改state,而是把這個對象放到一個更新隊列中,稍後纔會從隊列中把新的狀態提取出來合併到state中,而後再觸發組件更新,這樣的設計主要目的是爲了提升 UI 更新的性能。
從 React 源碼中也能夠看出 setState 第一個參數其實能夠接受兩種類型的,一種是對象,一種是函數。對象型是會進行合併處理 Object.assagin
,而函數型是不會的。
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼
咱們再來看看源碼中 enqueueSetState
是如何進行處理的:
for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
let partial = oldQueue[i];
let partialState =
typeof partial === 'function'
? partial.call(inst, nextState, element.props, publicContext)
: partial;
if (partialState != null) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState);
}
}
}
複製代碼
這是其中處理 state queue 的源碼片斷,咱們關注重點便可
if(partial === 'function'){
partial.call(inst, nextState, element.props, publicContext)
}
if (partialState != null) {
Object.assign(nextState, partialState);
}
複製代碼
若是 queue 中的 state 是函數則直接執行,若是是非函數,且不是null,則先進行對象合併操做。
咱們來測試下傳入函數是否真的不會合並
componentDidMount() {
console.log('SetState傳入函數');
this.setState((state, props) => ({
index: state.index + 1
}));
console.log('state', this.state.index); // 0
console.log('SetState傳入函數');
this.setState((state, props) => ({
index: state.index + 1
}));
console.log('state', this.state.index); // 0
setTimeout(()=>{
console.log('state', this.state.index); // 2
},0);
}
複製代碼
從結果能夠看出來,咱們同時更新了兩次index,說明傳入函數並不會被合併。
咱們那上面的 add 方法作例子,改造下:
add = ()=>{
this.state.count++;
this.setState({
count: this.state.count
})
}
複製代碼
咱們發現效果是同樣的,是否是表示咱們這樣作也能夠呢?答案是否認的。
必須得這樣寫:
add = ()=>{
this.setState({
count: this.state.count + 1
})
}
複製代碼
不可變值在對象和數組中的應用:
import React from 'react'
class StateDemo1 extends React.Component{
constructor(props) {
super(props);
this.state = {
list:[
{
id:1,
name:"a"
},
{
id:2,
name:"b"
},
{
id:3,
name:"C"
},
]
}
}
render() {
return (
<div> <ul> { this.state.list.map((item)=>{ return <li key={item.id}> <span>{item.name}</span> </li> }) } </ul> <button onClick={this.deletePop}>刪除最後一條</button> </div>
)
}
}
export default StateDemo1
複製代碼
當咱們須要對這個列表進行刪除時, this.state.list
就必須遵循不可變值。
deletePop = ()=>{
const newList = [...this.state.list];
newList.pop();
this.setState({
list: newList
})
}
複製代碼
this.state.list
進行淺複製在這裏咱們沒有去改變原 state.list
值,而是淺複製了一個副本出來再去操做的。這就是在 React
編程中要很是注意的「不可變值」。
至於爲何必需要不可變值呢,這個其實跟React性能優化有很是大的關係,在下一篇文章中會詳細講述其中緣由。
Virtual DOM 在內存中是以對象的形式存在的,若是想要在這些對象上添加事件,就會很是簡單。React 基於 Virtual DOM 實現了一個 SyntheticEvent (合成事件)層,咱們所定義的事件處理器會接收到一個 SyntheticEvent 對象的實例,它徹底符合 W3C 標準,不會存在任何 IE 標準的兼容性問題。而且與原生的瀏覽器事件同樣擁有一樣的接口,一樣支持事件的冒泡機制,咱們可使用 stopPropagation() 和 preventDefault() 來中斷它。
在 React 底層,主要對合成事件作了兩件事:事件委派和自動綁定。
在使用 React 事件前,必定要熟悉它的事件代理機制。它並不會把事件處理函數直接綁定到真實的節點上,而是把全部事件綁定到結構的最外層,使用一個統一的事件監聽器,這個事件監聽器上維持了一個映射來保存全部組件內部的事件監聽和處理函數。當組件掛載或卸載時,只是在這個統一的事件監聽器上插入或刪除一些對象;當事件發生時,首先被這個統一的事件監聽器處理,而後在映射裏找到真正的事件處理函數並調用。這樣作簡化了事件處理和回收機制,效率也有很大提高。
在 React 組件中,每一個方法的上下文都會指向該組件的實例,即自動綁定 this
爲當前組件。並且 React 還會對這種引用進行緩存,以達到 CPU 和內存的最優化。 實際上,React 的合成事件系統只是原生 DOM 事件系統的一個子集。它僅僅實現了 DOM Level 3 的事件接口,而且統一了瀏覽器間的兼容問題。有些事件 React 並無實現,或者受某些限制沒辦法去實現,好比 window
的 resize
事件。
對於沒法使用 React 合成事件的場景,咱們還須要使用原生事件來完成。
this
ES6類的方法內部若是含有this
,它默認指向類的實例。可是,必須很是當心,一旦單獨使用該方法,極可能報錯。而React中執行事件回調方法放入一個隊列中,當事件被觸發時執行相應的回調,所以該事件回調方法並未與React組件實例綁定在一塊兒,因此咱們須要進行手動綁定上下文。
import React from 'react'
class EventDemo extends React.Component{
constructor(props) {
super(props);
this.state = {
count: 0
}
}
add(){
this.setState({
count: this.state.count + 1
})
}
render() {
return <div> <p>{this.state.count}</p> <button onClick={this.add.bind(this)}>增長</button> </div>
}
}
複製代碼
或者在 constructor 中 bind this
constructor(props) {
super(props);
this.state = {
count: 0
}
this.add = this.add.bind(this);
}
複製代碼
使用箭頭函數就不須要使用 bind this 進行綁定了
add = ()=>{
this.setState({
count: this.state.count + 1
})
}
複製代碼
React
事件和原生事件的執行順序import React from 'react'
class EventDemo extends React.Component {
constructor(props) {
super(props);
this.parent = React.createRef();
this.child = React.createRef();
}
componentDidMount() {
this.parent.current.addEventListener('click', (e) => {
console.log('dom parent');
})
this.child.current.addEventListener('click', (e) => {
console.log('dom child');
})
document.addEventListener('click', (e) => {
console.log('document');
})
}
childClick = (e) => {
console.log('react child');
}
parentClick = (e) => {
console.log('react parent');
}
render() {
return (
<div onClick={this.parentClick} ref={this.parent}> <div onClick={this.childClick} ref={this.child}> test Event </div> </div>)
}
}
export default EventDemo
複製代碼
執行結果:
dom child
dom parent
react child
react parent
document
複製代碼
由上面的流程咱們能夠理解:
React
的全部事件都掛載在document
中document
後纔會對React
事件進行處理React
合成事件document
上掛載的事件React
事件和原生事件最好不要混用。
原生事件中若是執行了stopPropagation
方法,則會致使其餘React
事件失效。由於全部元素的事件將沒法冒泡到document
上。
受控組件與非受控組件主要是針對表單元素
咱們先來看一段React處理表單的代碼:
import React from 'react'
class FormDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'frank'
}
}
render() {
return <div> <p>{this.state.name}</p> <input id="inputName" value={this.state.name} onChange={this.onInputChange}/> </div> } onInputChange = (e) => { this.setState({ name: e.target.value }) } } export default FormDemo 複製代碼
你心中必定會有疑問,爲什麼 <input>
要綁定一個 change 事件呢?
在 HTML 中,表單元素(如<input>
、 <textarea>
和 <select>
)之類的表單元素一般本身維護 state,並根據用戶輸入進行更新。而在 React 中,可變狀態一般保存在組件的 state 屬性中,而且只能經過使用 setState()
來更新。被 React 以這種方式控制取值的表單輸入元素就叫作「受控組件」。
總結下 React 受控組件更新 state 的流程: (1) 能夠經過在初始 state 中設置表單的默認值。 (2) 每當表單的值發生變化時,調用 onChange
事件處理器。 (3) 事件處理器經過合成事件對象 e
拿到改變後的狀態,並更新應用的 state。 (4) setState
觸發視圖的從新渲染,完成表單組件值的更新。
先來看一段代碼:
class FormDemo1 extends React.Component {
constructor(props) {
super(props)
this.content = React.createRef();
}
handleSubmit=(e)=>{
e.preventDefault();
const { value } = this.content.current;
console.log(value);
}
render() {
return <form onSubmit={this.handleSubmit}> <input ref={this.content} type="text" defaultValue="frank" /> <button type="submit">Submit</button> </form> } } 複製代碼
在 React 中,非受控組件是一種反模式,它的值不受組件自身的 state 或 props 控制。一般,須要經過爲其添加 ref 來訪問渲染後的底層 DOM 元素。
受控組件和非受控組件的最大區別是:非受控組件的狀態並不會受應用狀態的控制,而受控組件的值來自於組件的 state。
因爲React在面試中佔有很是的比重,所以關於React技術棧的面試文章將分解爲多篇進行講解