React16經常使用api解析以及原理剖析

React16經常使用api解析以及原理剖析

目錄

  1. VueReact 兩個框架的粗略區別對比
  2. react 16 版本常見 api
  3. react 生命週期
  4. react 事件機制
  5. react.Component 如何實現組件化以及高階組件的應用
  6. setState 異步隊列數據管理
  7. react Fiber 架構分析
  8. react hooks
  9. domdiff 算法
  10. snabbdom 源碼,是怎樣實現精簡的 Virtual DOM
  11. redux單向數據流架構如何設計

VueReact 兩個框架的粗略區別對比

Vue 的優點包括:css

  1. 模板和渲染函數的彈性選擇
  2. 簡單的語法及項目建立
  3. 更快的渲染速度和更小的體積

React 的優點包括:html

  1. 更適用於大型應用和更好的可測試性
  2. 同時適用於 Web 端和原生 App
  3. 更大的生態圈帶來的更多支持和工具

類似之處

React 與 Vue 有不少類似之處,React 和 Vue 都是很是優秀的框架,它們之間的類似之處多過不一樣之處,而且它們大部分最棒的功能是相通的:如他們都是 JavaScript 的 UI 框架,專一於創造前端的富應用。不一樣於早期的 JavaScript 框架「功能齊全」,Reat 與 Vue 只有框架的骨架,其餘的功能如路由、狀態管理等是框架分離的組件。前端

  • 二者都是用於建立 UI 的 JavaScript 庫;
  • 二者都快速輕便;
  • 都有基於組件的架構;
  • 都是用虛擬 DOM;
  • 均可放入單個 HTML 文件中,或者成爲更復雜 webpack 設置中的模塊;
  • 都有獨立但經常使用的路由器和狀態管理庫;
  • 它們之間的最大區別是 Vue 一般使用 HTML 模板文件,而 React 則徹底是 JavaScript。Vue 有雙向綁定語法糖。

不一樣點

  • Vue 組件分爲全局註冊和局部註冊,在 react 中都是經過 import 相應組件,而後模版中引用;
  • props 是能夠動態變化的,子組件也實時更新,在 react 中官方建議 props 要像純函數那樣,輸入輸出一致對應,並且不太建議經過 props 來更改視圖;
  • 子組件通常要顯示地調用 props 選項來聲明它期待得到的數據。而在 react 中沒必要需,另二者都有 props 校驗機制;
  • 每一個 Vue 實例都實現了事件接口,方便父子組件通訊,小型項目中不須要引入狀態管理機制,而 react 必需本身實現;
  • 使用插槽分發內容,使得能夠混合父組件的內容與子組件本身的模板;
  • 多了指令系統,讓模版能夠實現更豐富的功能,而 React 只能使用 JSX 語法;
  • Vue 增長的語法糖 computed 和 watch,而在 React 中須要本身寫一套邏輯來實現;
  • react 的思路是 all in js,經過 js 來生成 html,因此設計了 jsx,還有經過 js 來操做 css,社區的 styled-component、jss 等;而 vue 是把 html,css,js 組合到一塊兒,用各自的處理方式,vue 有單文件組件,能夠把 html、css、js 寫到一個文件中,html 提供了模板引擎來處理。
  • react 作的事情不多,不少都交給社區去作,vue 不少東西都是內置的,寫起來確實方便一些, 好比 redux 的 combineReducer 就對應 vuex 的 modules, 好比 reselect 就對應 vuex 的 getter 和 vue 組件的 computed, vuex 的 mutation 是直接改變的原始數據,而 redux 的 reducer 是返回一個全新的 state,因此 redux 結合 immutable 來優化性能,vue 不須要。
  • react 是總體的思路的就是函數式,因此推崇純組件,數據不可變,單向數據流,固然須要雙向的地方也能夠作到,好比結合 redux-form,組件的橫向拆分通常是經過高階組件。而 vue 是數據可變的,雙向綁定,聲明式的寫法,vue 組件的橫向拆分不少狀況下用 mixin。

社區活躍度

從二者的 github 表現來看(數據取於 2019-09-16)vue

react

react

能夠看出 vue 的 star 數量已是前端框架中最火爆的。從維護上來看,react 是 facebook 在維護,而 vue 現階段雖然也有了團隊,但主要仍是尤雨溪在維護貢獻代碼,而且阿里巴巴開源的混合式框架 weex 也是基於 vue 的,因此咱們相信 vue 將來將會獲得更多的人和團隊維護。node

根據不徹底統計,包括餓了麼、簡書、高德、稀土掘金、蘇寧易購、美團、天貓、荔枝 FM、房多多、Laravel、htmlBurger 等國內外知名大公司都在使用 vue 進行新項目的開發和舊項目的前端重構工做。react

