學習 React 比你想象的更簡單


Webpack

webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關系圖(dependency graph),其中包含應用程序需要的每個模塊,然后將所有這些模塊打包成一個或多個 bundle

其它相似打包工具還有rollup.jsparcelFIS

按照webpack的指南 (注意是指南不是概念不是api) 進行針對性的講解即可,需要被充一下工程化的知識

工程化

這個部分的內容可以做為一些思想給學員講解,不需要學員掌握。可以理解為擴展的內容

什么是JS項目工程化

  • 版本控制
  • 自動化持續繼承、持續交付(CI/CD)
  • 代碼質量控制(QA)
  • 工具
  • 模塊化
  • 文檔
  • demo

編譯過程

自動化處理每次push, tag, release的任務隊列

  1. 安裝
    • 安裝 : npm命令行工具
    • 安全審計:npm audit
  2. Lint
    • 格式檢查: eslint/stylelint
    • 格式化: prettier
  3. 測試
    • 測試套裝: jest / mocha / ava / kamar
    • 代碼覆蓋量: nyc / codecov / coveralls
  4. 構建
    • 轉換器: babel / TS / flow
    • 預處理器: sass / less / postcss
    • 代碼混淆: uglify-js / terser
    • 打包及tree shaking: webpack / rollup / parcel
    • 壓縮(gzip等)
    • 復制 / 刪除 / 移動文件
    • 檢查打包文件的大小
    • 移除無用的代碼
  5. push
    • 交付: git
    • 發布: npm
  6. 部署
    • 服務器
      • Pages: git pages
      • 雲服務器: aliyun / qcloud / aws
  7. Story Book

create-react-app

全局安裝create-react-app

$ npm install -g create-react-app

創建一個項目

$ create-react-app your-app 注意命名方式

Creating a new React app in /dir/your-app.

Installing packages. This might take a couple of minutes. 安裝過程較慢,可以推薦學員使用yarn
Installing react, react-dom, and react-scripts... 

如果不想全局安裝,可以直接使用npx

$ npx create-react-app your-app	也可以實現相同的效果

這需要等待一段時間,這個過程實際上會安裝三個東西

  • react: react的頂級庫
  • react-dom: 因為react有很多的運行環境,比如app端的react-native, 我們要在web上運行就使用react-dom
  • react-scripts: 包含運行和打包react應用程序的所有腳本及配置

出現下面的界面,表示創建項目成功:

Success! Created your-app at /dir/your-app
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd your-app
  npm start

Happy hacking!

根據上面的提示,通過cd your-app命令進入目錄並運行npm start即可運行項目。

生成項目的目錄結構如下:

├── README.md							使用方法的文檔
├── node_modules					所有的依賴安裝的目錄
├── package-lock.json			鎖定安裝時的包的版本號,保證團隊的依賴能保證一致。
├── package.json					
├── public								靜態公共目錄
└── src										開發用的源代碼目錄

常見問題:

  • npm安裝失敗
    • 切換為npm鏡像為淘寶鏡像
    • 使用yarn,如果本來使用yarn還要失敗,還得把yarn的源切換到國內
    • 如果還沒有辦法解決,請刪除node_modules及package-lock.json然后重新執行npm install命令
    • 再不能解決就刪除node_modules及package-lock.json的同時清除npm緩存npm cache clean --force之后再執行npm install命令

關於React

React部分的內容包含了所有授課的思路

React的起源和發展

React 起源於 Facebook 的內部項目,因為該公司對市場上所有 JavaScript MVC 框架,都不滿意,就決定自己寫一套,用來架設Instagram 的網站。做出來以后,發現這套東西很好用,就在2013年5月開源了。

React與傳統MVC的關系

輕量級的視圖層A JavaScript library for building user interfaces

React不是一個完整的MVC框架,最多可以認為是MVC中的V(View),甚至React並不非常認可MVC開發模式;React 構建頁面 UI 的庫。可以簡單地理解為,React 將將界面分成了各個獨立的小塊,每一個塊就是組件,這些組件之間可以組合、嵌套,就成了我們的頁面。

React高性能的體現:虛擬DOM

React高性能的原理:

在Web開發中我們總需要將變化的數據實時反應到UI上,這時就需要對DOM進行操作。而復雜或頻繁的DOM操作通常是性能瓶頸產生的原因(如何進行高性能的復雜DOM操作通常是衡量一個前端開發人員技能的重要指標)。

React為此引入了虛擬DOM(Virtual DOM)的機制:在瀏覽器端用Javascript實現了一套DOM API。基於React進行開發時所有的DOM構造都是通過虛擬DOM進行,每當數據變化時,React都會重新構建整個DOM樹,然后React將當前整個DOM樹和上一次的DOM樹進行對比,得到DOM結構的區別,然后僅僅將需要變化的部分進行實際的瀏覽器DOM更新。而且React能夠批處理虛擬DOM的刷新,在一個事件循環(Event Loop)內的兩次數據變化會被合並,例如你連續的先將節點內容從A-B,B-A,React會認為A變成B,然后又從B變成A UI不發生任何變化,而如果通過手動控制,這種邏輯通常是極其復雜的。

盡管每一次都需要構造完整的虛擬DOM樹,但是因為虛擬DOM是內存數據,性能是極高的,部而對實際DOM進行操作的僅僅是Diff分,因而能達到提高性能的目的。這樣,在保證性能的同時,開發者將不再需要關注某個數據的變化如何更新到一個或多個具體的DOM元素,而只需要關心在任意一個數據狀態下,整個界面是如何Render的。

React Fiber:

在react 16之后發布的一種react 核心算法,React Fiber是對核心算法的一次重新實現(官網說法)。之前用的是diff算法。

在之前React中,更新過程是同步的,這可能會導致性能問題。

當React決定要加載或者更新組件樹時,會做很多事,比如調用各個組件的生命周期函數,計算和比對Virtual DOM,最后更新DOM樹,這整個過程是同步進行的,也就是說只要一個加載或者更新過程開始,中途不會中斷。因為JavaScript單線程的特點,如果組件樹很大的時候,每個同步任務耗時太長,就會出現卡頓。

React Fiber的方法其實很簡單——分片。把一個耗時長的任務分成很多小片,每一個小片的運行時間很短,雖然總時間依然很長,但是在每個小片執行完之后,都給其他任務一個執行的機會,這樣唯一的線程就不會被獨占,其他任務依然有運行的機會。

React的特點和優勢

  1. 虛擬DOM

我們以前操作dom的方式是通過document.getElementById()的方式,這樣的過程實際上是先去讀取html的dom結構,將結構轉換成變量,再進行操作

而reactjs定義了一套變量形式的dom模型,一切操作和換算直接在變量中,這樣減少了操作真實dom,性能真實相當的高,和主流MVC框架有本質的區別,並不和dom打交道

  1. 組件系統

react最核心的思想是將頁面中任何一個區域或者元素都可以看做一個組件 component

那么什么是組件呢?

組件指的就是同時包含了html、css、js、image元素的聚合體

使用react開發的核心就是將頁面拆分成若干個組件,並且react一個組件中同時耦合了css、js、image,這種模式整個顛覆了過去的傳統的方式

  1. 單向數據流

其實reactjs的核心內容就是數據綁定,所謂數據綁定指的是只要將一些服務端的數據和前端頁面綁定好,開發者只關注實現業務就行了

  1. JSX 語法

在vue中,我們使用render函數來構建組件的dom結構性能較高,因為省去了查找和編譯模板的過程,但是在render中利用createElement創建結構的時候代碼可讀性較低,較為復雜,此時可以利用jsx語法來在render中創建dom,解決這個問題,但是前提是需要使用工具來編譯jsx

編寫第一個react應用程序

react開發需要引入多個依賴文件:react.js、react-dom.js,分別又有開發版本和生產版本,create-react-app里已經幫我們把這些東西都安裝好了。把通過CRA創建的工程目錄下的src目錄清空,然后在里面重新創建一個index.js. 寫入以下代碼:

// 從 react 的包當中引入了 React。只要你要寫 React.js 組件就必須引入React, 因為react里有一種語法叫JSX,稍后會講到JSX,要寫JSX,就必須引入React
import React from 'react'
// ReactDOM 可以幫助我們把 React 組件渲染到頁面上去,沒有其它的作用了。它是從 react-dom 中引入的,而不是從 react 引入。
import ReactDOM from 'react-dom'

