很多人私信問我,學習vue好,還是學習react好?
其中一個人說:公司前端專案組準備推react,可是他個人覺得vue更加容易入手,只是他沒辦法說服leader,問我應該怎麼辦?還有其中一個人說,面試的時候總會問到框架的問題,可是現在他一種框架都沒掌握,應該選擇哪一種框架學習更加容易通過面試?
當然,前端學習對於我們來說越來越不友好,特別是隨著這幾年的發展,入門門檻越來越高,連進階道路都變成了一場馬拉松。在學習過程中,我們面臨很多選擇,vue與react便是一個兩難的選擇。
兩者都是非常優秀的框架,而且我不能非常主觀的說誰好誰不好。但是從我們初學者的角度來說,其實我們沒有必要去考慮誰的效能更好,誰的實現更優雅,誰更適合什麼場景等各種因素,唯一一個需要考慮的標準就是,學習誰,能夠讓我們更快的掌握它。因為無論他們兩個你掌握了誰,都能夠讓你在找工作時更有底氣。這就足夠了。
因此,我這篇文章的目的,則是希望從與官方文件不同的角度,來試圖讓react學習變得更加容易,如果你想要學習react,不妨花點時間讀下去。
為什麼對於新人來說,官方文件不能幫助你掌握得更好
對於vue的學習,很多朋友有一個大的誤解,認為vue官方出了中文文件,所以掌握起來會更加容易。然而事實上並非如此。
官方文件可能告訴了你vue/react的基礎知識有哪些,可是這些知識怎麼用,官方文件並沒有告訴我們。而且vue官方文件為了降低學習門檻(繞開了vue-cli),在講述知識的時候,不少地方其實與實際開發是有差距的,這個差距會導致你看完了官方文件,仍然不知道如何使用vue做一些事情。
當然,這樣的問題,react官方文件也存在。雖然對於經驗豐富的大神來說,這並不是問題,但是對於新人來說,這樣的差距往往會使得大家有一種似懂非懂的感覺。
這也是我為什麼要從和官方文件不一樣的角度來入手的原因。
學前準備
在準備學習本文的react知識之前,希望你已經擁有了ES6的知識與知道了create-react-app
的安裝與使用,我們的學習將會建立在這基礎之上,如果你暫時還沒有接觸過他們,不用擔心,可以回過頭去閱讀我的前兩篇文章。不用花太多時間就可以初步掌握。
ES6常用知識合集
詳解create-react-app 與 ES6 modules
你可以暫時不用對react有什麼基礎的瞭解,我們可以從0開始,當然,如果你看過官方文件或者從其他地方學習過相關知識就更好了。
開始啦,萬能的Hello World程式
首先,假設你已經在電腦上安裝好了create-react-app
並知道如何使用,那麼我就開始在你電腦上存放開發專案的目錄(本文中假設為develop)裡開始建立一個名為first-react
的react專案。操作順序如下:
1 2 3 4 5 6 7 8 9 10 11 |
// 在develop目錄建立first-react專案 > create-react-app first-react // 進入新建立的專案 > cd first-react // 安裝專案依賴包 > npm install // 安裝完畢之後啟動專案 > npm start |
啟動之後,效果分別如下圖所示:
自動生成的專案是一個簡單的react demo。這個時候專案中會有三個資料夾,我們來分別瞭解一下這三個資料夾的作用。
- node_modules
專案依賴包存放位置。當我們執行npm install安裝package.json中的依賴包時,該資料夾會自動建立,所有的依賴包會安裝到該資料夾裡。 - public
主要的作用是html入口檔案的存放。當然我們也可以存放其他公用的靜態資源,如圖片,css等。其中的index.html就是我們專案的入口html檔案。 - src
元件的存放目錄。在create-react-app建立的專案中,每一個單獨的檔案都可以被看成一個單獨的模組,單獨的image,單獨的css,單獨js等,而所有的元件都存放於src目錄中,其中index.js則是js的入口檔案。雖然我們並沒有在index.html中使用script標籤引入他,但是他的作用就和此一樣。
我們在最初學習開發一個頁面的時候,就已經知道一個頁面會有一個html檔案,比如index.html,然後分別在html檔案中,通過script與link標籤引入js與css。但是在構建工具中,我們只需要按照一定的規則來組織檔案即可,整合的工作構建工具會自動幫助我們完成,這也是構建工具給前端開發帶來的便利之處,也因為如此,前端的模組化開發才成為了可能。
我們還是和上一篇文章中說的一樣,先清空src目錄裡所有的其他檔案,僅僅只留下空的入口檔案index.js
,並在index.js
寫入如下的程式碼:
1 2 3 4 5 6 7 |
// src/index.js import React from 'react'; import { render } from 'react-dom'; const root = document.querySelector('#root'); render(<div>Hello World!</div>, root); |
儲存之後,結果如下:
如何你能輕鬆看懂這四行程式碼,那麼說明你離掌握react已經不遠了。至少你已經掌握了ES6的相關知識。我來解釋一下這些程式碼的作用。
import React from 'react';
在我們通過npm install
指令安裝依賴包的時候,就已經安裝好了react,因此我們可以直接import。這句話的作用就在於,能夠讓構建工具在當前模組中識別jsx。而jsx,是一種類似於html標籤的模板語言,我們只需要懂得html標籤,就不必花費額外的精力去了解jsx,因為我們可以直接理解為它就是html標籤,但是在此基礎上,擴充套件了更多的能力。例如這裡,程式能夠識別Hello World!,正是這句話的作用。
import { render } from 'react-dom';
這是利用ES6的解析結構的語法,僅僅引入了react-dom
的render
方法。render方法的作用,就是將react元件,渲染進DOM結構中,它的第一個引數就是react 元件,第二個引數則是一個DOM元素物件。const root = document.querySelector('#root');
這句話就很簡答了,如果你理解不了,那麼說明你的基礎還不足以支撐你學習react, – -。render(<div>Hello World!</div>, root);
這是最核心的一步,通過render
方法,將寫好的react元件渲染進DOM元素物件。而這裡的root
,則是在index.html
中寫好的一個元素。這裡的div
,可以理解為一個最簡單的react元件。
OK,理解了這些,那麼我們就可以開始學習react最核心的內容元件
了。
react元件
曾經,建立react元件有三種方式,但是既然都決定在ES6的基礎上來學習react了,那麼我也就只介紹其中的兩種方式了。反正另外一種方式也已經被官方廢棄。
當一個元件,並沒有額外的邏輯處理,僅僅只是用於資料的展示時,我們推薦使用函式式的方式來建立一個無狀態元件。
我們結合簡單的例子來理解。在專案的src目錄裡建立一個叫做helloWorld.jsx
的檔案。在該檔案中,我們將建立一個正式的react元件,程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
// src/helloWorld.jsx import React from 'react'; const HelloWorld = () => { return ( <div>Hello World!</div> ) } export default HelloWorld; |
並在index.js
中引入該元件。修改index.js
程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
// src/index.js import React from 'react'; import { render } from 'react-dom'; // 引入HelloWorld元件 import HelloWorld from './helloWorld'; const root = document.querySelector('#root'); render(<HelloWorld />, root); |
儲存後執行,我們發現結果一樣。
在helloWorld.jsx
中,我們仍然引入了react
,是因為所有會涉及到jsx模板的元件,我們都要引入它,這樣構建工具才會識別得到。
元件裡只是簡單的建立了一個HelloWorld函式,並返回了一段html(jsx模板)。並在最後將HelloWorld函式作為對外的介面暴露出來export default HelloWorld
。
接下來我們通過一點一點擴充套件HelloWorld元件能力的方式,來學習元件相關的基礎知識。
向元件內部傳遞引數
向元件內部傳遞引數的方式很簡單,這就和在html標籤上新增一個屬性一樣。
例如我們希望向HelloWorld元件內傳遞一個name屬性。那麼只需要我們在使用該元件的時候,新增一個屬性即可。
1 2 |
<HelloWorld name="Tom" /> <HelloWorld name="Jake" /> |
如果我們希望元件最終渲染的結果是諸如:Tom say: Hello, world!
其中的名字可以在傳入時自定義。那麼我們在元件中應該如何接收傳遞進來的引數呢?
我們修改HelloWorld.jsx
如下:
1 2 3 4 5 6 7 8 9 10 11 |
// src/helloWorld.jsx import React from 'react'; const HelloWorld = (props) => { console.log(props); return ( <div>{ props.name } say: Hello World!</div> ) } export default HelloWorld; |
並在index.js中修改render方法的使用,向元件中傳入一個name屬性
1 2 |
// src/index.js render(, root); |
結果如下:
在HelloWorld
元件中,我使用了一個叫做props的引數。而通過列印出來props可以得知,props正是一個元件在使用時,所有傳遞進來屬性組合而成的一個物件。大家也可以在學習時多傳入幾個額外的引數,他們都會出現在props物件裡。
而在jsx模板中,通過
這樣的方式來將變數傳入進來。這是jsx模板語言支援的一種語法,大家記住能用即可。
大家要記住,使用這種方式建立的無狀態元件,會比另外一種方式效能更高,因此如果你的元件僅僅只是用於簡單的資料展示,沒有額外的邏輯處理,就要優先選擇這種方式。
那麼我們繼續升級HelloWorld元件的能力。現在我希望有一個點選事件,當我們點選該元件時,會在Console工具中列印出傳入的name值。這就涉及到了另外一種元件的建立,也就是當我們的元件開始有邏輯處理,之前的那種方式勝任不了時索要採取的一種形式。
修改helloWorld.jsx
檔案如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// helloWorld.jsx import React, { Component } from 'react'; class HelloWorld extends Component { clickHander = () => { console.log(this.props); console.log(this.props.name); } render () { return ( <div onClick={this.clickHander}>{ this.props.name } say: Hello World!</div> ) } } export default HelloWorld; |
如果,你同時熟知第一種react元件的建立方式,與ES6語法的話,相信上面的程式碼,並不會對你造成多少困擾。
沒錯,這種方式建立的元件,正是通過繼承react的Component
物件而來。所以建立的方式也是利用ES6的class語法來生成。也正因為如此,其中的很多實用方式,也就跟class的使用一樣了。
上面的render方法,則是Component中,專門提供的用來處理jsx模板的方法。
與第一種方式不同的是,我們接收傳入進來的引數,使用的是this.props
,第一種方式將props放置於函式引數中,而這種方式則是將props掛載與例項物件上,因此會有所不同。
而我們想要給一個元件新增點選事件,方式也與html標籤中幾乎一致
react事件相關的知識大家可以當做一個進階課程去研究,這裡就暫時不多說,詳情可以參考官方文件 https://facebook.github.io/react/docs/events.html
好了,現在大家初步認識了react的第二種元件的建立方式,那麼我們繼續搞事情,現在我想要的效果,是傳入兩個名字,name1=Tom, name2='Jason'
,我希望第一次點選時,log出Tom,第二次log出Jason,第三次Tom…
這個時候,我們就需要引入react元件非常核心的知識狀態state
。
修改helloWorld.jsx
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// helloWorld.jsx import React, { Component } from 'react'; class HelloWorld extends Component { state = { switch: 0, name: this.props.name1 } clickHander = () => { const { name1, name2 } = this.props; if (this.state.switch === 0) { console.log(name1); this.setState({ switch: 1, name: name2 }) } else { console.log(name2); this.setState({ switch: 0, name: name1 }) } } render () { return ( <div onClick={this.clickHander}>{ this.state.name } say: Hello World!</div> ) } } export default HelloWorld; |
先來說說state相關的基礎知識。首先了解ES6 class語法的同學都應該知道,當我們通過這種方式來寫的時候,其實是將state寫入了建構函式之中。
1 2 3 4 |
state = {} // 等同於ES5建構函式中的 this.state = {} |
因此深入掌握class語法對於學習react元件的幫助非常巨大,我們需要清楚的知道什麼樣的寫法會放入物件的什麼位置,是建構函式中,還是原型中等。這也是為什麼開篇我會強調一定要先對我的前兩篇文章所介紹的知識有一定了解才行。
因此,在物件中,我們可以通過this.state
的方式來訪問state中所儲存的屬性。同時,react還提供瞭如下的方式來修改state的值
1 2 3 |
this.setState({ name: 'newName' }) |
setState
接收一個物件,它的執行結果類似於執行一次assign方法。會修改傳入的屬性,而其他的屬性則保持不變。
react賦予state的特性,則是當state被修改時,會引起元件的一次重新渲染。即render方法會重新執行一次。也正是由於這個特性,因此當我們想要改變介面上的元素內容時,常常只需要改變state中的值就行了。這也是為什麼結合render方法,我們可以不再需要jquery的原因所在。
而setState
也有一個非常重要的特性,那就是,該方法是非同步的。它並不會立即執行,而會在下一輪事件迴圈中執行。
說到這裡,基礎薄弱的同學就開始頭暈了,這就是為什麼我在前面的文章都反覆強調基礎知識的重要性,基礎紮實,很多東西稍微一提,你就知道是怎麼回事,不紮實,到處都是讓你頭暈的點,不知道的沒關係,讀我這篇文章 http://www.jianshu.com/p/12b9f73c5a4f。
相信不理解這個點的同學肯定會遇到很多坑,所以千萬要記住了。
1 2 3 4 5 6 7 |
// 假設state.name的初始值為Tom,我們改變它的值 this.setState({ name: 'Jason' }) // 然後立即檢視它的值 console.log(this.state.name) // 仍然為Tom,不會立即改變 |
refs
我們知道,react元件其實是虛擬DOM,因此通常我們需要通過特殊的方式才能拿到真正的DOM元素。大概說一說虛擬DOM是個什麼形式存在的,它其實就是通過js物件的方式將DOM元素相關的都儲存其實,比如一個div元素可能會是這樣:
1 2 3 4 5 6 7 8 9 |
// 當然可能命名會是其他的,大概表達一個意思,不深究哈 { nodeName: 'div', className: 'hello-world', style: {}, parentNodes: 'root', childrenNodes: [] ... } |
而我們想要拿到真實的DOM元素,react中提供了一種叫做ref
的屬性來實現這個目的。
修改helloWorld.jsx
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React, { Component } from 'react'; class HelloWorld extends Component { clickHander = () => { console.log(this.refs) } render () { return ( <div className="container" onClick={this.clickHander}> <div ref="hello" className="hello">Hello</div> <div ref="world" className="world">World</div> </div> ) } } export default HelloWorld; |
為了區分ES6語法中的class關鍵字,當我們在jsx中給元素新增class時,需要使用
className
來代替
我們在jsx中,可以給元素新增ref
屬性,而這些擁有ref屬性的元素,會統一放在元件物件的refs
中,因此,當我們想要訪問對應的真實DOM時,則通過this.refs
來訪問即可。
當然,ref的值不僅僅可以為一個名字,同時還可以為一個回撥函式,這個函式會在render渲染時執行,也就是說,每當render函式執行一次,ref的回撥函式也會執行一次。
修改helloWorld.jsx
如下,感受一下ref回撥的知識點
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// src/helloWorld.jsx import React, { Component } from 'react'; class HelloWorld extends Component { clickHander = () => { console.log(this.refs) } refCallback = (elem) => { console.log(elem); } render () { return ( <div className="container" onClick={this.clickHander}> <div ref="hello" className="hello">Hello</div> <div ref={this.refCallback} className="world">World</div> </div> ) } } export default HelloWorld; |
大概介紹一下我暫時能想到的ref使用的一個場景。例如我們要實現元素拖拽的時候,或者寫一個slider元件。我們可能會非常頻繁的改動元素的位置。這個時候,如果我們仍然通過react元件的state來儲存元素的位置,那麼就會導致react元件過於頻繁的渲染,這就會引發一個嚴重的效能問題。所以這個時候我們不得不獲取到真實DOM,並通過常規的方式來做。
同樣的道理也適用於vue中,我們要儘量避免將可能會變動頻率非常高的屬性存放於vue元件的data中。
元件生命週期
所謂元件的生命週期,指的就是一個元件,從建立到銷燬的這樣一個過程。
而react為元件的生命週期提供了很多的鉤子函式。很多地方也為生命週期畫了很清晰明瞭的圖幫助大家理解。但是我在初學的時候其實並沒有看懂,還是在我懂得了生命週期之後,才看懂的那些圖。所以呢,這裡我也就不去找圖了。我們這樣理解。
通俗來說,react為一個元件,劃分瞭如下的時刻。
- 元件第一次渲染完成的前後時刻,
componentWillMount
渲染完成之前
componentDidMount
渲染完成之後
所謂的渲染完成,即元件已經被渲染成為真實DOM並插入到了html之中。
- 元件屬性(我們前面提到的props與state)更新的前後時刻
componentWillReceiveProps
接收到一個新的props時,在重新render之前呼叫
shouldComponentUpdate
接收到一個新的state或者props時,在重新render之前呼叫
componentWillUpdate
接收到一個新的state或者props時,在重新render之前呼叫
componentDidUpdate
元件完成更新之後呼叫 - 元件取消掛載之前(取消之後就沒必要提供鉤子函式了)
componentWillUnmount
在學習之初你不用記住這些函式的具體名字,你只需要記住這三個大的時刻即可,第一次渲染完成前後,更新前後,取消之前。當你要使用時,再查具體對應的名字叫什麼即可。
而且根據我的經驗,初學之時,其實也不知道這些鉤子函式會有什麼用,會在什麼時候用,這需要我們在實踐中慢慢掌握,所以也不用著急。當我們上手寫了幾個稍微複雜的例子,自然會知道如何去使用他們。
所以這裡我只詳細介紹一下,我們最常用的一個生命週期建構函式,元件第一次渲染完成之後呼叫的componentDidMount
。
既然是元件第一次渲染完成之後才會呼叫,也就是說,該函式在react元件的生命週期中,只會呼叫一次。而渲染完成,則表示元件已經被渲染成為真實DOM插入了html中。所以這時候就可以通過ref獲取真實元素。記住它的特點,這會幫助我們正確的使用它。
修改helloWorld.jsx
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/helloWorld.jsx import React, { Component } from 'react'; class HelloWorld extends Component { clickHander = () => { console.log(this.refs) } // 這時已經可以獲取到真實DOM,而componentWillMount則不行 componentDidMount (props) { console.log(this.refs) } render () { return ( <div className="container" onClick={this.clickHander}> <div ref="hello" className="hello">Hello</div> <div ref="world" className="world">World</div> </div> ) } } export default HelloWorld; |
我們在實際開發中,常常需要通過ajax獲取資料,而資料請求的這個行為,則最適合放在componentDidMount
中來執行。
通常會在首次渲染改變元件狀態(state)的行為,或者稱之為有副作用的行為,都建議放在
componentDidMount
中來執行。主要是因為state的改動會引發元件的重新渲染。
元件之間的互動
作為react學習中的一個非常重要的點,元件之間的互動還是需要我們認真掌握的。這個時候hello world就滿足不了我們學習的慾望了,所以我們可以先把它給刪掉。
那麼元件之間的互動,大概可以分為如下兩種:
- 父元件與子元件之間互動
- 子元件與子元件之間互動
當然可能有的人會問,2個不相干的元件之間如何互動?如果,你的程式碼裡,出現了兩個不相干的元件還要互動,那說明你的元件劃分肯定是有問題的。這就是典型的給自己挖坑找事兒。即使確實有,那也是通過react-redux把他們變成子元件對吧。但是,通常情況下,不到萬不得已,並不建議使用react-redux,除非你的專案確實非常龐大了,需要管理的狀態非常多了,已經不得不使用,一定要記住,react-redux這類狀態管理器是最後的選擇。
我們來想想一個簡單常見的場景:頁面裡有一個submit提交按鈕,當我們點選提交後,按鈕前出現一個loading圖,並變為不可點選狀態,片刻之後,介面請求成功,飄出一個彈窗,告訴你,提交成功。大家可以想一想,這種場景,藉助react元件應該如何做?
首先可以很簡單的想到,將按鈕與彈窗分別劃分為兩個不同的元件:
。然後建立一個父元件來管理這兩個子元件
。
那麼在父元件中,我們需要考慮什麼因素?Button的loading圖是否展示,彈窗是否展示對吧。
OK,根據這些思考,我們開始來實現這個簡單的場景。
首先建立一個Button
元件。在src目錄下建立一個叫做Button.jsx
的檔案,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/Button.jsx import React from 'react'; const Button = (props) => { const { children, loading, submit } = props return ( <button onClick={submit} disabled={ loading ? 'disabled' : null }> { loading && <i className="loading"></i> } { children } </button> ) } export default Button; |
注意,當你引入了一個新建立的檔案時,可能需要重新啟動服務才會找得到新的元件
由於這裡的Button元件僅僅是簡單的展示,並無額外的邏輯需要處理,因此我們使用無狀態的元件。在這個元件裡,出現了一個新的知識點:children
1 2 3 4 5 6 7 8 9 |
// 假如我們這樣使用Button元件時 <Button>確認</Button> // 那麼標籤中間的確認二字就會放入props的children屬性中 // 無狀態元件中 props.children = '確認' // 有狀態元件中 this.props.children = '確認' |
當然,children還可以是更多的元素,這和我們熟知的DOM元素的children保持一致。
還有一個需要注意的知識點,則是在jsx模板中,我們可以使用JavaScript表示式來執行簡單的邏輯處理
我們可以列舉一些常見的表示式:
1 2 3 4 5 6 7 |
<div>{ message }</div> <Button disabled={ loading ? 'disabled' : null }></Button> { dialog && <Dialog /> } { pending ? <Aaaa /> : <Bbbb /> } |
如果對於JavaScript表示式瞭解不夠多的朋友,建議深入學習一下相關的知識。
理解了這些知識之後,相信對於上面的Button元件所涉及到的東西也就能夠非常清楚知道是怎麼回事了。接下來,我們需要建立一個彈窗元件,Dialog.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/Dialog.jsx import React, { Component } from 'react'; const Dialog = (props) => { const { message, close } = props; return ( <div className="dialog-backdrop"> <div className="dialog-container"> <div className="dialog-header">提示</div> <div className="dialog-body">{ message }</div> <div className="dialog-footer"> <button className="btn" onClick={ close }>確定</button> </div> </div> </div> ) } export default Dialog; |
這個元件沒有太多特別的東西,唯一需要關注的一點是,我們也可以通過props傳遞一個函式給子元件。例如這裡的close方法。該方法在父元件中定義,但是卻在子元件Dialog中執行,他的作用是關閉彈窗。
我們很容易知道父元件想要修改子元件,只需要通過改變傳入的props屬性即可。那麼子元件想要修改父元件的狀態呢?正是父元件通過向子元件傳遞一個函式的方式來改變。
該函式在父元件中定義,在子元件中執行。而函式的執行內容,則是修改父元件的狀態。這就是close的原理,我們來看看父元件中是如何處理這些邏輯的。
建立一個父元件App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
// src/App.jsx import React, { Component } from 'react'; import Button from './Button'; import Dialog from './Dialog'; import './style.css'; class App extends Component { state = { loading: false, dialog: false, message: 'xxx' } submit = () => { this.setState({ loading: true }) // 模擬資料請求的過程,假設資料請求會經歷1s得到結果 setTimeout(() => { // 通過隨機數的方式模擬可能出現的成功與失敗兩種結果 const res = Math.random(1); if (res > 0.5) { this.setState({ dialog: true, message: '提交成功!' }) } else { this.setState({ dialog: true, message: '提交失敗!' }) } this.setState({ loading: false }) }, 1000) } close = () => { this.setState({ dialog: false }) } render () { const { loading, dialog, message } = this.state; return ( <div className="app-wrap"> <Button loading={ loading } submit={ this.submit }>提交</Button> { dialog && <Dialog message={ message } close={ this.close } /> } </div> ) } } export default App; |
App元件的state中,loading用於判斷Button按鈕是否顯示loading圖示,dialog用於判斷是否需要顯示彈窗,message則是表示彈窗的提示內容。
我們自定義的鉤子函式submit
和close
則分別是與子元件Button與Dialog互動的一個橋樑。前面我們說過了,想要在子元件中改變父級的狀態,就需要通過在父元件中建立鉤子函式,並傳遞給子元件執行的方式來完成。
在App.jsx中我們還看到程式碼中引入了一個css檔案。這是構建工具幫助我們整合的方式,我們可以直接將css檔案當做一個單獨的模組引入進來。我們還可以通過同樣的方式引入圖片等資源。
style.css也是在src目錄下建立的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
// src/style.scss button { background: none; border: none; outline: none; width: 100px; height: 30px; border: 1px solid orange; border-radius: 4px; font-size: 16px; display: block; margin: 20px auto; } .loading { display: inline-block; width: 10px; height: 10px; border: 2px solid #ccc; border-radius: 10px; margin-right: 10px; border-bottom: transparent; border-top: transparent; animation-name: loading; animation-duration: 1s; animation-timing-function: linear; animation-iteration-count: infinite; } .dialog-backdrop { background: rgba(0, 0, 0, 0.2); position: fixed; top: 0; left: 0; right: 0; bottom: 0; } .dialog-container { width: 300px; background: #FFFFFF; border-radius: 4px; position: absolute; top: 20%; left: 50%; transform: translate(-50%, -50%); padding: 10px; } .dialog-header { height: 20px; text-align: center; line-height: 20px; } .dialog-body { line-height: 1.6; text-align: center; margin-top: 20px; } .dialog-footer { margin-top: 20px; } .dialog-footer button { margin: 0 auto; border: none; background: orange; color: #fff; } @keyframes loading { from { transform: rotate(0); } to { transform: rotate(360deg); } } |
最後修改index.js,即可將程式執行起來。
1 2 3 4 5 6 7 8 |
// src/index.js import React from 'react'; import { render } from 'react-dom'; import App from './App'; const root = document.querySelector('#root'); render(, root); |
那麼總結一下元件之間的互動。
父元件改變子元件,通過改變傳入的props屬性值即可。
而子元件改變父元件的狀態,則需要在父元件中建立鉤子函式,然後讓鉤子函式通過props傳遞給子元件,並在子元件中執行。
那麼子元件與子元件之間的互動方式,也就是通過影響共同的父元件來進行互動的。正如我們這個例子中的點選按鈕,出現彈窗一樣。這就是react元件之間互動的核心。
非同步元件
在學習非同步元件之前,可能還需要大家去折騰一下如何禁用瀏覽器的跨域限制。禁用跨域限制可以讓我們使用更多的公共api進行學習,但是很多人並不知道還可以這樣玩。總之一句話,知道了如何禁用瀏覽器的跨域限制,會讓你的學習速度提升很多,很多專案你就可以動手自己嘗試了。
我這裡只能提供在mac環境下如何禁用chrome瀏覽器的跨域限制。在命令列工具中輸入以下指令啟動chrome即可。
1 |
> open -a "Google Chrome" --args --disable-web-security --user-data-dir |
在safari瀏覽器中則更加簡單。
windows環境下如何做需要大家自己去研究。
OK,禁用跨域限制以後,我們就可以自如的請求別人的介面。這個時候再來學習非同步元件就能輕鬆很多。
非同步元件並不是那麼複雜,由於介面請求會經歷一點時間,因此在元件第一次渲染的時候,並不能直接將我們想要的資料渲染完成,那麼就得再介面請求成功之後,重新渲染一次元件。上面的知識已經告訴大家,通過使用this.setState
修改state的值可以達到重新渲染的目的。
所以我們通常的做法就是在介面請求成功之後,使用this.setState
。
為了降低學習難度,我們暫時先使用jquery中提供的方法來請求資料。
目前比較常用的是axios
首先在我們的專案中,安裝jquery庫。我們通常都會使用這樣的方式來安裝新的元件和庫。
1 |
> npm install jquery |
然後在src目錄下建立一個News.jsx,藉助知乎日報的api,我們來嘗試完成一個簡單的非同步元件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// src/News.jsx import React, { Component } from 'react'; import $ from 'jquery'; class News extends Component { state = { stories: [], topStories: [] } componentDidMount() { $.get('http://news-at.zhihu.com/api/4/news/latest').then(resp => { console.log(resp); this.setState({ stories: resp.stories, topStories: resp.top_stories }) }) } render() { const { stories, topStories } = this.state; // 觀察每一次render資料的變化 console.log(this.state); return ( <div className="latest-news"> <section className="part1"> <div className="title">最熱</div> <div className="container"> { topStories.map((item, i) => ( <div className="item-box" key={i}> <img src={ item.image } alt=""/> <div className="sub-title">{ item.title }</div> </div> )) } </div> </section> <section className="part2"> <div className="title">熱門</div> <div className="container"> { stories.map((item, i) => ( <div className="item-box" key={i}> <img src={ item.images[0] } alt=""/> <div className="sub-title">{ item.title }</div> </div> )) } </div> </section> </div> ) } } export default News; |
在style.css
中簡單補上相關的css樣式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// src/style.css .latest-news { width: 780px; margin: 20px auto; } .latest-news section { margin-bottom: 20px; } .latest-news .title { height: 40px; line-height: 40px; font-size: 16px; padding: 0 10px; } .latest-news .container { display: flex; flex-wrap: wrap; justify-content: space-around; } .latest-news .item-box { width: 30%; overflow: hidden; margin-bottom: 20px; } .latest-news .item-box img { width: 100%; height: 200px; } .latest-news .item-box .sub-title { font-size: 12px; line-height: 1.6; margin-top: 10px; } |
並在App.jsx中引入使用即可。
1 2 3 4 5 |
// src/App.jsx import News from './News'; // 將下面這一句放於render函式的jsx模板中即可 |
這個元件除了獲取資料,沒有額外的邏輯處理,但仍然有幾個需要非常注意的地方。
1、 若非特殊情況,儘量保證資料請求的操作在componentDidMount
中完成。
2、 react中的列表渲染通常通過呼叫陣列的原生方法map方法來完成,具體使用方式可參考上例。
3、為了確保效能,被渲染的每一列都需要給他配置一個唯一的標識,正入上慄中的key={i}
。我們來假想一個場景,如果我們在陣列的最前面新增一條資料,如果沒有唯一的標識,那麼所有的資料都會被重新渲染,一旦資料量過大,這會造成嚴重的效能消耗。唯一標識會告訴react,這些資料已經存在了,你只需要渲染新增的那一條就可以了。
4、如果你想要深入瞭解該元件的具體變化,你可以在render方法中,通過console.log(this.state)
的方式,觀察在整個過程中,元件渲染了多少次,已經每一次this.state
中的具體值是什麼,是如何變化的。
高階元件
很多人寫文章喜歡把問題複雜化,因此當我學習高階元件的時候,查閱到的很多文章都給人一種高階元件高深莫測的感覺。但是事實上卻未必。我們常常有一些口頭俗語,比如說“包一層”就是可以用來簡單解釋高階元件的。在普通元件外面包一層邏輯,就是高階元件。
在進一步學習高階元件之前,我們來回顧一下new與建構函式之間的關係。在前面我有文章提到過為什麼建構函式中this在執行時會指向new出來的例項,不知道還有沒有人記得。我將那段程式碼複製過來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// 先一本正經的建立一個建構函式,其實該函式與普通函式並無區別 var Person = function(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; } } // 將建構函式以引數形式傳入 function New(func) { // 宣告一箇中間物件,該物件為最終返回的例項 var res = {}; if (func.prototype !== null) { // 將例項的原型指向建構函式的原型 res.__proto__ = func.prototype; } // ret為建構函式執行的結果,這裡通過apply,將建構函式內部的this指向修改為指向res,即為例項物件 var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); // 當我們在建構函式中明確指定了返回物件時,那麼new的執行結果就是該返回物件 if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } // 如果沒有明確指定返回物件,則預設返回res,這個res就是例項物件 return res; } // 通過new宣告建立例項,這裡的p1,實際接收的正是new中返回的res var p1 = New(Person, 'tom', 20); console.log(p1.getName()); // 當然,這裡也可以判斷出例項的型別了 console.log(p1 instanceof Person); // true |
在上面的例子中,首先我們定義了一個本質上與普通函式沒區別的建構函式,然後將該建構函式作為引數傳入New函式中。我在New函式中進行了一些的邏輯處理,讓New函式的返回值為一個例項,正因為New的內部邏輯,讓建構函式中的this能夠指向返回的例項。這個例子就是一個“包一層”的案例。
再來看一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React, { Component } from 'react'; class Div extends Component { componentDidMount() { console.log('這是新增的能力'); } render () { return ( <div>{ this.props.children }</div> ) } } export default Div; |
在上面的例子中,我們把html的DIV標籤作為基礎元件。對他新增了一個輸出一條提示資訊的能力。而新的Div元件,就可以理解為div標籤的高階元件。所以到這裡希望大家已經理解了包一層的具體含義。
react元件的高階元件,就是在基礎react元件外面包一層,給該基礎元件賦予新的能力。
OK,我們來試試定義第一個高階元件,該高階元件的第一個能力,就是向基礎元件中傳入一個props引數。
在例子中,傳入的引數可能沒有任何實際意義,但是在實際開發中,我們可以傳入非常有必要的引數來簡化我們的程式碼和邏輯。
先來定義一個擁有上述能力的高階元件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/Addsss.jsx import React from 'react'; // 定義一個接受一個react元件作為引數的函式 function Addsss(Container) { // 該函式返回一個新的元件,我們可以在該元件中進行新能力的附加 return class Asss extends React.Component { componentDidMount() {} render() { return ( { this.props.children } ) } } } export default Addsss; |
儘管這個高階組價足夠簡單,但是他已經呈現了高階元件的定義方式。現在我們在一個基礎元件中來使用該高階元件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/basic.jsx import React, { Component } from 'react'; import Addsss from './Addsss'; class Basic extends Component { componentDidMount() { console.log(this.props.name); } render() { return ( <div className={this.props.name}>{ this.props.children }</div> ) } } export default Addsss(Basic); |
我們看到其實在基礎元件中,對外丟擲的介面是Addsss(Basic)
,這是高階元件裡定義的函式執行的結果。也就是說,其實基礎元件中返回的是高階元件中定義的Asss中間元件。這和new的思路幾乎完全一致。
當然,想要理解,並熟練使用高階元件並不是一件容易的事情,大家初學時也不用非要完全掌握他。當你對react慢慢熟練之後,你可以嘗試使用高階元件讓自己的程式碼更加靈活與簡練。這正是向函數語言程式設計思維轉變的一個過程。
在進步學習的過程中,你會發現無論是路由元件react-router,或者react-redux都會使用高階元件來實現一些功能。只要你遇到他們的時候,你能明白,哦,原來是這麼回事兒就行了。
react路由
react提供了react-router元件來幫助我們實現路由功能。
但是react-router是一個不太好講的知識點。因為由於react-router 4進行了顛覆性的更新,導致了react-router 3與react-router 4的使用方式大不一樣。也正是由於變化太大,所以很多專案仍然正在使用react-router3,並且沒有過渡到react-router4的打算。
因此這裡我就不多講,提供一些參考學習資料。
- react-router 3: react-router 3 使用教程
- react-router 4: react-router 4 全攻略
未完待續
由於時間關係,暫時就只能寫到這裡了。
本來還寫了一個比較完整的例子也在這篇文章裡逐步分析如何實現的,但是時間確實不夠。所以如果覺得看了上面的知識還想進一步學習的話,可以先去https://github.com/yangbo5207/advance15 看看這個完整例子的樣子。
另外我曾經寫了一篇如何快速掌握一門前端框架,希望大家可以參考參考。
按照我的計劃,只要理解了上面我所提到的知識,並把我準備的這個完整例子理解了。那麼你的react掌握程度也算是小有所成了。至少應屆畢業生找工作能提到這些思維方式應該會很有幫助。