使用 React 的公司 facebook、Twitter、INS、Airbnb、Yahoo、ThoughtWorks、螞蟻金服、阿里巴巴、騰訊、百度、口碑、美團、滴滴出行、餓了麼、京東、網易等。webpack

UI 生態

vue react
pc 端 iview、element 等 Ant Design、Materal-UI 等
h5 端 有贊 vant、mintui 等 Ant Design Mobile、weui
混合開發 weexui、bui-weex teaset、react-native-elements
微信小程序 iview、Weapp、zanui iView Weapp、Taro UI

不管您選擇React.js仍是Vue.js,兩個框架都沒有至關大的差別,根據您的要求,這個決定是很是主觀的。若是您想將前端JavaScript框架集成到現有應用程序中,Vue.js是更好的選擇,若是您想使用JavaScript構建移動應用程序,React絕對是您的選擇。git

react16 版本常見 api

先來看一下 react 暴露出來的 APIgithub

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
  unstable_Profiler: REACT_PROFILER_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals
}
複製代碼

Children

這個對象提供了一堆幫你處理 props.children 的方法,由於 children 是一個相似數組可是不是數組的數據結構,若是你要對其進行處理能夠用 React.Children 外掛的方法。web

createRef

新的 ref 用法,React 即將拋棄<div ref="myDiv" />這種 string ref 的用法,未來你只能使用兩種方式來使用 ref

class App extends React.Component {
  constructor() {
    this.ref = React.createRef()
  }

  render() {
    return <div ref={this.ref} />
    // or
    return <div ref={node => (this.funRef = node)} />
  }
}
複製代碼

createContext

createContext 是官方定稿的 context 方案,在這以前咱們一直在用的老的 context API 都是 React 不推薦的 API,如今新的 API 釋出,官方也已經肯定在 17 大版本會把老 API 去除(老 API 的性能不是通常的差)。

新 API 的使用方法:

const { Provider, Consumer } = React.createContext('defaultValue')

const ProviderComp = (props) => (
  <Provider value={'realValue'}> {props.children} </Provider>
)

const ConsumerComp = () => (
  <Consumer> {(value) => <p>{value}</p>} </Consumber>
)
複製代碼

react 生命週期

目前 react 16.8 +的生命週期分爲三個階段,分別是掛載階段、更新階段、卸載階段

  • 掛載階段: constructor(props): 實例化。
    static getDerivedStateFromPropsprops 中獲取 state
    render 渲染。
    componentDidMount: 完成掛載。
  • 更新階段: static getDerivedStateFromProps 從 props 中獲取 state。
    shouldComponentUpdate 判斷是否須要重繪。
    render 渲染。
    getSnapshotBeforeUpdate 獲取快照。
    componentDidUpdate 渲染完成後回調。
  • 卸載階段: componentWillUnmount 即將卸載。
  • 錯誤處理: static getDerivedStateFromError 從錯誤中獲取 state
    componentDidCatch 捕獲錯誤並進行處理。
class ExampleComponent extends react.Component {
  // 構造函數,最早被執行,咱們一般在構造函數裏初始化state對象或者給自定義方法綁定this
  constructor() {}
  //getDerivedStateFromProps(nextProps, prevState)用於替換 `componentWillReceiveProps` ,該函數會在初始化和 `update` 時被調用
  // 這是個靜態方法,當咱們接收到新的屬性想去修改咱們state,可使用getDerivedStateFromProps
  static getDerivedStateFromProps(nextProps, prevState) {
    // 新的鉤子 getDerivedStateFromProps() 更加純粹, 它作的事情是將新傳進來的屬性和當前的狀態值進行對比, 若不一致則更新當前的狀態。
    if (nextProps.riderId !== prevState.riderId) {
      return {
        riderId: nextProps.riderId
      }
    }
    // 返回 null 則表示 state 不用做更新
    return null
  }
  // shouldComponentUpdate(nextProps, nextState),有兩個參數nextProps和nextState,表示新的屬性和變化以後的state,返回一個布爾值,true表示會觸發從新渲染,false表示不會觸發從新渲染,默認返回true,咱們一般利用今生命週期來優化react程序性能
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.id !== this.props.id
  }
  // 組件掛載後調用
  // 能夠在該函數中進行請求或者訂閱
  componentDidMount() {}
  // getSnapshotBeforeUpdate(prevProps, prevState):這個方法在render以後,componentDidUpdate以前調用,有兩個參數prevProps和prevState,表示以前的屬性和以前的state,這個函數有一個返回值,會做爲第三個參數傳給componentDidUpdate,若是你不想要返回值,能夠返回null,今生命週期必須與componentDidUpdate搭配使用
  getSnapshotBeforeUpdate() {}
  // 組件即將銷燬
  // 能夠在此處移除訂閱,定時器等等
  componentWillUnmount() {}
  // 組件銷燬後調用
  componentDidUnMount() {}
  // componentDidUpdate(prevProps, prevState, snapshot):該方法在getSnapshotBeforeUpdate方法以後被調用,有三個參數prevProps,prevState,snapshot,表示以前的props,以前的state,和snapshot。第三個參數是getSnapshotBeforeUpdate返回的,若是觸發某些回調函數時須要用到 DOM 元素的狀態,則將對比或計算的過程遷移至 getSnapshotBeforeUpdate,而後在 componentDidUpdate 中統一觸發回調或更新狀態。
  componentDidUpdate() {}
  // 渲染組件函數
  render() {}
  // 如下函數不建議使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}