// ReactDOM里有一個render方法,功能就是把組件渲染並且構造 DOM 樹,然后插入到頁面上某個特定的元素上
ReactDOM.render(
// 這里就比較奇怪了,它並不是一個字符串,看起來像是純 HTML 代碼寫在 JavaScript 代碼里面。語法錯誤嗎?這並不是合法的 JavaScript 代碼, “在 JavaScript 寫的標簽的”語法叫 JSX- JavaScript XML。
  <h1>歡迎進入React的世界</h1>,
// 渲染到哪里
  document.getElementById('root')
)

元素與組件

如果代碼多了之后,不可能一直在render方法里寫,所以就需要把里面的代碼提出來,定義一個變量,像這樣:

import React from 'react'
import ReactDOM from 'react-dom'
// 這里感覺又不習慣了?這是在用JSX定義一下react元素
const app = <h1>歡迎進入React的世界</h1>
ReactDOM.render(
  app,
  document.getElementById('root')
)

函數式組件

由於元素沒有辦法傳遞參數,所以我們就需要把之前定義的變量改為一個方法,讓這個方法去return一個元素:

import React from 'react'
import ReactDOM from 'react-dom'

// 特別注意這里的寫法,如果要在JSX里寫js表達式(只能是表達式,不能流程控制),就需要加 {},包括注釋也是一樣,並且可以多層嵌套
const app = (props) => <h1>歡迎進入{props.name}的世界</h1>

ReactDOM.render(
  app({
    name: 'react'
  }),
  document.getElementById('root')
)

這里我們定義的方法實際上也是react定義組件的第一種方式-定義函數式組件,這也是無狀態組件。但是這種寫法不符合react的jsx的風格,更好的方式是使用以下方式進行改造

import React from 'react'
import ReactDOM from 'react-dom'

const App = (props) => <h1>歡迎進入{props.name}的世界</h1>

ReactDOM.render(
  // React組件的調用方式
  <App name="react" />,
  document.getElementById('root')
)

這樣一個完整的函數式組件就定義好了。但要注意!注意!注意!組件名必須大寫,否則報錯。

class組件

ES6的加入讓JavaScript直接支持使用class來定義一個類,react的第二種創建組件的方式就是使用的類的繼承,ES6 class是目前官方推薦的使用方式,它使用了ES6標准語法來構建,看以下代碼:

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  render () {
    return (
      // 注意這里得用this.props.name, 必須用this.props
      <h1>歡迎進入{this.props.name}的世界</h1>
  	)
  }
}
ReactDOM.render(
  <App name="react" />,
  document.getElementById('root')
)

運行結果和之前完全一樣,因為JS里沒有真正的class,這個class只是一個語法糖, 但二者的運行機制底層運行機制不一樣。

  • 函數式組件是直接調用, 在前面的代碼里已經有看到

  • es6 class組件其實就是一個構造器,每次使用組件都相當於在實例化組件,像這樣:

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    class App extends React.Component {
      render () {
        return (
      		<h1>歡迎進入{this.props.name}的世界</h1>
      	)
      }
    }
    
    const app = new App({
      name: 'react'
    }).render()
    
    ReactDOM.render(
      app,
      document.getElementById('root')
    )
    

更老的一種方法

在16以前的版本還支持這樣創建組件, 但現在的項目基本上不用

React.createClass({
  render () {
    return (
      <div>{this.props.xxx}</div>
  	)
  }
})

組件的組合、嵌套

將一個組件渲染到某一個節點里的時候,會將這個節點里原有內容覆蓋

組件嵌套的方式就是將子組件寫入到父組件的模板中去,且react沒有Vue中的內容分發機制(slot),所以我們在一個組件的模板中只能看到父子關系

// 從 react 的包當中引入了 React 和 React.js 的組件父類 Component
// 還引入了一個React.js里的一種特殊的組件 Fragment
import React, { Component, Fragment } from 'react'
import ReactDOM from 'react-dom'

class Title extends Component {
  render () {
    return (
      <h1>歡迎進入React的世界</h1>
  	)
  }
}
class Content extends Component {
  render () {
    return (
      <p>React.js是一個構建UI的庫</p>
  	)
  }
}
/** 由於每個React組件只能有一個根節點,所以要渲染多個組件的時候,需要在最外層包一個容器,如果使用div, 會生成多余的一層dom
class App extends Component {
  render () {
    return (
    	<div>
    		<Title />
        <Content />
      </div>
  	)
  }
}
**/
// 如果不想生成多余的一層dom可以使用React提供的Fragment組件在最外層進行包裹
class App extends Component {
  render () {
    return (
      <Fragment>
      	<Title />
        <Content />
      </Fragment>
  	)
  }
}
ReactDOM.render(
  <App/>,
  document.getElementById('root')
)

JSX 原理

要明白JSX的原理,需要先明白如何用 JavaScript 對象來表現一個 DOM 元素的結構?

看下面的DOM結構

<div class='app' id='appRoot'>
  <h1 class='title'>歡迎進入React的世界</h1>
  <p>
    React.js 是一個幫助你構建頁面 UI 的庫
  </p>
</div>

上面這個 HTML 所有的信息我們都可以用 JavaScript 對象來表示:

{
  tag: 'div',
  attrs: { className: 'app', id: 'appRoot'},
  children: [
    {
      tag: 'h1',
      attrs: { className: 'title' },
      children: ['歡迎進入React的世界']
    },
    {
      tag: 'p',
      attrs: null,
      children: ['React.js 是一個構建頁面 UI 的庫']
    }
  ]
}

但是用 JavaScript 寫起來太長了,結構看起來又不清晰,用 HTML 的方式寫起來就方便很多了。

於是 React.js 就把 JavaScript 的語法擴展了一下,讓 JavaScript 語言能夠支持這種直接在 JavaScript 代碼里面編寫類似 HTML 標簽結構的語法,這樣寫起來就方便很多了。編譯的過程會把類似 HTML 的 JSX 結構轉換成 JavaScript 的對象結構。

下面代碼:

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  render () {
    return (
      <div className='app' id='appRoot'>
        <h1 className='title'>歡迎進入React的世界</h1>
        <p>
          React.js 是一個構建頁面 UI 的庫
        </p>
      </div>
    )
  }
}

ReactDOM.render(
	<App />,
  document.getElementById('root')
)

編譯之后將得到這樣的代碼:

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  render () {
    return (
      React.createElement(
        "div",
        {
          className: 'app',
          id: 'appRoot'
        },
        React.createElement(
          "h1",
          { className: 'title' },
          "歡迎進入React的世界"
        ),
        React.createElement(
          "p",
          null,
          "React.js 是一個構建頁面 UI 的庫"
        )
      )
    )
  }
}

ReactDOM.render(
	React.createElement(App),
  document.getElementById('root')
)

React.createElement 會構建一個 JavaScript 對象來描述你 HTML 結構的信息,包括標簽名、屬性、還有子元素等, 語法為

React.createElement(
  type,
  [props],
  [...children]
)

所謂的 JSX 其實就是 JavaScript 對象,所以使用 React 和 JSX 的時候一定要經過編譯的過程:

JSX —使用react構造組件,bable進行編譯—> JavaScript對象 — ReactDOM.render()—>DOM元素 —>插入頁面

組件中DOM樣式

  • 行內樣式

想給虛擬dom添加行內樣式,需要使用表達式傳入樣式對象的方式來實現:

// 注意這里的兩個括號,第一個表示我們在要JSX里插入JS了,第二個是對象的括號
 <p style={{color:'red', fontSize:'14px'}}>Hello world</p>

行內樣式需要寫入一個樣式對象,而這個樣式對象的位置可以放在很多地方,例如render函數里、組件原型上、外鏈js文件中

  • 使用class

React推薦我們使用行內樣式,因為React覺得每一個組件都是一個獨立的整體

其實我們大多數情況下還是大量的在為元素添加類名,但是需要注意的是,class需要寫成className(因為畢竟是在寫類js代碼,會收到js規則的現在,而class是關鍵字)

<p className="hello" style = {this.style}>Hello world</p>
  • 不同的條件添加不同的樣式

