本文將會從零開始介紹 React 的核心知識點,以下是參考大綱~
- React
- React 是什麼
- 為什麼要使用 React
- 專案預覽
- JSX
- 樣式
- 元件
- props
- 類元件
- State
- 事件處理
- 生命週期
- 更多
- 腳手架
- 狀態管理與路由
話不多說,直接進入~
React#
React 是什麼#
官方定義:一個用來構建用戶界面的 JavaScript 庫
從定義中我們要有一個認知: React 本身所做的只是構建用戶界面,而大型的 React 專案一般都會緊密結合它的生態圈 (路由: React-Router 狀態管理庫: Redux 等等) 來實現,這篇文章主要專注的還是 React 的核心知識點
為什麼要使用 React#
- 虛擬 DOM: 我們都知道 js 頻繁操作 dom 的成本是非常昂貴的,而 React 首創的 virtual dom 實現了在 js 層面來操作 dom,極大地提高了應用的效率
- 可重用元件: React 的流行帶動了元件化的思想,元件化的核心就是在於可重用性,相比於傳統的 Web 開發模式也更容易維護,很好地提高了開發效率
- 由 Facebook 維護: React 背靠 Facebook 這座大山,其身後有許多優秀的開發者在維護迭代,同時社區也十分的活躍,開發遇到的大部分問題很快都可以得到解決
- 現實:最後一點就是國內的現狀,大廠的技術棧基本都是基於 React 的,所以向公司 (qian) 看齊的話 React 也是必不可少的技能
專案預覽#
先看一下最終的專案效果,可以思考一下利用原生 js 如何實現?
初始的空模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html>
JSX#
我們首先來看 React 是如何構建界面即渲染元素的:
- React 的語法為 JSX, 即混合了 JavaScript 和 HTML 的語法
這裡我們採用外鏈引入的方式加入 React 專案最基本的兩個鏈接:
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> // react 核心
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> // react-dom 瀏覽器(dom)的渲染
同時為了直接能夠使用 JSX 的語法,我們還需要引入 babel 的外鏈:
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
// script 標註 type
<script type="text/babel">
// coding....
</script>
然後敲上第一行 React 代碼:
ReactDOM.render(<h1>Hello React!</h1>, document.getElementById('app'));
刷新瀏覽器:
ReactDOM
對象來自 react-dom
, 這裡我們調用了它的 render
方法,第一個參數是要渲染的元素,第二個是實際的 DOM 對象,這樣就成功的將 JSX 元素渲染到了頁面上
我們可以看到 JSX 語法實際上跟 HTML 還是很像的,那麼 CSS 呢?
樣式#
內聯樣式:
ReactDOM.render(<h1 style={{backgroundColor: 'lightblue'}}>Hello React!</h1>, document.getElementById('app'))
外部樣式:
const h1Style = {
backgroundColor: 'lightblue'
}
ReactDOM.render(<h1 style={h1Style}>Hello React!</h1>, document.getElementById('app'))
PS: 如果是使用了 css 類選擇器,那麼 JSX 中的寫法是 className (為了與 ES6 的 class
區分)
目前學會這幾種寫法就足夠了
元件#
我們剛剛在 render
方法中直接寫的 JSX , 當你的 JSX 元素變得複雜起來就需要單獨定義一個 Component
, 我們先來看看 無狀態元件 的寫法:
function App () {
return (
<div>
<h1>Hello React!</h1>
<p>react is so awesome!</p>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('app'))
刷新瀏覽器:
這裡需要注意的是返回的元件必須只由一個最大的標籤來包含
接下來讓我們敲一些有意思的:
function App () {
const books = ['dataBase', 'data structure', 'computer network']
return (
<div>
<h3>My books: </h3>
<ul>
{books.map(book =>
<li>{book}</li>
)}
</ul>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('app'))
我們定義一個 books
陣列,然後在函數元件內部使用 ES6 的 map
方法循環渲染出對應的 book
元素
刷新瀏覽器:
首先我們可以看到界面上出現了三個 li
, 但是更顯眼的是控制台出現了顯眼的報錯,這個報錯提示很重要,意即每一個循環出的元素都需要有一個 key
, 這樣的話 React 就能在列表變化時識別其中成員的添加 、更改和刪除的操作 (diff 算法), 會有更好的性能,因此這裡我們使用 map
的第二個參數來加上對應的 key
:
function App () {
const books = ['dataBase', 'data structure', 'computer network']
return (
<div>
<h3>My books: </h3>
<ul>
{books.map((book, i) =>
<li key={i}>{book}</li>
)}
</ul>
</div>
)
}
刷新控制台不再報錯
這裡我們也可以發現,App
元件內部的循環列表更適合抽出來單獨做一個列表元件以實現更好的重用性:
function BookList () {
return (
<ul>
{books.map((book, i) =>
<li key={i}>{book}</li>
)}
</ul>
)
}
function App () {
const books = ['dataBase', 'data structure', 'computer network']
return (
<div>
<h3>My books: </h3>
<BookList />
</div>
)
}
但是很明顯我們又發現了另一個問題,books
陣列是定義在 App
元件內部的,bookList
元件如何獲取到它的值?
props#
上面問題即是父子元件如何傳遞值?很直接的想法,我們可以在父元件內部放置子元件時傳入一些自定義的參數:
function App () {
const books = ['dataBase', 'data structure', 'computer network']
return (
<div>
<h3>My books: </h3>
<BookList list={books} />
</div>
)
}
然後我們在 BookList
子元件內捕捉到傳下來的參數:
function BookList (props) {
console.log('props: ', props)
const books = props.list
return (
<ul>
{books.map((book, i) =>
<li key={i}>{book}</li>
)}
</ul>
)
}
刷新瀏覽器:
OK! 這就是 props
, 我們同時在控制台打印了這個對象,從這就可以看出數據在不同元件間傳遞的方式.
現在來思考一個新問題:目前的數據只是默默地在傳遞,不同元件只是單純地把它顯示出來,如果我們需要添加或者刪除這些數據該如何操作,React 又如何獲知這些數據被更改了並及時更新 UI 呢?
類元件#
讓我們來認識 React 自身給我們提供的另一種元件 --- 類元件
類元件的來源於 ES6 中 的class
, 這裡我們看一下將 App
元件改寫成類元件的寫法:
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
books: ['dataBase', 'data structure', 'computer network']
}
}
render () {
return (
<div>
<h3>My books: </h3>
<BookList list={this.state.books} />
</div>
)
}
}
React.Component
是 React 自帶的通用類,它封裝了所有 React 類需要的實現細節,類元件都是通過繼承它來實現,通過重寫 render
方法來定義返回的元件元素
State#
我們可以看到原來的 books
陣列放到了 constructor
構造函數中作為該類元件的內部狀態來使用:
this.state = {
books: ['dataBase', 'data structure', 'computer network']
}
state
通過使用 this
綁定在類上,我們可以在整個元件內訪問到 state
, 每次修改元件的 state
, 元件的 render
方法會再次運行即元件重新渲染,那我們可以直接修改 state
嗎?
React 有兩個重要的原則:一個是單向數據流,另一個是明確的狀態改變。我們唯一改變 state
的方式是通過 setState()
元件在 render
中獲取最新 state
的信息進行渲染,在 View
層通過調用 setState
來更新 state
, 然後元件再次運行 render
方法並更新界面.
我們來嘗試一下:
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
books: ['database', 'data structure', 'computer network']
}
}
render () {
return (
<div>
<h3>My books: </h3>
<BookList list={this.state.books} />
<button onClick={() => this.setState({ books: ['Compilation principle', 'operating system'] })}>Change</button>
</div>
)
}
}
事件處理#
當 setState
關聯的邏輯複雜起來以後,包括我們需要在不同元件間調用 setState
時,從重用性與維護性角度上來說,我們都需要將事件處理抽離成自定義的函數來調用,React 中推薦事件處理函數的前綴都為 handle
, 監聽函數的前綴都為 on
:
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
books: ['database', 'data structure', 'computer network'],
input: ''
}
this.handleAddBook = this.handleAddBook.bind(this)
this.handleRemoveBook = this.handleRemoveBook.bind(this)
this.updateInput = this.updateInput.bind(this)
}
handleAddBook () {
this.setState(currentState => {
return {
books: currentState.books.concat([this.state.input])
}
})
}
handleRemoveBook (name) {
this.setState(currentState => {
return {
books: currentState.books.filter(book => book !== name)
}
})
}
updateInput (e) {
this.setState({
input: e.target.value
})
}
render () {
return (
<div>
<h3>My books: </h3>
<input
type="text"
placeholder="new book"
value={this.state.input}
onChange={this.updateInput}
/>
<button onClick={this.handleAddBook}>Add</button>
<BookList
list={this.state.books}
onRemoveBook={this.handleRemoveBook}
/>
</div>
)
}
}
handleAddBook
和 handleRemoveBook
為新增和修改的操作,這裡還需要特別強調的是構造函數中的這三行代碼:
this.handleAddBook = this.handleAddBook.bind(this)
this.handleRemoveBook = this.handleRemoveBook.bind(this)
this.updateInput = this.updateInput.bind(this)
當我們想在自定義的類方法中調用 this.setState
時,這裡的 this
是 undefined
, 所以為了類元件的 this
在類方法中可以訪問,我們需要將 this
綁定到類方法上,而放在構造函數裡面的話綁定只會在元件實例化時運行一次,性能消耗更少.
OK! 事實上到這裡我們已經可以基本完成專案預覽所呈現的內容了,現在請你試著做更多的改進以達到下面的效果:
如果你已經完成,可以參考以下的代碼:
function ActiveBooks (props) {
return (
<div>
<h2>Reading Books</h2>
<ul>
{props.list.map((book, i) => (
<li key={i}>
<span>{book.name}</span>
<button onClick={() => props.onRemoveBook(book.name)}>Remove</button>
<button onClick={() => props.onDeactive(book.name)}>Readed</button>
</li>
))}
</ul>
</div>
)
}
function InactiveBooks (props) {
return (
<div>
<h2>Readed Books</h2>
<ul>
{props.list.map((book, i) => (
<li key={i}>
<span>{book.name}</span>
<button onClick={() => props.onActive(book.name)}>Reading</button>
</li>
))}
</ul>
</div>
)
}
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
books: [
{
name: 'database',
active: true
},
{
name: 'data structure',
active: true
},
{
name: 'computer network',
active: true
}],
input: ''
}
this.handleAddBook = this.handleAddBook.bind(this)
this.handleRemoveBook = this.handleRemoveBook.bind(this)
this.handleToggleBook = this.handleToggleBook.bind(this)
this.updateInput = this.updateInput.bind(this)
}
handleAddBook () {
this.setState(currentState => {
return {
books: currentState.books.concat([{
name: this.state.input,
active: true
}]),
input: ''
}
})
}
handleRemoveBook (name) {
this.setState(currentState => {
return {
books: currentState.books.filter(book => book.name !== name)
}
})
}
handleToggleBook (name) {
this.setState(currentState => {
const book = currentState.books.find(book => book.name === name)
return {
books: currentState.books.filter(book => book.name !== name)
.concat([{
name,
active: !book.active
}])
}
})
}
updateInput (e) {
this.setState({
input: e.target.value
})
}
render () {
return (
<div>
<h3>My books: </h3>
<input
type="text"
placeholder="new book"
value={this.state.input}
onChange={this.updateInput}
/>
<button onClick={this.handleAddBook}>Add</button>
<button onClick={() => this.setState({ books: [] })}> Clear All </button>
<ActiveBooks
list={this.state.books.filter(book => book.active)}
onRemoveBook={this.handleRemoveBook}
onDeactive={this.handleToggleBook}
/>
<InactiveBooks
list={this.state.books.filter(book => !book.active)}
onActive={this.handleToggleBook}
/>
</div>
)
}
}
走到這裡,你已經可以自己再寫幾個小 demo 熟悉一下了,那麼讓我們再來思考最後一個問題:
- 專案中很多時候的數據都是要與後台交互的,也就是會有異步的操作, 在數據還未請求到時我們希望顯示加載樣式,請求到以後再更新界面,這樣的邏輯應該放在哪裡?
生命週期#
對於上面問題,我們實際希望的是當元件被掛載到 DOM 上以後再來渲染界面,同時對於有很多元件的應用,當元件銷毀時,我們也需要釋放它所佔用的資源,這就是 React 生命週期 當中很重要的兩個函數: componentDidMount
和 componentWillUnmout
讓我們整體感受一下生命週期函數執行的過程:
class App extends React.Component {
constructor (props) {
......
console.log('--constructor--')
}
componentDidMount () {
console.log('--componentDidMount--')
}
componentDidUpdate () {
console.log('--componentDidUpdate--')
}
componentWillUnmout () {
console.log('--componentWillUnmout--')
}
......
render () {
console.log('--render--')
return (
......
)
}
}
我們可以看出,元件整個的生命週期是從 constructor
--> render
--> componentDidMount
, 然後元件更新再次 render
--> componentDidUpdate
, 元件銷毀前則會調用 componentWillUnmout
接下來我們將會深入使用這幾個函數:
讓我們先手動模擬一個 API:
window.API = {
fetchBooks () {
return new Promise((res, rej) => {
const books = [
{
name: 'database',
active: true
},
{
name: 'data structure',
active: true
},
{
name: 'computer network',
active: false
}
]
setTimeout(() => res(books), 2000)
})
}
}
然後在 componentDidMount
函數中調用它:
componentDidMount () {
console.log('--componentDidMount--')
API.fetchBooks()
.then(books => {
this.setState({
books
})
})
}
我們可以看到在 componentDidMount
之後再去請求數據,然後 render
重新渲染再執行了 componentDidUpdate
讓我們再來提升一下用戶體驗加上 Loading 的邏輯:
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
books: [],
loading: true,
input: ''
}
......
console.log('--constructor--')
}
componentDidMount () {
console.log('--componentDidMount--')
API.fetchBooks()
.then(books => {
this.setState({
books,
loading: false
})
})
}
componentDidUpdate () {
console.log('--componentDidUpdate--')
}
componentWillUnmout () {
console.log('--componentWillUnmout--')
}
......
render () {
console.log('--render--')
if (this.state.loading === true) {
return <h2>Loading...</h2>
}
return (
......
)
}
}
OK! 現在我們整個的 React 入門歷程已經結束了,當然並沒有完全實現預覽的效果,鼓勵你進一步獨立封裝一個 Loading
元件,最後讓我們簡單談一下更進一步的開發操作
更多#
腳手架#
我們的入門教程是用傳統的外鏈引入方式來使用 React 的,並且為了使用 JSX 我們還需要再引入 babel , 現代化的 Web 開發流程都是基於 Webpack 的模塊化構建與部署過程,對於實際成型的專案來說,一般都推薦使用官方的腳手架 create-react-app 來一步構建,簡化依賴安裝與環境部署的流程,更多地專注在代碼邏輯的編寫上
狀態管理與路由#
還記得 React 的定義嗎?它只是專注在用戶界面的構建上面,雖然我們通過類元件可以管理一定的內部狀態,但是當專案複雜到一定程度以後,避免不了是要引入外部的狀態管理庫,這裡推薦使用跟 React 理念相合的 Redux ; 目前的單頁面應用都需要用到路由管理,推薦使用 React-Router
最後我想說,前端的技術表面是發展得很快的,但是內部的原理基本都是萬變不離其宗,React 帶來的是一種新的變革的開發方式,希望你以此為起點,結合 React 的設計理念去深入它更多的特性.