複製代碼

react 版本 17 將棄用幾個類組件 API 生命週期:componentWillMountcomponentWillReceivePropscomponentWillUpdate

react 事件機制

簡單的理解 react 如何處理事件的,React 在組件加載(mount)和更新(update)時,將事件經過 addEventListener 統一註冊到 document 上,而後會有一個事件池存儲了全部的事件,當事件觸發的時候,經過 dispatchEvent 進行事件分發。

引用新手學習 react 迷惑的點(二)

  • react 裏面綁定事件的方式和在 HTML 中綁定事件相似,使用駝峯式命名指定要綁定的 onClick 屬性爲組件定義的一個方法{this.handleClick.bind(this)}。
  • 因爲類的方法默認不會綁定 this,所以在調用的時候若是忘記綁定,this 的值將會是 undefined。 一般若是不是直接調用,應該爲方法綁定 this,將事件函數上下文綁定要組件實例上。

綁定事件的四種方式

class Button extends react.Component {
  constructor(props) {
    super(props)
    this.handleClick1 = this.handleClick1.bind(this)
  }
  //方式1:在構造函數中使用bind綁定this,官方推薦的綁定方式,也是性能最好的方式
  handleClick1() {
    console.log('this is:', this)
  }
  //方式2:在調用的時候使用bind綁定this
  handleClick2() {
    console.log('this is:', this)
  }
  //方式3:在調用的時候使用箭頭函數綁定this
  // 方式2和方式3會有性能影響而且當方法做爲屬性傳遞給子組件的時候會引發重渲問題
  handleClick3() {
    console.log('this is:', this)
  }
  //方式4:使用屬性初始化器語法綁定this,須要babel轉義
  handleClick4 = () => {
    console.log('this is:', this)
  }
  render() {
    return (
      <div> <button onClick={this.handleClick1}>Click me</button> <button onClick={this.handleClick2.bind(this)}>Click me</button> <button onClick={() => this.handleClick3}>Click me</button> <button onClick={this.handleClick4}>Click me</button> </div>
    )
  }
}
複製代碼

爲何直接調用方法會報錯

class Foo extends React.Component {
  handleClick() {
    this.setState({ xxx: aaa })
  }

  render() {
    return <button onClick={this.handleClick.bind(this)}>Click me</button>
  }
}
複製代碼

會被 babel 轉化成

React.createElement(
  'button',
  {
    onClick: this.handleClick
  },
  'Click me'
)
複製代碼

「合成事件」和「原生事件」

react 實現了一個「合成事件」層(synthetic event system),這抹平了各個瀏覽器的事件兼容性問題。全部事件均註冊到了元素的最頂層-document 上,「合成事件」會以事件委託(event delegation)的方式綁定到組件最上層,而且在組件卸載(unmount)的時候自動銷燬綁定的事件。

react 組件開發

react 組件化思想

一個 UI 組件的完整模板
import classNames from 'classnames'
class Button extends react.Component {
  //參數傳參與校驗
  static propTypes = {
    type: PropTypes.oneOf(['success', 'normal']),
    onClick: PropTypes.func
  }
  static defaultProps = {
    type: 'normal'
  }
  handleClick() {}
  render() {
    let { className, type, children, ...other } = this.props
    const classes = classNames(
      className,
      'prefix-button',
      'prefix-button-' + type
    )
    return (
      <span className={classes} {...other} onClick={() => this.handleClick}> {children} </span>
    )
  }
}
複製代碼

函數定義組件(Function Component)

純展現型的,不須要維護 state 和生命週期,則優先使用 Function Component

  1. 代碼更簡潔,一看就知道是純展現型的,沒有複雜的業務邏輯
  2. 更好的複用性。只要傳入相同結構的 props,就能展現相同的界面,不須要考慮反作用。
  3. 打包體積小,執行效率高
import react from 'react'
function MyComponent(props) {
  let { firstName, lastName } = props
  return (
    <div> <img src="avatar.png" className="profile" /> <h3>{[firstName, lastName].join(' ')}</h3> </div> ) } 複製代碼

會被 babel 轉義成

return React.createElement(
  'div',
  null,
  React.createElement('img', { src: 'avatar.png', className: 'profile' }),
  React.createElement('h3', null, [firstName, lastName].join(' '))
)
複製代碼