有時候需要根據不同的條件添加不同的樣式,比如:完成狀態,完成是綠色,未完成是紅色。那么這種情況下,我們推薦使用classnames這個包:

  • css-in-js

styled-components是針對React寫的一套css-in-js框架,簡單來講就是在js中寫css。npm鏈接

TodoList

組件化開發React todolist, 項目開發中的組件的基本目錄結構基本上是這樣的:

/your-project

  • src
    • components
      • YourComponentOne
        • index.js/YourComponentOne.js
      • YourComponentTwo
        • index.js/YourComponentTwo.js
      • index.js 用於導出組件

注意:一個組件只干一件事情 ,所以TodoList和TodoItem要做成兩個組件,這樣也方便於后期理解shouldComponentUpdate

組件的數據掛載方式

屬性(props)

props是正常是外部傳入的,組件內部也可以通過一些方式來初始化的設置,屬性不能被組件自己更改,但是你可以通過父組件主動重新渲染的方式來傳入新的 props

屬性是描述性質、特點的,組件自己不能隨意更改。

之前的組件代碼里面有props的簡單使用,總的來說,在使用一個組件的時候,可以把參數放在標簽的屬性當中,所有的屬性都會作為組件 props 對象的鍵值。通過箭頭函數創建的組件,需要通過函數的參數來接收props:

import React, { Component, Fragment } from 'react'
import ReactDOM from 'react-dom'

class Title extends Component {
  render () {
    return (
  		<h1>歡迎進入{this.props.name}的世界</h1>
  	)
  }
}

const Content = (props) => {
  return (
    <p>{props.name}是一個構建UI的庫</p>
  )
}

class App extends Component {
  render () {
    return (
  		<Fragment>
      	<Title name="React" />
        <Content name="React.js" />
      </Fragment>
  	)
  }
}

ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

設置組件的默認props

import React, { Component, Fragment } from 'react'
import ReactDOM from 'react-dom'

class Title extends Component {
  // 使用類創建的組件,直接在這里寫static方法,創建defaultProps
  static defaultProps = {
    name: 'React'
  }
  render () {
    return (
  		<h1>歡迎進入{this.props.name}的世界</h1>
  	)
  }
}

const Content = (props) => {
  return (
    <p>{props.name}是一個構建UI的庫</p>
  )
}

// 使用箭頭函數創建的組件,需要在這個組件上直接寫defaultProps屬性
Content.defaultProps = {
  name: 'React.js'
}

class App extends Component {
  render () {
    return (
  		<Fragment>
        {/* 由於設置了defaultProps, 不傳props也能正常運行,如果傳遞了就會覆蓋defaultProps的值 */}
      	<Title />
        <Content />
      </Fragment>
  	)
  }
}

ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

props.children

我們知道使用組件的時候,可以嵌套。要在自定義組件的使用嵌套結構,就需要使用 props.children 。在實際的工作當中,我們幾乎每天都需要用這種方式來編寫組件。

import React, { Component, Fragment } from 'react'
import ReactDOM from 'react-dom'

class Title extends Component {
  render () {
    return (
  		<h1>歡迎進入{this.props.children}的世界</h1>
  	)
  }
}

const Content = (props) => {
  return (
    <p>{props.children}</p>
  )
}

class App extends Component {
  render () {
    return (
  		<Fragment>
      	<Title>React</Title>
        <Content><i>React.js</i>是一個構建UI的庫</Content>
      </Fragment>
  	)
  }
}

ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

使用prop-types檢查props

React其實是為了構建大型應用程序而生, 在一個大型應用中,根本不知道別人使用你寫的組件的時候會傳入什么樣的參數,有可能會造成應用程序運行不了,但是不報錯。為了解決這個問題,React提供了一種機制,讓寫組件的人可以給組件的props設定參數檢查,需要安裝和使用prop-types:

$ npm i prop-types -S

狀態(state)

狀態就是組件描述某種顯示情況的數據,由組件自己設置和更改,也就是說由組件自己維護,使用狀態的目的就是為了在不同的狀態下使組件的顯示不同(自己管理)

定義state

第一種方式

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  state = {
    name: 'React',
    isLiked: false
  }
  render () {
    return (
      <div>
        <h1>歡迎來到{this.state.name}的世界</h1>
        <button>
          {
            this.state.isLiked ? '❤️取消' : '🖤收藏'
          }
        </button>
      </div>
  	)
  }
}
ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

另一種方式(推薦)

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor() {
    super()
    this.state = {
      name: 'React',
      isLiked: false
    }
  }
  render () {
    return (
  		<div>
        <h1>歡迎來到{this.state.name}的世界</h1>
        <button>
          {
            this.state.isLiked ? '❤️取消' : '🖤收藏'
          }
        </button>
      </div>
  	)
  }
}
ReactDOM.render(
  <App/>,
  document.getElementById('root')
)

this.propsthis.state是純js對象,在vue中,data屬性是利用Object.defineProperty處理過的,更改​data的數據的時候會觸發數據的gettersetter,但是React中沒有做這樣的處理,如果直接更改的話,react是無法得知的,所以,需要使用特殊的更改狀態的方法setState

setState

isLiked 存放在實例的 state 對象當中,組件的 render 函數內,會根據組件的 state 的中的isLiked不同顯示“取消”或“收藏”內容。下面給 button 加上了點擊的事件監聽。

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor() {
    super()
    this.state = {
      name: 'React',
      isLiked: false
    }
  }
  handleBtnClick = () => {
    this.setState({
      isLiked: !this.state.isLiked
    })
  }
  render () {
    return (
      <div>
        <h1>歡迎來到{this.state.name}的世界</h1>
        <button onClick={this.handleBtnClick}>
          {
            this.state.isLiked ? '❤️取消' : '🖤收藏'
          }
        </button>
      </div>
  	)
  }
}
ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

setState有兩個參數

第一個參數可以是對象,也可以是方法return一個對象,我們把這個參數叫做updater

  • 參數是對象

    this.setState({
      isLiked: !this.state.isLiked
    })
    
  • 參數是方法

    this.setState((prevState, props) => {
      return {
        isLiked: !prevState.isLiked
      }
    })
    

    注意的是這個方法接收兩個參數,第一個是上一次的state, 第二個是props

setState是異步的,所以想要獲取到最新的state,沒有辦法獲取,就有了第二個參數,這是一個可選的回調函數

this.setState((prevState, props) => {
  return {
    isLiked: !prevState.isLiked
  }
}, () => {
  console.log('回調里的',this.state.isLiked)
})
console.log('setState外部的',this.state.isLiked)

屬性vs狀態

相似點:都是純js對象,都會觸發render更新,都具有確定性(狀態/屬性相同,結果相同)

不同點:

  1. 屬性能從父組件獲取,狀態不能
  2. 屬性可以由父組件修改,狀態不能
  3. 屬性能在內部設置默認值,狀態也可以
  4. 屬性不在組件內部修改,狀態要改
  5. 屬性能設置子組件初始值,狀態不可以
  6. 屬性可以修改子組件的值,狀態不可以

state 的主要作用是用於組件保存、控制、修改自己的可變狀態。state 在組件內部初始化,可以被組件自身修改,而外部不能訪問也不能修改。你可以認為 state 是一個局部的、只能被組件自身控制的數據源。state 中狀態可以通過 this.setState方法進行更新,setState 會導致組件的重新渲染。

props 的主要作用是讓使用該組件的父組件可以傳入參數來配置該組件。它是外部傳進來的配置參數,組件內部無法控制也無法修改。除非外部組件主動傳入新的 props,否則組件的 props 永遠保持不變。

如果搞不清 stateprops 的使用場景,記住一個簡單的規則:盡量少地用 state,多用 props

沒有 state 的組件叫無狀態組件(stateless component),設置了 state 的叫做有狀態組件(stateful component)。因為狀態會帶來管理的復雜性,我們盡量多地寫無狀態組件,盡量少地寫有狀態的組件。這樣會降低代碼維護的難度,也會在一定程度上增強組件的可復用性。

狀態提升

如果有多個組件共享一個數據,把這個數據放到共同的父級組件中來管理

受控組件與非受控組件

React組件的數據渲染是否被調用者傳遞的props完全控制,控制則為受控組件,否則非受控組件。

渲染數據

  • 條件渲染
{
  condition ? '❤️取消' : '🖤收藏'
}
  • 列表渲染