那麼,React.createElement 是在作什麼?看下相關部分代碼:

var ReactElement = function(type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allow us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner
  }
  // ...
  return element
}

ReactElement.createElement = function(type, config, children) {
  // ...
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  )
}
複製代碼

React.createElement()來構建 React 元素的。它接受三個參數,第一個參數type能夠是一個標籤名。如 div、span,或者 React 組件。第二個參數props爲傳入的屬性。第三個以及以後的參數children,皆做爲組件的子組件。

createElement 函數對 key 和 ref 等特殊的 props 進行處理,並獲取 defaultProps 對默認 props 進行賦值,而且對傳入的孩子節點進行處理,最終構形成一個 reactElement 對象(所謂的虛擬 DOM)。 reactDOM.render 將生成好的虛擬 DOM 渲染到指定容器上,其中採用了批處理、事務等機制而且對特定瀏覽器進行了性能優化,最終轉換爲真實 DOM。

ES6 class 定義一個純組件(PureComponent

組件須要維護 state 或使用生命週期方法,則優先使用 PureComponent

class MyComponent extends react.Component {
  render() {
    let { name } = this.props
    return <h1>Hello, {name}</h1>
  }
}
複製代碼
PureComponent

Component & PureComponent 這兩個類基本相同,惟一的區別是 PureComponent 的原型上多了一個標識,shallowEqual(淺比較),來決定是否更新組件,淺比較相似於淺複製,只會比較第一層。使用 PureComponent 至關於省去了寫 shouldComponentUpdate 函數

if (ctor.prototype && ctor.prototype.isPureReactComponent) {
  return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
}
複製代碼

這是檢查組件是否須要更新的一個判斷,ctor 就是你聲明的繼承自 Component or PureComponent 的類,他會判斷你是否繼承自 PureComponent,若是是的話就 shallowEqual 比較 state 和 props。

React 中對比一個 ClassComponent 是否須要更新,只有兩個地方。一是看有沒有 shouldComponentUpdate 方法,二就是這裏的 PureComponent 判斷

使用不可變數據結構 Immutablejs

Immutable.js 是 Facebook 在 2014 年出的持久性數據結構的庫,持久性指的是數據一旦建立,就不能再被更改,任何修改或添加刪除操做都會返回一個新的 Immutable 對象。可讓咱們更容易的去處理緩存、回退、數據變化檢測等問題,簡化開發。而且提供了大量的相似原生 JS 的方法,還有 Lazy Operation 的特性,徹底的函數式編程。

import { Map } from 'immutable'
const map1 = Map({ a: { aa: 1 }, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1 !== map2 // true
map1.get('b') // 2
map2.get('b') // 50
map1.get('a') === map2.get('a') // true
複製代碼

能夠看到,修改 map1 的屬性返回 map2,他們並非指向同一存儲空間,map1 聲明瞭只有,全部的操做都不會改變它。

ImmutableJS提供了大量的方法去更新、刪除、添加數據,極大的方便了咱們操縱數據。除此以外,還提供了原生類型與 ImmutableJS 類型判斷與轉換方法:

import { fromJS, isImmutable } from 'immutable'
const obj = fromJS({
  a: 'test',
  b: [1, 2, 4]
}) // 支持混合類型
isImmutable(obj) // true
obj.size() // 2
const obj1 = obj.toJS() // 轉換成原生 `js` 類型
複製代碼

ImmutableJS 最大的兩個特性就是: immutable data structures(持久性數據結構)與 structural sharing(結構共享),持久性數據結構保證數據一旦建立就不能修改,使用舊數據建立新數據時,舊數據也不會改變,不會像原生 js 那樣新數據的操做會影響舊數據。而結構共享是指沒有改變的數據共用一個引用,這樣既減小了深拷貝的性能消耗,也減小了內存。

好比下圖:

react-tree

左邊是舊值,右邊是新值,我須要改變左邊紅色節點的值,生成的新值改變了紅色節點到根節點路徑之間的全部節點,也就是全部青色節點的值,舊值沒有任何改變,其餘使用它的地方並不會受影響,而超過一大半的藍色節點仍是和舊值共享的。在 ImmutableJS 內部,構造了一種特殊的數據結構,把原生的值結合一系列的私有屬性,建立成 ImmutableJS 類型,每次改變值,先會經過私有屬性的輔助檢測,而後改變對應的須要改變的私有屬性和真實值,最後生成一個新的值,中間會有不少的優化,因此性能會很高。

高階組件(higher order component)

高階組件是一個以組件爲參數並返回一個新組件的函數。HOC 運行你重用代碼、邏輯和引導抽象。

function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props
      if (visible === false) return null
      return <WrappedComponent {...props} /> } } } 複製代碼

上面的代碼就是一個 HOC 的簡單應用,函數接收一個組件做爲參數,並返回一個新組件,新組建能夠接收一個 visible props,根據 visible 的值來判斷是否渲染 Visible。 最多見的還有 Redux 的 connect 函數。除了簡單分享工具庫和簡單的組合,HOC 最好的方式是共享 react 組件之間的行爲。若是你發現你在不一樣的地方寫了大量代碼來作同一件事時,就應該考慮將代碼重構爲可重用的 HOC。 下面就是一個簡化版的 connect 實現:

export const connect = (
  mapStateToProps,
  mapDispatchToProps
) => WrappedComponent => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor() {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount() {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps() {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {}
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {}
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render() {
      return <WrappedComponent {...this.state.allProps} /> } } return Connect } 複製代碼

代碼很是清晰,connect 函數其實就作了一件事,將 mapStateToPropsmapDispatchToProps 分別解構後傳給原組件,這樣咱們在原組件內就能夠直接用 props 獲取 state 以及 dispatch 函數了。

高階組件的應用

某些頁面須要記錄用戶行爲,性能指標等等,經過高階組件作這些事情能夠省去不少重複代碼。

日誌打點
function logHoc(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      this.start = Date.now()
    }
    componentDidMount() {
      this.end = Date.now()
      console.log(
        `${WrappedComponent.dispalyName} 渲染時間:${this.end - this.start} ms`
      )
      console.log(`${user}進入${WrappedComponent.dispalyName}`)
    }
    componentWillUnmount() {
      console.log(`${user}退出${WrappedComponent.dispalyName}`)
    }
    render() {
      return <WrappedComponent {...this.props} /> } } } 複製代碼
可用、權限控制
function auth(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, auth, display = null, ...props } = this.props
      if (visible === false || (auth && authList.indexOf(auth) === -1)) {
        return display
      }
      return <WrappedComponent {...props} /> } } } 複製代碼
表單校驗

基於上面的雙向綁定的例子,咱們再來一個表單驗證器,表單驗證器能夠包含驗證函數以及提示信息,當驗證不經過時,展現錯誤信息:

function validateHoc(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props)
      this.state = { error: '' }
    }
    onChange = event => {
      const { validator } = this.props
      if (validator && typeof validator.func === 'function') {
        if (!validator.func(event.target.value)) {
          this.setState({ error: validator.msg })
        } else {
          this.setState({ error: '' })
        }
      }
    }
    render() {
      return (
        <div> <WrappedComponent onChange={this.onChange} {...this.props} /> <div>{this.state.error || ''}</div> </div> ) } } } 複製代碼
const validatorName = {
  func: (val) => val && !isNaN(val),
  msg: '請輸入數字'
}
const validatorPwd = {
  func: (val) => val && val.length > 6,
  msg: '密碼必須大於6位'
}
<HOCInput validator={validatorName} v_model="name"></HOCInput>
<HOCInput validator={validatorPwd} v_model="pwd"></HOCInput>
複製代碼

HOC 的缺陷

  • HOC 須要在原組件上進行包裹或者嵌套,若是大量使用 HOC,將會產生很是多的嵌套,這讓調試變得很是困難。
  • HOC 能夠劫持 props,在不遵照約定的狀況下也可能形成衝突。

render props

一種在 React 組件之間使用一個值爲函數的 prop 共享代碼的簡單技術 具備 render prop 的組件接受一個函數,該函數返回一個 React 元素並調用它而不是實現本身的渲染邏輯。

<DataProvider render={data => <h1>Hello {data.target}</h1>} />
複製代碼

setState 數據管理

**不要直接更新狀態**
// Wrong 此代碼不會從新渲染組件,構造函數是惟一可以初始化 this.state 的地方。
this.state.comment = 'Hello'
// Correct 應當使用 setState():
this.setState({ comment: 'Hello' })
複製代碼
組件生命週期中或者 react 事件綁定中,setState 是經過異步更新的,在延時的回調或者原生事件綁定的回調中調用 setState 不必定是異步的。
  • 多個 setState() 調用合併成一個調用來提升性能。
  • this.props 和 this.state 多是異步更新的,不該該依靠它們的值來計算下一個狀態。
// Wrong
this.setState({
  counter: this.state.counter + this.props.increment
})
// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}))
複製代碼
原生事件綁定不會經過合成事件的方式處理,會進入更新事務的處理流程。`setTimeout` 也同樣,在 `setTimeout` 回調執行時已經完成了原更新組件流程,不會放入 `dirtyComponent` 進行異步更新,其結果天然是同步的。

setState 原理

setState 並無直接操做去渲染,而是執行了一個 updateQueue(異步 updater 隊列),

setState( stateChange ) {
    Object.assign( this.state, stateChange );
    //合併接收到的state||stateChange改變的state(setState接收到的參數)
    renderComponent( this );//調用render渲染組件
}
複製代碼

這種實現,每次調用 setState 都會更新 state 並立刻渲染一次(不符合其更新優化機制),因此咱們要合併 setState