// 數據
const people = [{
  id: 1,
  name: 'Leo',
  age: 35
}, {
  id: 2,
  name: 'XiaoMing',
  age: 16
}]
// 渲染列表
{
  people.map(person => {
    return (
      <dl key={person.id}>
        <dt>{person.name}</dt>
        <dd>age: {person.age}</dd>
      </dl>
    )
  })
}

React的高效依賴於所謂的 Virtual-DOM,盡量不碰 DOM。對於列表元素來說會有一個問題:元素可能會在一個列表中改變位置。要實現這個操作,只需要交換一下 DOM 位置就行了,但是React並不知道其實我們只是改變了元素的位置,所以它會重新渲染后面兩個元素(再執行 Virtual-DOM ),這樣會大大增加 DOM 操作。但如果給每個元素加上唯一的標識,React 就可以知道這兩個元素只是交換了位置,這個標識就是key,這個 key 必須是每個元素唯一的標識

  • dangerouslySetHTML

對於富文本創建的內容,后台拿到的數據是這樣的:

content = "<p>React.js是一個構建UI的庫</p>"

處於安全的原因,React當中所有表達式的內容會被轉義,如果直接輸入,標簽會被當成文本。這時候就需要使用dangerouslySetHTML屬性,它允許我們動態設置innerHTML

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor() {
    super()
    this.state = {
      content : "<p>React.js是一個構建UI的庫</p>"
    }
  }
  render () {
    return (
  		<div
        // 注意這里是兩個下下划線 __html
        dangerouslySetInnerHTML={{__html: this.state.content}}
      />
  	)
  }
}
ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

事件處理

綁定事件

采用on+事件名的方式來綁定一個事件,注意,這里和原生的事件是有區別的,原生的事件全是小寫onclick, React里的事件是駝峰onClickReact的事件並不是原生事件,而是合成事件

事件handler的寫法

  • 直接在render里寫行內的箭頭函數(不推薦)
  • 在組件內使用箭頭函數定義一個方法(推薦)
  • 直接在組件內定義一個非箭頭函數的方法,然后在render里直接使用onClick={this.handleClick.bind(this)}(不推薦)
  • 直接在組件內定義一個非箭頭函數的方法,然后在constructor里bind(this)(推薦)

Event 對象

和普通瀏覽器一樣,事件handler會被自動傳入一個 event 對象,這個對象和普通的瀏覽器 event 對象所包含的方法和屬性都基本一致。不同的是 React中的 event 對象並不是瀏覽器提供的,而是它自己內部所構建的。它同樣具有event.stopPropagationevent.preventDefault 這種常用的方法

事件的參數傳遞

  • render里調用方法的地方外面包一層箭頭函數
  • render里通過this.handleEvent.bind(this, 參數)這樣的方式來傳遞
  • 通過event傳遞
  • 比較推薦的是做一個子組件, 在父組件中定義方法,通過props傳遞到子組件中,然后在子組件件通過this.props.method來調用

處理用戶輸入

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor() {
    super()
    this.state = {
      xing: '',
      ming: ''
    }
  }
  handleInputChange = (e) => {
    this.setState({
      [e.target.name]: e.target.value
    })
  }
  render () {
    const {
      xing,
      ming
    } = this.state
    return (
  		<div>
        <label>
          <span>姓:</span>
          <input
            type="text"
            name="xing"
            value={xing}
            onChange={this.handleInputChange}
          />
        </label>
        <label>
          <span>名:</span>
          <input
            type="text"
            name="ming"
            value={ming}
            onChange={this.handleInputChange}
          />
        </label>
        <p>歡迎您: {xing}{ming}</p>
      </div>
  	)
  }
}
ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

組件的生命周期

React中組件也有生命周期,也就是說也有很多鈎子函數供我們使用, 組件的生命周期,我們會分為四個階段,初始化、運行中、銷毀、錯誤處理(16.3之后)

初始化

在組件初始化階段會執行 
1. constructor 
2. static getDerivedStateFromProps()
3. componentWillMount() / UNSAFE_componentWillMount() 
4. render() 
5. componentDidMount()

更新階段

propsstate的改變可能會引起組件的更新,組件重新渲染的過程中會調用以下方法:

1. componentWillReceiveProps() / UNSAFE_componentWillReceiveProps() 
2. static getDerivedStateFromProps()
3. shouldComponentUpdate() 
4. componentWillUpdate() / UNSAFE_componentWillUpdate() 
5. render() 
6. getSnapshotBeforeUpdate() 
7. componentDidUpdate()

卸載階段

  1. componentWillUnmount()

錯誤處理

  1. componentDidCatch()

各生命周期詳解

1.constructor(props)

React組件的構造函數在掛載之前被調用。在實現React.Component構造函數時,需要先在添加其他內容前,調用super(props),用來將父組件傳來的props綁定到這個類中,使用this.props將會得到。

官方建議不要在constructor引入任何具有副作用和訂閱功能的代碼,這些應當使用componentDidMount()

constructor中應當做些初始化的動作,如:初始化state,將事件處理函數綁定到類實例上,但也不要使用setState()。如果沒有必要初始化state或綁定方法,則不需要構造constructor,或者把這個組件換成純函數寫法。

當然也可以利用props初始化state,在之后修改state不會對props造成任何修改,但仍然建議大家提升狀態到父組件中,或使用redux統一進行狀態管理。

constructor(props) {
  super(props);
  this.state = {
    isLiked: props.isLiked
  };
}
2.static getDerivedStateFromProps(nextProps, prevState)

getDerivedStateFromProps 是react16.3之后新增,在組件實例化后,和接受新的props后被調用。他必須返回一個對象來更新狀態,或者返回null表示新的props不需要任何state的更新。

如果是由於父組件的props更改,所帶來的重新渲染,也會觸發此方法。

調用steState()不會觸發getDerivedStateFromProps()

之前這里都是使用constructor+componentWillRecieveProps完成相同的功能的

3. componentWillMount() / UNSAFE_componentWillMount()

componentWillMount()將在React未來版本(官方說法 17.0)中被棄用。UNSAFE_componentWillMount()在組件掛載前被調用,在這個方法中調用setState()不會起作用,是由於他在render()前被調用。

為了避免副作用和其他的訂閱,官方都建議使用componentDidMount()代替。這個方法是用於在服務器渲染上的唯一方法。這個方法因為是在渲染之前被調用,也是惟一一個可以直接同步修改state的地方。

4.render()

render()方法是必需的。當他被調用時,他將計算this.propsthis.state,並返回以下一種類型:

  1. React元素。通過jsx創建,既可以是dom元素,也可以是用戶自定義的組件。
  2. 字符串或數字。他們將會以文本節點形式渲染到dom中。
  3. Portals。react 16版本中提出的新的解決方案,可以使組件脫離父組件層級直接掛載在DOM樹的任何位置。 
    4. null,什么也不渲染
  4. 布爾值。也是什么都不渲染。

當返回null,false,ReactDOM.findDOMNode(this)將會返回null,什么都不會渲染。

render()方法必須是一個純函數,他不應該改變state,也不能直接和瀏覽器進行交互,應該將事件放在其他生命周期函數中。
如果shouldComponentUpdate()返回falserender()不會被調用。

5. componentDidMount

componentDidMount在組件被裝配后立即調用。初始化使得DOM節點應該進行到這里。

通常在這里進行ajax請求

如果要初始化第三方的dom庫,也在這里進行初始化。只有到這里才能獲取到真實的dom.

6.componentWillReceiveProps()/UNSAFE_componentWillReceiveProps(nextProps)

官方建議使用getDerivedStateFromProps函數代替componentWillReceiveProps。當組件掛載后,接收到新的props后會被調用。如果需要更新state來響應props的更改,則可以進行this.propsnextProps的比較,並在此方法中使用this.setState()

如果父組件會讓這個組件重新渲染,即使props沒有改變,也會調用這個方法。

React不會在組件初始化props時調用這個方法。調用this.setState也不會觸發。

7.shouldComponentUpdate(nextProps, nextState)

調用shouldComponentUpdate使React知道,組件的輸出是否受stateprops的影響。默認每個狀態的更改都會重新渲染,大多數情況下應該保持這個默認行為。