具體能夠閱讀源碼 ReactUpdateQueue.js

react 中的事務實現

待完善 這塊看的還有點懵圈 React 源碼解析(三):詳解事務與更新隊列 React 中的 Transaction React 的事務機制

transaction 事務

ErrorBoundarySuspenseFragment

Error Boundaries

react 16 提供了一個新的錯誤捕獲鉤子 componentDidCatch(error, errorInfo), 它能將子組件生命週期裏所拋出的錯誤捕獲, 防止頁面全局崩潰。demo componentDidCatch 並不會捕獲如下幾種錯誤

  • 事件機制拋出的錯誤(事件裏的錯誤並不會影響渲染)
  • Error Boundaries 自身拋出的錯誤
  • 異步產生的錯誤
  • 服務端渲染

lazy、Suspence 延遲加載組件

lazy 須要跟 Suspence 配合使用,不然會報錯。

lazy 其實是幫助咱們實現代碼分割 ,相似 webpack 的 splitchunk 的功能。

Suspense 意思是能暫停當前組件的渲染, 當完成某件事之後再繼續渲染。簡單來講就是減小首屏代碼的體積,提高性能。

import react, { lazy, Suspense } from 'react'
const OtherComponent = lazy(() => import('./OtherComponent'))
function MyComponent() {
  return (
    <Suspense fallback={<div>loading...</div>}> <OtherComponent /> </Suspense>
  )
}
複製代碼

一種簡單的預加載思路, 可參考 preload

const OtherComponentPromise = import('./OtherComponent')
const OtherComponent = react.lazy(() => OtherComponentPromise)
複製代碼

Fragments(v16.2.0)

Fragments 容許你將子列表分組,避免向 DOM 添加額外的節點。

render() {
  return (
    <> <ChildA /> <ChildB /> <ChildC /> </> ); } 複製代碼

react Fiber 架構分析

react-fiber是爲了加強動畫、佈局、移動端手勢領域的適用性,最重要的特性是對頁面渲染的優化: 容許將渲染方面的工做拆分爲多段進行。

react Fiber 架構解決了什麼問題

react-fiber 能夠爲咱們提供以下幾個功能:

  • 設置渲染任務的優先
  • 採用新的 Diff 算法
  • 採用虛擬棧設計容許當優先級更高的渲染任務和較低優先的任務之間來回切換

Fiber 如何作到異步渲染 Virtual DomDiff 算法

衆所周知,畫面每秒鐘更新 60 次,頁面在人眼中顯得流暢,無明顯卡頓。每秒 60 次,即 16ms 要更新一次頁面,若是更新頁面消耗的時間不到 16ms,那麼在下一次更新時機來到以前會剩下一點時間執行其餘的任務,只要保證及時在 16ms 的間隔下更新界面就徹底不會影響到頁面的流暢程度。fiber 的核心正是利用了 60 幀原則,實現了一個基於優先級和 requestIdleCallback 的循環任務調度算法。

function fiber(剩餘時間) {
  if (剩餘時間 > 任務所需時間) {
    作任務
  } else {
    requestIdleCallback(fiber)
    // requestIdleCallback 是瀏覽器提供的一個 api,可讓瀏覽器在空閒的時候執行回調,
    // 在回調參數中能夠獲取到當前幀剩餘的時間,fiber 利用了這個參數,
    // 判斷當前剩下的時間是否足夠繼續執行任務,
    // 若是足夠則繼續執行,不然暫停任務,
    // 並調用 requestIdleCallback 通知瀏覽器空閒的時候繼續執行當前的任務
  }
}
複製代碼

react hooks

在 react 16.7 以前, react 有兩種形式的組件, 有狀態組件(類)和無狀態組件(函數)。 官方解釋: hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。 我的理解:讓傳統的函數組件 function component 有內部狀態 state 的函數 function,簡單來講就是 hooks 讓函數組件有了狀態,能夠徹底替代 class。

接下來梳理 Hooks 中最核心的 2 個 api, useStateuseEffect

useState

useState 是一個鉤子,他能夠爲函數式組件增長一些狀態,而且提供改變這些狀態的函數,同時它接收一個參數,這個參數做爲狀態的默認值。

const [count, setCount] = useState(initialState)
複製代碼

使用 Hooks 相比以前用 class 的寫法最直觀的感覺是更爲簡潔

function App() {
  const [count, setCount] = useState(0)
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
  )
}
複製代碼

useEffect(fn)

在每次 render 後都會執行這個鉤子。能夠將它當成是 componentDidMountcomponentDidUpdate``、componentWillUnmount 的合集。所以使用 useEffect 比以前優越的地方在於:

能夠避免在 componentDidMountcomponentDidUpdate 書寫重複的代碼; 能夠將關聯邏輯寫進一個 useEffect(在之前得寫進不一樣生命週期裏);

深刻理解 react 原理

react 虛擬 dom 原理剖析

react 組件的渲染流程

使用 react.createElement 或 JSX 編寫 react 組件,實際上全部的 JSX 代碼最後都會轉換成 react.createElement(...),Babel 幫助咱們完成了這個轉換的過程。

createElement 函數對 key 和 ref 等特殊的 props 進行處理,並獲取 defaultProps 對默認 props 進行賦值,而且對傳入的孩子節點進行處理,最終構形成一個 reactElement 對象(所謂的虛擬 DOM)。

reactDOM.render 將生成好的虛擬 DOM 渲染到指定容器上,其中採用了批處理、事務等機制而且對特定瀏覽器進行了性能優化,最終轉換爲真實 DOM。

虛擬 DOM 的組成

reactElementelement 對象,咱們的組件最終會被渲染成下面的結構:

`type`:元素的類型,能夠是原生 html 類型(字符串),或者自定義組件(函數或 class)
`key`:組件的惟一標識,用於 Diff 算法,下面會詳細介紹
`ref`:用於訪問原生 dom 節點
`props`:傳入組件的 props,chidren 是 props 中的一個屬性,它存儲了當前組件的孩子節點,能夠是數組(多個孩子節點)或對象(只有一個孩子節點)
`owner`:當前正在構建的 Component 所屬的 Component
`self`:(非生產環境)指定當前位於哪一個組件實例
`_source`:(非生產環境)指定調試代碼來自的文件(fileName)和代碼行數(lineNumber)
複製代碼

當組件狀態 state 有更改的時候,react 會自動調用組件的 render 方法從新渲染整個組件的 UI。 固然若是真的這樣大面積的操做 DOM,性能會是一個很大的問題,因此 react 實現了一個 Virtual DOM,組件 DOM 結構就是映射到這個 Virtual DOM上,react 在這個 Virtual DOM 上實現了一個 diff 算法,當要從新渲染組件的時候,會經過 diff 尋找到要變動的 DOM 節點,再把這個修改更新到瀏覽器實際的 DOM 節點上,因此實際上不是真的渲染整個 DOM 樹。這個 Virtual DOM 是一個純粹的 JS 數據結構,因此性能會比原生 DOM 快不少。

react 是如何防止 XSS

reactElement 對象還有一個$$typeof屬性,它是一個 Symbol 類型的變量Symbol.for('react.element'),當環境不支持 Symbol 時,$$typeof 被賦值爲 0xeac7。 這個變量能夠防止 XSS。若是你的服務器有一個漏洞,容許用戶存儲任意 JSON 對象, 而客戶端代碼須要一個字符串,這可能爲你的應用程序帶來風險。JSON 中不能存儲 Symbol 類型的變量,而 react 渲染時會把沒有\$\$typeof 標識的組件過濾掉。

diff 算法

傳統的 diff 算法經過循環遞歸對節點一次對比,效率很低,算法複雜度達到 O(n^3),其中 n 是樹中節點的總數,React 經過制定大膽的策略,將 O(n^3) 複雜度的問題轉換成 O(n) 複雜度的問題。

diff 策略:

  1. web ui 中 Dom 節點跨層級的移動操做不多,diff 算法比較新舊節點的時候,比較只會在同層級比較,不會跨層級比較
  2. 擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構。
  3. 對於同一層級的一組子節點,他們能夠經過惟一 key 進行區分

基於以上三個前提策略,React 分別對 tree diffcomponent diff 以及 element diff 進行算法優化,事實也證實這三個前提策略是合理且準確的,它保證了總體界面構建的性能。 簡單的講就是:

具體能夠參考React 源碼剖析系列 - 難以想象的 react diff

  • React 經過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
  • React 經過分層求異的策略,對 tree diff 進行算法優化;
  • React 經過相同類生成類似樹形結構,不一樣類生成不一樣樹形結構的策略,對 component diff 進行算法優化;
  • React 經過設置惟一 key 的策略,對 element diff 進行算法優化;

建議,在開發組件時,保持穩定的 DOM 結構會有助於性能的提高; 建議,在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。

snabbdom 源碼,是怎樣實現精簡的 Virtual DOM 的

待補充

react 性能分析與優化

減小沒必要要的渲染

在使用 class Component 進行開發的時候,咱們可使用 shouldComponentUpdate 來減小沒必要要的渲染,那麼在使用 react hooks 後,咱們如何實現這樣的功能呢?

解決方案:React.memouseMemo 對於這種狀況,react 固然也給出了官方的解決方案,就是使用 React.memo 和 useMemo。

React.memo

React.momo 其實並非一個 hook,它其實等價於 PureComponent,可是它只會對比 props。使用方式以下(用上面的例子):

import React, { useState } from 'react'