在渲染新的propsstate前,shouldComponentUpdate會被調用。默認為true。這個方法不會在初始化時被調用,也不會在forceUpdate()時被調用。返回false不會阻止子組件在state更改時重新渲染。

如果shouldComponentUpdate()返回falsecomponentWillUpdate,rendercomponentDidUpdate不會被調用。

官方並不建議在shouldComponentUpdate()中進行深度查詢或使用JSON.stringify(),他效率非常低,並且損傷性能。

8.UNSAFE_componentWillUpdate(nextProps, nextState)

在渲染新的stateprops時,UNSAFE_componentWillUpdate會被調用,將此作為在更新發生之前進行准備的機會。這個方法不會在初始化時被調用。

不能在這里使用this.setState(),也不能做會觸發視圖更新的操作。如果需要更新stateprops,調用getDerivedStateFromProps

9.getSnapshotBeforeUpdate()

在react render()后的輸出被渲染到DOM之前被調用。它使您的組件能夠在它們被潛在更改之前捕獲當前值(如滾動位置)。這個生命周期返回的任何值都將作為參數傳遞給componentDidUpdate()。

10.componentDidUpdate(prevProps, prevState, snapshot)

在更新發生后立即調用componentDidUpdate()。此方法不用於初始渲染。當組件更新時,將此作為一個機會來操作DOM。只要您將當前的props與以前的props進行比較(例如,如果props沒有改變,則可能不需要網絡請求),這也是做網絡請求的好地方。

如果組件實現getSnapshotBeforeUpdate()生命周期,則它返回的值將作為第三個“快照”參數傳遞給componentDidUpdate()。否則,這個參數是undefined

11.componentWillUnmount()

在組件被卸載並銷毀之前立即被調用。在此方法中執行任何必要的清理,例如使定時器無效,取消網絡請求或清理在componentDidMount中創建的任何監聽。

12.componentDidCatch(error, info)

錯誤邊界是React組件,可以在其子組件樹中的任何位置捕獲JavaScript錯誤,記錄這些錯誤並顯示回退UI,而不是崩潰的組件樹。錯誤邊界在渲染期間,生命周期方法以及整個樹下的構造函數中捕獲錯誤。

如果類組件定義了此生命周期方法,則它將成錯誤邊界。在它中調用setState()可以讓你在下面的樹中捕獲未處理的JavaScript錯誤,並顯示一個后備UI。只能使用錯誤邊界從意外異常中恢復; 不要試圖將它們用於控制流程。

錯誤邊界只會捕獲樹中下面組件中的錯誤。錯誤邊界本身不能捕獲錯誤。

PureComponent

PureComponnet里如果接收到的新屬性或者是更改后的狀態和原屬性、原狀態相同的話,就不會去重新render了
在里面也可以使用shouldComponentUpdate,而且。是否重新渲染以shouldComponentUpdate的返回值為最終的決定因素。

import React, { PureComponent } from 'react'

class YourComponent extends PureComponent {
  ……
}

ref

React提供的這個ref屬性,表示為對組件真正實例的引用,其實就是ReactDOM.render()返回的組件實例,ref可以掛載到組件上也可以是dom元素上。

  • 掛到組件(class聲明的組件)上的ref表示對組件實例的引用。不能在函數式組件上使用 ref 屬性,因為它們沒有實例:
  • 掛載到dom元素上時表示具體的dom元素節點。

在React 最新的版本中,要使用ref, 需要使用React.createRef方法先生成一個ref

import React, { Component, createRef } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor() {
    super()
    // 創建inputRef
    this.inputRef=createRef()
  }
  componentDidMount () {
    console.log(this.inputRef.current) // <input type="text">
  }
  render () {
    return (
  		<div>
        {/* 關聯ref和dom */}
        <input type="text" ref={this.inputRef} />
      </div>
  	)
  }
}
ReactDOM.render(
	<App/>,
  document.getElementById('root')
)

React Hooks

React Hooks 是 React 16.7.0-alpha 版本推出的新特性, 有了React Hooks,在 react 函數組件中,也可以使用類組件(classes components)的 state 和 組件生命周期。通過下面幾個例子來學習React Hooks。

  • State Hook
// useState是react包提供的一個方法
import React, { useState } from "react";
import ReactDOM from "react-dom";

const Counter = () => {
  // useState 這個方法可以為我們的函數組件擁有自己的state,它接收一個用於初始 state 的值,返回一對變量。這里我們把計數器的初始值設置為0, 方法都是以set開始
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>你點擊了{count}次</p>
      <button onClick={() => setCount(count + 1)}>點擊</button>
    </div>
  );
};

const rootElement = document.getElementById("root");

ReactDOM.render(<Counter />, rootElement);
  • Effect Hook
// useState是react包提供的一個方法
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const Counter = () => {
  // useState 這個方法可以為我們的函數組件擁有自己的state,它接收一個用於初始 state 的值,返回一對變量。這里我們把計數器的初始值設置為0, 方法都是以set開始
  const [count, setCount] = useState(0);
  // 類似於componentDidMount或者componentDidUpdate:
  useEffect(() => {
    // 更改網頁的標題,還可以做其它的監聽
    document.title = `你點擊了${count}次`;
  });
  return (
    <div>
      <p>你點擊了{count}次</p>
      <button onClick={() => setCount(count + 1)}>點擊</button>
    </div>
  );
};

const rootElement = document.getElementById("root");

ReactDOM.render(<Counter />, rootElement);

組件通信

父組件與子組件通信

  • 父組件將自己的狀態傳遞給子組件,子組件當做屬性來接收,當父組件更改自己狀態的時候,子組件接收到的屬性就會發生改變

  • 父組件利用ref對子組件做標記,通過調用子組件的方法以更改子組件的狀態,也可以調用子組件的方法..

子組件與父組件通信

  • 父組件將自己的某個方法傳遞給子組件,在方法里可以做任意操作,比如可以更改狀態,子組件通過this.props接收到父組件的方法后調用。

跨組件通信

在react沒有類似vue中的事件總線來解決這個問題,我們只能借助它們共同的父級組件來實現,將非父子關系裝換成多維度的父子關系。react提供了context api來實現跨組件通信, React 16.3之后的contextapi較之前的好用。

實例,使用context 實現購物車中的加減功能

// counterContext.js
import React, { Component, createContext } from 'react'

const {
  Provider,
  Consumer: CountConsumer
} = createContext()

class CountProvider extends Component {
  constructor () {
    super()
    this.state = {
      count: 1
    }
  }
  increaseCount = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  decreaseCount = () => {
    this.setState({
      count: this.state.count - 1
    })
  }
  render() {
    return (
      <Provider value={{
        count: this.state.count,
        increaseCount: this.increaseCount,
        decreaseCount: this.decreaseCount
      }}
      >
        {this.props.children}
      </Provider>
    )
  }
}

export {
  CountProvider,
  CountConsumer
}
// 定義CountButton組件
const CountButton = (props) => {
  return (
    <CountConsumer>
      // consumer的children必須是一個方法
      {
        ({ increaseCount, decreaseCount }) => {
          const { type } = props
          const handleClick = type === 'increase' ? increaseCount : decreaseCount
          const btnText = type === 'increase' ? '+' : '-'
          return <button onClick={handleClick}>{btnText}</button>
        }
      }
    </CountConsumer>
  )
}
// 定義count組件,用於顯示數量
const Count = (prop) => {
  return (
    <CountConsumer>
      {
        ({ count }) => {
          return <span>{count}</span>
        }
      }
    </CountConsumer>
  )
}
// 組合
class App extends Component {
  render () {
    return (
  		<CountProvider>
        <CountButton type='decrease' />
        <Count />
        <CountButton type='increase' />
      </CountProvider>
  	)
  }
}

復雜的非父子組件通信在react中很難處理,多組件間的數據共享也不好處理,在實際的工作中我們會使用flux、redux、mobx來實現

HOC(高階組件)

Higher-Order Components就是一個函數,傳給它一個組件,它返回一個新的組件。

const NewComponent = higherOrderComponent(YourComponent)

比如,我們想要我們的組件通過自動注入一個版權信息。

// withCopyright.js 定義一個高階組件
import React, { Component, Fragment } from 'react'