export const Count = React.memo(props => {
  const [data, setData] = useState({
    count: 0,
    name: 'cjg',
    age: 18
  })

  const handleClick = () => {
    const { count } = data
    setData({
      ...data,
      count: count + 1
    })
  }

  return <button onClick={handleClick}>count:{data.count}</button>
})
複製代碼
useMemo

useMemo 它的用法其實跟 useEffects 有點像,咱們直接看官方給的例子

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a])
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b])
  return (
    <> {child1} {child2} </> ) } 複製代碼

從例子能夠看出來,它的第二個參數和 useEffect 的第二個參數是同樣的,只有在第二個參數數組的值發生變化時,纔會觸發子組件的更新。

引用React hooks 實踐

使用 shouldComponentUpdate() 防止沒必要要的從新渲染

當一個組件的 props 或 state 變動,React 會將最新返回的元素與以前渲染的元素進行對比,以此決定是否有必要更新真實的 DOM,當它們不相同時 React 會更新該 DOM。

即便 React 只更新改變了的 DOM 節點,從新渲染仍然花費了一些時間。在大部分狀況下它並非問題,可是若是渲染的組件很是多時,就會浮現性能上的問題,咱們能夠經過覆蓋生命週期方法 shouldComponentUpdate 來進行提速。

shouldComponentUpdate 方法會在從新渲染前被觸發。其默認實現老是返回 true,若是組件不須要更新,能夠在 shouldComponentUpdate 中返回 false 來跳過整個渲染過程。其包括該組件的 render 調用以及以後的操做。

shouldComponentUpdate(nextProps, nextState) {
   return nextProps.next !== this.props.next
}
複製代碼

React 性能分析器

React 16.5 增長了對新的開發者工具 DevTools 性能分析插件的支持。 此插件使用 React 實驗性的 Profiler API 來收集有關每一個組件渲染的用時信息,以便識別 React 應用程序中的性能瓶頸。 它將與咱們即將推出的 time slicing(時間分片) 和 suspense(懸停) 功能徹底兼容。

redux

Store:保存數據的地方,你能夠把它當作一個容器,整個應用只能有一個 Store

StateStore 對象包含全部數據,若是想獲得某個時點的數據,就要對 Store 生成快照,這種時點的數據集合,就叫作 State

ActionState 的變化,會致使 View 的變化。可是,用戶接觸不到 State,只能接觸到 View。因此,State 的變化必須是 View 致使的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。

Action Creator:View 要發送多少種消息,就會有多少種 Action。若是都手寫,會很麻煩,因此咱們定義一個函數來生成 Action,這個函數就叫 Action Creator

ReducerStore 收到 Action 之後,必須給出一個新的 State,這樣 View 纔會發生變化。這種 State 的計算過程就叫作 ReducerReducer 是一個函數,它接受 Action 和當前 State 做爲參數,返回一個新的 State

dispatch:是 View 發出 Action 的惟一方法。

redux 的基本原理

而後咱們過下整個工做流程:

首先,用戶(經過 View)發出 Action,發出方式就用到了 dispatch 方法。

而後,Store 自動調用 Reducer,而且傳入兩個參數:當前 State 和收到的 ActionReducer 會返回新的 State

State 一旦有變化,Store 就會調用監聽函數,來更新 View

到這兒爲止,一次用戶交互流程結束。能夠看到,在整個流程中數據都是單向流動的,這種方式保證了流程的清晰。

redux 單向數據流架構如何設計

待完善

redux 中間件

Redux 的中間件提供的是位於 action 被髮起以後,到達 reducer 以前的擴展點,換而言之,本來 view -> action -> reducer -> store 的數據流加上中間件後變成了 view -> action -> middleware -> reducer -> store ,在這一環節咱們能夠作一些 「反作用」 的操做,如 異步請求、打印日誌等。

redux 中間件經過改寫 store.dispatch 方法實現了 action -> reducer 的攔截,從上面的描述中能夠更加清晰地理解 redux 中間件的洋蔥圈模型:

中間件A -> 中間件B-> 中間件C-> 原始 dispatch -> 中間件C -> 中間件B ->  中間件A
複製代碼

這也就提醒咱們使用中間件時須要注意這個中間件是在何時 「搞事情」 的,好比 redux-thunk 在執行 next(action) 前就攔截了類型爲 function 的 action,而 redux-saga 就在 next(action) 纔會觸發監聽 sagaEmitter.emit(action), 並不會攔截已有 action 到達 reducer。

參考:

  1. 深刻分析虛擬 DOM 的渲染原理和特性
  2. react 事件機制
  3. 從 Mixin 到 HOC 再到 Hook
  4. 美團技術團隊-Redux 從設計到源碼
  5. 解析 snabbdom 源碼,教你實現精簡的 Virtual DOM 庫
  6. react 源碼解析
  7. Vue 與 React 兩個框架的粗略區別對比
相關文章
相關標籤/搜索