const withCopyright = (WrappedComponent) => {
  return class NewComponent extends Component {
    render() {
      return (
        <Fragment>
          <WrappedComponent />
          <div>&copy;版權所有 千鋒教育 2019 </div>
        </Fragment>
      )
    }
  }
}
export default withCopyright
// 使用方式
import withCopyright from './withCopyright'

class App extends Component {
  render () {
    return (
  		<div>
        <h1>Awesome React</h1>
        <p>React.js是一個構建用戶界面的庫</p>
      </div>
  	)
  }
}
const CopyrightApp = withCopyright(App)

這樣只要我們有需要用到版權信息的組件,都可以直接使用withCopyright這個高階組件包裹即可。

在這里要講解在CRA 中配置裝飾器模式的支持。

狀態管理

傳統MVC框架的缺陷

什么是MVC?

image-20190420010944626

MVC的全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,是一種軟件設計典范。

V即View視圖是指用戶看到並與之交互的界面。

M即Model模型是管理數據 ,很多業務邏輯都在模型中完成。在MVC的三個部件中,模型擁有最多的處理任務。

C即Controller控制器是指控制器接受用戶的輸入並調用模型和視圖去完成用戶的需求,控制器本身不輸出任何東西和做任何處理。它只是接收請求並決定調用哪個模型構件去處理請求,然后再確定用哪個視圖來顯示返回的數據。

MVC只是看起來很美

MVC框架的數據流很理想,請求先到Controller, 由Controller調用Model中的數據交給View進行渲染,但是在實際的項目中,又是允許Model和View直接通信的。然后就出現了這樣的結果:

image-20190420012010718

Flux

在2013年,Facebook讓React亮相的同時推出了Flux框架,React的初衷實際上是用來替代jQuery的,Flux實際上就可以用來替代Backbone.jsEmber.js等一系列MVC架構的前端JS框架。

其實FluxReact里的應用就類似於Vue中的Vuex的作用,但是在Vue中,Vue是完整的mvvm框架,而Vuex只是一個全局的插件。

React只是一個MVC中的V(視圖層),只管頁面中的渲染,一旦有數據管理的時候,React本身的能力就不足以支撐復雜組件結構的項目,在傳統的MVC中,就需要用到Model和Controller。Facebook對於當時世面上的MVC框架並不滿意,於是就有了Flux, 但Flux並不是一個MVC框架,他是一種新的思想。

image-20190420012450223

  • View: 視圖層
  • ActionCreator(動作創造者):視圖層發出的消息(比如mouseClick)
  • Dispatcher(派發器):用來接收Actions、執行回調函數
  • Store(數據層):用來存放應用的狀態,一旦發生變動,就提醒Views要更新頁面

Flux的流程:

  1. 組件獲取到store中保存的數據掛載在自己的狀態上
  2. 用戶產生了操作,調用actions的方法
  3. actions接收到了用戶的操作,進行一系列的邏輯代碼、異步操作
  4. 然后actions會創建出對應的action,action帶有標識性的屬性
  5. actions調用dispatcher的dispatch方法將action傳遞給dispatcher
  6. dispatcher接收到action並根據標識信息判斷之后,調用store的更改數據的方法
  7. store的方法被調用后,更改狀態,並觸發自己的某一個事件
  8. store更改狀態后事件被觸發,該事件的處理程序會通知view去獲取最新的數據

Redux

React 只是 DOM 的一個抽象層,並不是 Web 應用的完整解決方案。有兩個方面,它沒涉及。

  • 代碼結構
  • 組件之間的通信

2013年 Facebook 提出了 Flux 架構的思想,引發了很多的實現。2015年,Redux 出現,將 Flux 與函數式編程結合一起,很短時間內就成為了最熱門的前端架構。

如果你不知道是否需要 Redux,那就是不需要它

只有遇到 React 實在解決不了的問題,你才需要 Redux

簡單說,如果你的UI層非常簡單,沒有很多互動,Redux 就是不必要的,用了反而增加復雜性。

  • 用戶的使用方式非常簡單
  • 用戶之間沒有協作
  • 不需要與服務器大量交互,也沒有使用 WebSocket
  • 視圖層(View)只從單一來源獲取數據

需要使用Redux的項目:

  • 用戶的使用方式復雜
  • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
  • 多個用戶之間可以協作
  • 與服務器大量交互,或者使用了WebSocket
  • View要從多個來源獲取數據

從組件層面考慮,什么樣子的需要Redux:

  • 某個組件的狀態,需要共享
  • 某個狀態需要在任何地方都可以拿到
  • 一個組件需要改變全局狀態
  • 一個組件需要改變另一個組件的狀態

Redux的設計思想:

  1. Web 應用是一個狀態機,視圖與狀態是一一對應的。
  2. 所有的狀態,保存在一個對象里面(唯一數據源)。

注意:flux、redux都不是必須和react搭配使用的,因為flux和redux是完整的架構,在學習react的時候,只是將react的組件作為redux中的視圖層去使用了。

Redux的使用的三大原則:

  • Single Source of Truth(唯一的數據源)
  • State is read-only(狀態是只讀的)
  • Changes are made with pure function(數據的改變必須通過純函數完成)

自己實現Redux

這個部分,可以根據班級情況看是否講解。對於學生使用redux有很大的幫助。不使用react,直接使用原生的html/js來寫一個簡易的的redux

基本的狀態管理及數據渲染

<!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>Redux principle 01</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }

    // 定一個方法叫changeState,用於處理state的數據,每次都返回一個新的狀態
    const changeState = (action) => {
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          countState.count -= action.number
          break;
        // 處理加        
        case 'COUNT_INCREMENT':
          countState.count += action.number
          break;
        default:
          break;
      }
    }

    // 定義一個方法用於渲染計數器的dom
    const renderCount = (state) => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = state.count
    }
  
    // 首次渲染數據
    renderCount(countState)

    // 定義一個dispatch的方法,接收到動作之后,自動調用
    const dispatch = (action) => {
      changeState(action)
      renderCount(countState)
    }

  </script>
</body>
</html>

創建createStore方法

<!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>Redux principle 02</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個方法,用於集中管理state和dispatch
    const createStore = (state, changeState) => {
      // getState用於獲取狀態
      const getState = () => state
      
      // 定義一個監聽器,用於管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

       // 定義一個dispatch方法,讓每次有action傳入的時候返回render執行之后的結果
      const dispatch = (action) => {
        // 調用changeState來處理數據
        changeState(state, action)
        // 讓監聽器里的所以方法運行
        listeners.forEach(listener => listener())
      }
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
    // 定一個方法叫changeState,用於處理state的數據,每次都返回一個新的狀態
    const changeState = (state, action) => {
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          state.count -= action.number
          break;
        // 處理加        
        case 'COUNT_INCREMENT':
          state.count += action.number
          break;
        default:
          break;
      }
    }

    // 創建一個store
    const store = createStore(countState, changeState)
    // 定義一個方法用於渲染計數器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染數據
    renderCount()
    // 監聽,只要有dispatch,這個方法就會自動運行
    store.subscribe(renderCount)
  </script>
</body>
</html>

讓changeState方法變為一個純函數

<!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>Redux principle 03</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個方法,用於集中管理state和dispatch
    const createStore = (state, changeState) => {
      // getState用於獲取狀態
      const getState = () => state
      
      // 定義一個監聽器,用於管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

      // 定義一個dispatch方法,讓每次有action傳入的時候返回render執行之后的結果
      const dispatch = (action) => {
        // 調用changeState來處理數據
        state = changeState(state, action)
        // 讓監聽器里的所有方法運行
        listeners.forEach(listener => listener())
      }
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
    // 定一個方法叫changeState,用於處理state的數據,每次都返回一個新的狀態
    const changeState = (state, action) => {
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 處理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }

    // 創建一個store
    const store = createStore(countState, changeState)
    // 定義一個方法用於渲染計數器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染數據
    renderCount()
    // 監聽,只要有dispatch,這個方法就會自動運行
    store.subscribe(renderCount)
  </script>
</body>
</html>

合並state和changeState(最終版)

<!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>Redux principle 04</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個方法,用於集中管理state和dispatch, changeState改名了,專業的叫法是reducer
    const createStore = (reducer) => {
      // 定義一個初始的state
      let state = null
      // getState用於獲取狀態
      const getState = () => state
      
      // 定義一個監聽器,用於管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)

      // 定義一個dispatch方法,讓每次有action傳入的時候返回reducer執行之后的結果
      const dispatch = (action) => {
        // 調用reducer來處理數據
        state = reducer(state, action)
        // 讓監聽器里的所有方法運行
        listeners.forEach(listener => listener())
      }
      //  初始化state
      dispatch({})
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
    // 定一個方法叫changeState,用於處理state的數據,每次都返回一個新的狀態
    const changeState = (state, action) => {
      // 如果state是null, 就返回countState
      if (!state) return countState
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 處理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }

    // 創建一個store
    const store = createStore(changeState)
    // 定義一個方法用於渲染計數器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染數據
    renderCount()
    // 監聽,只要有dispatch,renderCount就會自動運行
    store.subscribe(renderCount)
  </script>
</body>
</html>

使用Redux框架

Redux的流程:

image-20190420013410981

1.store通過reducer創建了初始狀態

2.view通過store.getState()獲取到了store中保存的state掛載在了自己的狀態上

3.用戶產生了操作,調用了actions 的方法

4.actions的方法被調用,創建了帶有標示性信息的action

5.actions將action通過調用store.dispatch方法發送到了reducer中

6.reducer接收到action並根據標識信息判斷之后返回了新的state

7.store的state被reducer更改為新state的時候,store.subscribe方法里的回調函數會執行,此時就可以通知view去重新獲取state

Reducer必須是一個純函數:

Reducer 函數最重要的特征是,它是一個純函數。也就是說,只要是同樣的輸入,必定得到同樣的輸出。Reducer不是只有Redux里才有,之前學的數組方法reduce, 它的第一個參數就是一個reducer

純函數是函數式編程的概念,必須遵守以下一些約束。

  • 不得改寫參數

  • 不能調用系統 I/O 的API

  • 不能調用Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結果

由於 Reducer 是純函數,就可以保證同樣的State,必定得到同樣的 View。但也正因為這一點,Reducer 函數里面不能改變 State,必須返回一個全新的對象,請參考下面的寫法。

// State 是一個對象
function reducer(state = defaultState, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一個數組
function reducer(state = defaultState, action) {
  return [...state, newItem];
}

最好把 State 對象設成只讀。要得到新的 State,唯一辦法就是生成一個新對象。這樣的好處是,任何時候,與某個 View 對應的 State 總是一個不變(immutable)的對象。

我們可以通過在createStore中傳入第二個參數來設置默認的state,但是這種形式只適合於只有一個reducer的時候。

划分reducer:

因為一個應用中只能有一個大的state,這樣的話reducer中的代碼將會特別特別的多,那么就可以使用combineReducers方法將已經分開的reducer合並到一起

注意:

  1. 分離reducer的時候,每一個reducer維護的狀態都應該不同
  2. 通過store.getState獲取到的數據也是會按照reducers去划分的
  3. 划分多個reducer的時候,默認狀態只能創建在reducer中,因為划分reducer的目的,就是為了讓每一個reducer都去獨立管理一部分狀態

最開始一般基於計數器的例子講解redux的基本使用即可

關於action/reducer/store的更多概念,請查看官網

Redux異步

通常情況下,action只是一個對象,不能包含異步操作,這導致了很多創建action的邏輯只能寫在組件中,代碼量較多也不便於復用,同時對該部分代碼測試的時候也比較困難,組件的業務邏輯也不清晰,使用中間件了之后,可以通過actionCreator異步編寫action,這樣代碼就會拆分到actionCreator中,可維護性大大提高,可以方便於測試、復用,同時actionCreator還集成了異步操作中不同的action派發機制,減少編碼過程中的代碼量

常見的異步庫:

  • Redux-thunk(就講這個)
  • Redux-saga
  • Redux-effects
  • Redux-side-effects
  • Redux-loop
  • Redux-observable

基於Promise的異步庫:

  • Redux-promise
  • Redux-promises
  • Redux-simple-promise
  • Redux-promise-middleware

容器組件(Smart/Container Components)和展示組件(Dumb/Presentational Components)

展示組件 容器組件
作用 描述如何展現(骨架、樣式) 描述如何運行(數據獲取、狀態更新)
直接使用 Redux
數據來源 props 監聽 Redux state
數據修改 從 props 調用回調函數 向 Redux 派發 actions
調用方式 手動 通常由 React Redux 生成

使用react-redux

可以先結合context來手動連接react和redux。

react-redux提供兩個核心的api:

  • Provider: 提供store
  • connect: 用於連接容器組件和展示組件
  1. Provider

    根據單一store原則 ,一般只會出現在整個應用程序的最頂層。

  2. connect

    語法格式為

    connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)

    一般來說只會用到前面兩個,它的作用是:

    • store.getState()的狀態轉化為展示組件的props
    • actionCreators轉化為展示組件props上的方法

特別強調:

官網上的第二個參數為mapDispatchToProps, 實際上就是actionCreators

只要上層中有Provider組件並且提供了store, 那么,子孫級別的任何組件,要想使用store里的狀態,都可以通過connect方法進行連接。如果只是想連接actionCreators,可以第一個參數傳遞為null

React Router

React Router現在的版本是5, 於2019年3月21日搞笑的發布,搞笑的官網鏈接, 本來是要發布4.4的版本的,結果成了5。從4開始,使用方式相對於之前版本的思想有所不同。之前版本的思想是傳統的思想:路由應該統一在一處渲染, Router 4之后是這樣的思想:一切皆組件

React Router包含了四個包:

包名 Description
react-router React Router核心api
react-router-dom React Router的DOM綁定,在瀏覽器中運行不需要額外安裝react-router
react-router-native React Native 中使用,而實際的應用中,其實不會使用這個。
react-router-config 靜態路由的配置

主要使用react-router-dom

使用方式

正常情況下,直接按照官網的demo就理解 路由的使用方式,有幾個點需要特別的強調:

  • Route組件的exact屬性

exact屬性標識是否為嚴格匹配, 為true是表示嚴格匹配,為false時為正常匹配。

  • Route組件的render屬性而不是component屬性

怎么在渲染組件的時候,對組件傳遞屬性呢?使用component的方式是不能直接在組件上添加屬性的。所以,React Router的Route組件提供了另一種渲染組件的方式 render, 這個常用於頁面組件級別的權限管理。

  • 路由的參數傳遞與獲取

  • Switch組件

總是渲染第一個匹配到的組件

  • 處理404與默認頁

  • withRoute高階組件的使用

  • 管理一個項目路由的方法

  • code spliting

  • HashRouter和BrowserRouter的區別,前端路由和后端路由的區別。這個在Vue里應該有講過了。

React Router基本原理

React Router甚至大部分的前端路由都是依賴於history.js的,它是一個獨立的第三方js庫。可以用來兼容在不同瀏覽器、不同環境下對歷史記錄的管理,擁有統一的API。

  • 老瀏覽器的history: 通過hash來存儲在不同狀態下的history信息,對應createHashHistory,通過檢測location.hash的值的變化,使用location.replace方法來實現url跳轉。通過注冊監聽window對象上的hashChange事件來監聽路由的變化,實現歷史記錄的回退。
  • 高版本瀏覽器: 利用HTML5里面的history,對應createBrowserHistory, 使用包括pushStatereplaceState方法來進行跳轉。通過注冊監聽window對象上的popstate事件來監聽路由的變化,實現歷史記錄的回退。
  • node環境下: 在內存中進行歷史記錄的存儲,對應createMemoryHistory。直接在內存里pushpop狀態。

Immutable.js

JavaScript數據修改的問題

看一段大家熟悉的代碼

const state = {
  str: '千鋒教育',
  obj: {
    y: 1
  },
  arr: [1, 2, 3]
}
const newState = state

console.log(newState === state) // true

由於js的對象和數組都是引用類型。所以newState的state實際上是指向於同一塊內存地址的, 所以結果是newState和state是相等的。

嘗試修改一下數據

const state = {
  str: '千鋒教育',
  obj: {
    y: 1
  },
  arr: [1, 2, 3]
}
const newState = state

newState.str = '千鋒教育H5學院'

console.log(state.str, newState.str)

可以看到,newState的修改也會引起state的修改。要解決這個問題,js中提供了另一種修改數據的方式,要修改一個數據之前先制作一份數據的拷貝,像這樣

const state = {
  str: '千鋒教育',
  obj: {
    y: 1
  },
  arr: [1, 2, 3]
}
const newState = Object.assign({}, state)

newState.str = '千鋒教育H5學院'

console.log(state.str, newState.str)

我們可以使用很多方式在js中復制數據,比如, Object.assign, Object.freeze, slice, concat, map, filter, reduce等方式進行復制,但這些都是淺拷貝,就是只拷貝第一層數據,更深層的數據還是同一個引用,比如:

const state = {
  str: '千鋒教育',
  obj: {
    y: 1
  },
  arr: [1, 2, 3]
}
const newState = Object.assign({}, state)

newState.obj.y = 2
newState.arr.push(4)

console.log(state, newState)

可以看到,當在更改newState更深層次的數據的時候,還是會影響到state的值。如果要深層復制,就得一層一層的做遞歸拷貝,這是一個復雜的問題。雖然有些第三方的庫已經幫我們做好了,比如lodashcloneDeep方法。深拷貝是非常消耗性能的。

import { cloneDeep } from 'lodash'

const state = {
  str: '千鋒教育',
  obj: {
    y: 1
  },
  arr: [1, 2, 3]
}
const newState = cloneDeep(state)

newState.obj.y = 2
newState.arr.push(4)

console.log(state, newState)

什么是不可變數據

不可變數據 (Immutable Data )就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。Immutable 實現的原理是持久化數據結構( Persistent Data Structure),也就是使用舊數據創建新數據時,要保證舊數據同時可用且不變。同時為了避免 deepCopy 把所有節點都復制一遍帶來的s性能損耗,Immutable 使用了 結構共享(Structural Sharing),即如果對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。

react

immutable.js的優缺點

優點:

  • 降低mutable帶來的復雜度
  • 節省內存
  • 歷史追溯性(時間旅行):時間旅行指的是,每時每刻的值都被保留了,想回退到哪一步只要簡單的將數據取出就行,想一下如果現在頁面有個撤銷的操作,撤銷前的數據被保留了,只需要取出就行,這個特性在redux或者flux中特別有用
  • 擁抱函數式編程:immutable本來就是函數式編程的概念,純函數式編程的特點就是,只要輸入一致,輸出必然一致,相比於面向對象,這樣開發組件和調試更方便。推薦一本函數式編程的在線免費書《JS 函數式編程指南》, 此書可以推薦給學生做為課外補充閱讀。

缺點:

  • 需要重新學習api
  • 資源包大小增加(源碼5000行左右)
  • 容易與原生對象混淆:由於api與原生不同,混用的話容易出錯。

使用Immutable.js

參考官網重點講解數據不可變數據的創建、更新及比較方式 。對於就業班來說,掌握以下知識點即可。

Map

import { Map } from 'immutable'

const map = Map({
  a: 1,
  b: 2,
  c: 3
})

const newMap = map.set('b', 20) // immutable數據每次都是生成新的再重新調用set進行修改,所以需要 重新賦值給一個新的變量

console.log(map, newMap) // immutable.Map不是原生的對象
console.log(map.b, newMap.b) // immutable.Map不是原生的對象, 所以是undefined
console.log(map.get('b'), newMap.get('b')) // 要取值,需要調用get(key)方法,可以看到,兩個值不一樣

const obj = {
  a: 1,
  b: 2,
  c: 3
}

console.log(Map.isMap(map), Map.isMap(obj)) // true false, 使用Map.isMap來判斷是否是一個immutable.Map類型

List

import { List } from 'immutable'

const list = List([1, 2, 3, 4])
const newList = list.push(5)
console.log(list, newList)
console.log(list[4], newList[4]) // undefined undefined
console.log(list.get(4), newList.get(4)) // undefined 5
console.log(list.size, newList.size) // 4 5

const arr = [1, 2, 3, 4]

console.log(List.isList(list), List.isList(arr)) // true false

equals & is

import { Map, is } from 'immutable'

const map = Map({
  a: 1,
  b: 2,
  c: 3
})

const anotherMap = Map({
  a: 1,
  b: 2,
  c: 3
})

console.log(map == anotherMap) // false
console.log(map === anotherMap) // false
console.log(map.equals(anotherMap)) // 使用equals進行比較 true
console.log(is(map, anotherMap)) // 使用is進行比較 true

List常用api

import { List } from 'immutable'

const list = List([1, 2, 3, 4])
const list1 = list.push(5)
const list2 = list1.unshift(0)
const list3 = list.concat(list1, list2)
const list4 = list.map(v => v * 2)

console.log(list.size, list1.size, list2.size, list3.size, list4.toJS()) // 4 5 6 15, [2, 4, 6, 8]

Map常用api

import { Map } from 'immutable'

const alpha = Map({
  a: 1,
  b: 2,
  c: 3
})
const objKeys = alpha.map((v, k) => k)
console.log(objKeys.join()) // a, b, c


const map1 = Map({
  a: 1,
  b: 2
})
const map2 = Map({
  c: 3,
  d: 4
})
const obj = {
  d: 400,
  e: 50
}

const mergedMap = map1.merge(map2, obj)

console.log(mergedMap.toObject())
console.log(mergedMap.toJS())

嵌套數據結構

const { fromJS } = require('immutable');
const nested = fromJS({ a: { b: { c: [ 3, 4, 5 ] } } });

const nested2 = nested.mergeDeep({ a: { b: { d: 6 } } });
// Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 6 } } }

console.log(nested2.getIn([ 'a', 'b', 'd' ])); // 6

const nested3 = nested2.updateIn([ 'a', 'b', 'd' ], value => value + 1);
console.log(nested3);
// Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 7 } } }

const nested4 = nested3.updateIn([ 'a', 'b', 'c' ], list => list.push(6));
// Map { a: Map { b: Map { c: List [ 3, 4, 5, 6 ], d: 7 } } }

在redux中使用immutable.js

redux官網推薦使用redux-immutable進行redux和immutable的集成。幾個注意點:

redux中,利用combineReducers來合並多個reduce, redux自帶的combineReducers只支持原生js形式的,所以需要使用redux-immutable提供的combineReducers來代替

// 使用redux-immutable提供的combineReducers方法替換redux里的combineReducers
import {combineReducers} from 'redux-immutable'
import reducerOne from './reducerOne'
import reducerTwo from './reducerTwo'
 
const rootReducer = combineReducers({
    reducerOne,
    reducerTwo
});
 
export default rootReducer;

reducer中的initialState也需要初始化成immutable類型, 比如一個counter的reducer

import { Map } from 'immutable'

import ActionTypes from '../actions'

const initialState = Map({
  count: 0
})

export default (state = initialState, action) => {
  switch (action.type) {
    case ActionTypes.INCREAMENT:
      return state.set('count', state.get('count') + 1) // 使用set或setIn來更改值, get或者getIn來取值
    case ActionTypes.DECREAMENT:
      return state.set('count', state.get('count') - 1)
    default:
      return state
  }
}

state成為了immutable類型,connectmapStateToProp也需要相應的改變

const mapStateToProps = state => ({
  count: state.getIn(['counter', 'count']) // 永遠不要在mapStateToProps里使用`toJS`方法,因為它永遠返回一個新的對象
})

shouldComponentUpdate里就可以使用immutable.is或者instance.equals來進行數據的對比了。

Mobx

Mobx是一個功能強大,上手非常容易的狀態管理工具。redux的作者也曾經向大家推薦過它,在不少情況下可以使用Mobx來替代掉redux。

官網有明確的核心概念使用方法,並配有egghead的視頻教程。這里就不一一贅述了。

要特別注意當使用 mobx-react 時可以定義一個新的生命周期鈎子函數 componentWillReact。當組件因為它觀察的數據發生了改變,它會安排重新渲染,這個時候 componentWillReact 會被觸發。這使得它很容易追溯渲染並找到導致渲染的操作(action)。

  • componentWillReact 不接收參數

  • componentWillReact 初始化渲染前不會觸發 (使用 componentWillMount 替代)

  • componentWillReact 對於 mobx-react@4+, 當接收新的 props 時並在 setState 調用后會觸發此鈎子

  • 要觸發componentWillReact必須在render里面用到被觀察的變量

  • 使用Mobx之后不會觸發componentWillReceiveProps


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM