React Ways1——函數即組件

未經審視的代碼是不值得寫的javascript

​ —— 沃茲吉碩德html

React 中有一個經典的公式:前端

const View = f(data)
複製代碼

從這個公式裏咱們能夠提取出兩個特色:vue

  • 視圖由函數定義——函數即組件
  • 視圖的展現與 data 有關——數據驅動

接下來,咱們就從這兩點出發,來探討探討 React 的編程模式java

函數即組件——聲明式編程

函數即組件,顧名思義,就是一個函數就能夠是一個組件。 在 React 中,組件通常有兩種形式:react

  • 類組件git

    class MyClassComp extends React.Component {
    	render () {
    		return <div className="my-comp"></div>
    	}
    }
    複製代碼
  • 純函數組件(無狀態組件)github

    const MyPureFuncComp = () => (
    	<div className="my-comp"></div>
    )
    複製代碼

純函數描述的組件一目瞭然,可是類組件是否就不那麼「函數即組件」了呢?算法

這就像偶像劇的劇情同樣毫無驚喜——並不是如此。express

首先,咱們知道,在 JavaScript 中,class 其實更像是函數對象的語法糖,本質上仍是原型及原型鏈那一套,沒出圈兒!

其次,在實際的開發場景下,囿於當前的瀏覽器形勢,咱們產出的代碼更多時候須要兼容到 es5 這個沒有 class 概念的標準。

因此咱們會看到上面的 MyClassComp 在生產環境下會這樣產出:

"use strict";

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

var MyClassComp =
/*#__PURE__*/
function (_React$Component) {
  _inheritsLoose(MyClassComp, _React$Component);

  function MyClassComp() {
    return _React$Component.apply(this, arguments) || this;
  }

  var _proto = MyClassComp.prototype;

  _proto.render = function render() {
    return React.createElement("div", {
      className: "myClassComp"
    });
  };

  return MyClassComp;
}(React.Component);
複製代碼

其中 _inheritsLoose 函數用於實現繼承,在它下面,MyClassComp 被編譯成了一個函數!

好的,如今咱們無須擔憂函數即組件這個概念的準確性了。同時,自 Hooks 在 React 16.8 正式 release 後,函數寫法的組件會愈來愈多。

PS:代碼中的 /#*__PURE__*/ 的做用是將純函數(即無反作用)標記出來,方便編譯器在作 tree-shaking 的時候能夠放心的將其剝除

那麼,爲何 React 要使用函數做爲組件的最小單元呢?

答案就是聲明式編程(Declarative Programming)

聲明式編程

In computer science, declarative programming is a programming paradigm—a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow. ——Wiki

根據維基的解釋可知,與命令式編程相對立,聲明式編程更加註重於表現代碼的邏輯,而不是描述具體的過程。

也就是說,在聲明式編程的實踐過程當中,咱們須要更多的告知計算機咱們須要什麼——好比調用一個具體的函數,而不是用一些抽象的關鍵字來一行一行的實現咱們的需求。

在這個模型下,數據是不可變的,這就避免如死鎖等變量改變帶來的問題。

這不就是封裝方法?

是的,JavaScript 做爲一門基於對象的語言,封裝是很常見的 coding 方式。但封裝的目的是爲了經過對外提供接口來隱藏細節和屬性,增強對象的便捷性和安全性。而聲明式編程不只要將對象內部的細節封裝起來,更要將各類流程封裝起來,在須要實現該流程的時候能夠直接使用該封裝。

舉個例子,現有以下一個數據結構,咱們要經過一個方法將其中名字的各個部分用空格鏈接起來,而後返回一個新數組,

const raw = [
    {
        firstName: 'Daoming',
        lastName: 'Chen'
    },
    {
        firstName: 'Scarlett',
        lastName: 'Johnson'
    },
    {
        firstName: 'Samuel',
        lastName: 'Jackson'
    },
    {
        firstName: 'Kevin',
        lastName: 'Spacy'
    }
]
複製代碼

咱們很容易想到,一個 for 循環便可,

function getFullNames (data) {
    const res = []
    
    for (let i = 0; i < data.length; i++) {
        res.push(data[i].firstName + ' ' + data[i].lastName)
    }
    
    return res
}
複製代碼

那麼問題來了,以上的數據看起來至關的標準,所以咱們只須要鏈接 firstName 和 lastName。然而,有一天數據結構中加入了中間名,姑且叫 midName 吧,爲了正確的輸出全名,咱們不得不修改一下 getFullNames 方法——搞個 if…else 來判斷 midName 是否可用?這個思路能夠排除了,由於它並無考慮擴展性,若是未來錄入了一個不知道多少箇中間名的俄羅斯人怎麼辦?

好吧,咱們用 Object.keys() 先將全部部分提取出來,而後嵌套一個 for 循環將它們都拼在一塊兒。

這看起來並無什麼問題,但咱們仔細分析一下這個算法的目的——將名字用空格鏈接起來,咱們並不須要關心這些名字究竟屬於什麼部分,直須安順序將這些值提取就行——對,咱們能夠用 Object.values() 來實現這個改寫,

function getFullNames (data) {
    const res = []
    
    for (let i = 0; i < data.length; i++) {
        res.push(Object.values(data[i]).join(' '))
    }
    
    return
}
複製代碼

這甚至無須手動的拼接這些值,告訴計算機讓 join 來完成它。

PS:Object.values 屬於 es2017 標準,在使用它的時候須要加上對應的 preset 或 polyfill,固然了,也能夠在你的方法庫中實現一個。

It is the end?

No。既然咱們已經省去了一個 for 循環命令,何再也不省一個?來吧,

function getFullNames (data) {
    return data.map(item => Object.values(item).join(' '))
}

// 甚至再簡單一點
const getFullNames = data => data.map(item => Object.values(item).join(' '))
複製代碼

一行代碼!

想一想原來的命令式寫法,不支持中間名的狀況下就有 9 行,如果再嵌套一層循環,這個如此簡單的需求看着就不那麼簡單了。

咱們分析分析如今的 getFullNames:

  1. 咱們須要遍歷裏面全部的對象並返回一個新的數組,就調用 map 方法,它正好知足這個須要
  2. 對於裏面每一個元素,有未知的屬性個數,但實際上咱們只關注它們的值,那就用 Object.values 來提取這些值吧
  3. 拿到這些值後咱們要用空格將它們鏈接起來,Object.values 返回一個數組,那就順勢交給 join。
  4. 返回 map 的返回值
  5. 在該過程當中,沒有額外的變量引用源數據,而全部的方法也未對源數據作修改,其中,惟一出現的變量 item 在整個流程中值是不變的,在使用完畢以後立刻就會被回收,無污染性。因此,綜合來看,getFullNames 是安全的。

能夠看到,在這個分析過程當中,咱們注重的是流程的邏輯,而實現每一個邏輯點的時候,咱們均可以用現成的方式去獲得想要的結果,換言之,咱們是在一個個的求值過程當中去達到目的,而非在一大堆代碼中掙扎。

簡單總結一下聲明式編程的優勢:

  • 複用性:封裝是聲明式編程的一大要點,而封裝的主要目的之一就是複用代碼
  • 安全性:明確關注點,省去了沒必要要的變量聲明和對象引用,防止反作用。
  • 可讀性:方法的調用代替直接編寫流程,使得代碼更加直觀
  • 邏輯性:顯然經過語義化的方法名能更加清楚的體現代碼的邏輯

固然了,在這些優勢之下,對開發者的編程素質也有至關的要求。好比代碼規範,全部有意義的封裝都是爲了複用,那麼其規範性就必須得提起來,包括註釋以及代碼格式,咱們都知道良好的代碼規範是提高團隊編程效率的重中之重;其次,前面提到了「有意義的封裝」,這意味着,並不是全部的流程都須要隱藏起來,封裝到什麼程度?哪些東西須要被封裝?該如何封裝?這都是須要在實踐中逐漸總結的,咱們也稱其爲封裝的粒度問題。

好了,說了這麼多,back to React!

首先 React 的核心思想是組件化,其最小的粒度單元就是組件,還記得前面提到的嗎——函數即組件!

咱們能夠將這種思惟理解成,React 就是將一個個函數按照必定的邏輯關係組合起來,最終構建出咱們想要的應用。這也幾乎就是聲明式編程的思惟。

所以,一個好的 React 組件,也應當具備前文提到的聲明式編程的優勢,而且有更深的含義:

  • 複用性:對於組件來講,複用性體如今其是否與其餘組件有太多沒必要要的耦合,你不必定會真的複用它,可是保持其獨立性對於維護有着至關積極的意義

  • 安全性:因業務複雜程度的關係,組件不必定能保證徹底沒有反作用(幾乎不可能),可是它們對流程來講應當是透明可見的。也就是說,開發者應當知道一個組件會產生哪些反作用,以及它們會在其餘地方產生什麼影響,盡力使得總體依然是可控的。

  • 可讀性:這涉及到組件的總體設計,包含命名和接口等因素,舉個例子,咱們設計一個時針組件:

    // 時針的英文爲 hour hand,那麼咱們有以下的選擇
    const HH = () => (<div className="hh"></div>)
    const SZ = () => (<div className="sz" />)
    const HourHand = () => (<div className="hour-hand" />)
    const ShiZhen = () => (<div className="shizhen" />)
    複製代碼

    顯然,前兩種方式容易讓人摸不着頭腦,它們須要進一步的閱讀代碼才能推斷出其做用,這仍是對其邏輯性樂觀的狀況下。

    第三種方案則一目瞭然,幾乎沒有推理成本,對於項目的交接以及維護的便捷性都大有裨益。

    第四種方案呢,一樣一目瞭然,但這隻對懂漢語拼音的開發者有效,若是你的項目向全世界開源了,那麼對於外國友人來講,可能依然和沒開源同樣。

    好了,咱們肯定這個時針組件叫 HourHand 了,那咱們應該怎麼使用它呢?

    // 聯想到時鐘的形態,咱們首先會意識到的就是旋轉角度,那組件的接口或許是這樣
    import PropTypes from 'prop-types'
    
    const HourHand = props => (
    	<div className="hour-hand" style={{ transform: `rotate(${props.deg}deg)` }} /> ) HourHand.propTypes = { deg: PropTypes.number } 複製代碼

    這看起來並無什麼問題,可是從邏輯上來講,時針的含義是角度嗎?固然不是,應該是當前的小時,而咱們知道小時之間的角度偏移爲 30°,所以,爲了使其總體更具備邏輯性,咱們優化一下:

    import PropTypes from 'prop-types'
    
    const HourHand = props => (
    	<div className="hour-hand" style={{ transform: `rotate(${this.props.hour * 30}deg)` }} /> ) HourHand.propTypes = { hour: (props, propName) { if (props[propName] < 0 || props[propName] > 12) { return new Error('Hour must be a number between 0 and 12.') } } } 複製代碼

    如今這個時針組件的接口就與其自己的含義統一了,下次再使用它的時候,只需關注咱們熟悉的小時這個屬性,而不用再去關心應當轉換什麼角度——這個流程已經被封裝到組件內部了。

  • 邏輯性:其實這一點人爲的因素比較大,由於不管多麼優秀的編程模型,只要涉及到了業務,都能被 coding 成難以讀懂的代碼。而咱們使用 React 的最終目的就是實現咱們的業務需求,於是提高邏輯性須要咱們增強對應用的總體理解。

    不過這裏咱們能夠列舉一個 JSX 在邏輯性上的優點。

    在經過模板編譯的方式構建視圖的框架中,每每須要先在父組件中註冊子組件:

    <template>
    	<el-form>
            <el-form-item>
        		<el-input></el-input>
        	</el-form-item>
        </el-form>
    </template>
    
    <script>
        import { Form, FormItem } from 'element-ui'
        export default {
            components: {
                [Form.name]: Form,
                [FormItem.name]: FormItem
            }
        }
    </script>
    
    複製代碼

    這樣寫其實已經足夠語義化了,沒有問題。

    然而咱們再仔細想一想,其實視圖和邏輯自己是應該分離的,但在這個模式下咱們除了要在模板中查看組件結構以外,在邏輯中去關注組件的關係,而且 FormFormItem 的父子關係並未獲得體現。

    How about JSX's way?

    import { Form, Input } from 'antd'
    
    export default () => (
    	<Form> <Form.Item> <Input /> </Form.Item> </Form> ) 複製代碼

    顯然,在這種模式下,組件結構能夠完美的體現組件關係,咱們對視圖的關注只須要集中在這個 JSX 代碼塊中。

    其實,在這個問題上,分離一下關注點彷佛也沒什麼大不了(同時,許多框架也兼容了 JSX);在 components 裏註冊也能夠理解成是配置而非邏輯;甚至,根據習慣和 UI 庫實現的不一樣,咱們也可能解構的引入這些邏輯上的子組件。最重要的是,咱們如何在不一樣的模型下優化咱們的邏輯。

以上部份內容看起來有些像是在聊函數式編程,沒有錯,函數式編程是最多見的聲明式編程的子範式之一,在實際開發中,咱們還體會到許多函數式編程的理念。

以上就是對」函數即組件「這一大概念的基本詮釋,理解起來並不難,可是最重要的是如何將其最優的實踐到實際開發中,這是須要咱們不斷探索的。Ok,Let‘s next into the next plate。

數據驅動——單向數據流

在 JS 的世界中,對象是最基本的數據形式之一;而在 React 的世界中,驅動視圖更新的因可能來自 state 的更新,也可能來自 props 的更新——它們都是對象。也就是說數據決定了 React 應用的展現形態,這些形態當且僅當數據發生改變的時候纔會更新(這裏先回避直接操做 dom 的場景),這是徹徹底底的數據驅動

順勢的,咱們來理解一下 React 的數據驅動方式——單向數據流

單向數據流

單向數據流雙向數據流相對,是一種通道內只容許單向數據傳遞的數據流形式。也就是說,在同一鏈路上,只有一種數據流向,相似於通訊工程中的單工信道

而雙向數據流則是上容許同一鏈路有兩種數據流向,如 MVVM 的雙向綁定形式。其在概念上相似於雙工信道。進一步的,在代碼層面,同一時段只能有一種方向的工做,因此在實際的工做方式上它更接近半雙工

那麼,咱們就從兩種信道的角度來理解探索兩種數據流的特色及使用場景:

  • 單工信道一般被應用在電視、廣播等純內容輸出的場景。在這種狀況下,數據的來源及其下發的目標都是可追蹤可記錄的,而且能夠保證數據的統一性
  • 半雙工信道最多見的應用便是對講機,其特色是在一方在響應(發)的時候,另外一方只能接收(收)。若是兩邊同時響應(收發並行),那麼信道便不能正常工做了

回過頭來,咱們前端開發的主要目的就是將數據視覺化的輸出給用戶,那麼,單向數據流(單工信道)天然是具備得天獨厚的優點的——從 QA 到線上,前端的一大目標就是在同一狀態下對同一角色的用戶有同一的展現形式。

對,提到用戶,用戶的行爲是如何引發的視圖變化的呢?

再一次回到 React。前面咱們提到了,在 React 中全部的視圖變化都來自於數據的變化,而數據存儲在狀態中,所以,不管是用戶仍是其餘反作用,引發視圖變化的緣由都是他們修改了狀態,咱們看看在 React 中這是如何進行的:

class MyComp extends React.Component {
    state = {
        showName: true
    }

    hideName = () => {
        this.setState({
            showName: false
        })
    }

    render () {
		return (
        	<div> {this.state.showName ? (<div>Samuel</div>) : null} <button onClick={this.hideName}>hide name</button> </div>
        )
    }
}
複製代碼

在上面的代碼中,咱們實現了經過一個按鈕來隱藏顯示名字的組件,能夠看到,點擊按鈕後,會觸發 hideName 方法,而這個方法中只作了一件事,就是調用 setState 方法來修改 state,而 setState 方法則會去開啓更新視圖的流程。

看到了嗎,hideName 自己並不知道會對視圖會有什麼影響,它只是影響了狀態,而 render 才知道如何根據這些狀態來渲染界面。咱們能夠獲得一個簡單的示意圖:

single flow

如此就清晰多了——行爲到狀態一樣也是單向的!

所以,在 React 中,狀態到視圖的更新行爲到狀態的修改,是兩條相互獨立的通道,這意味着,在這個基礎上,全部的行爲和變化均可以追蹤,可控性很是的強。

這種模式就比如開發者爲應用構建了一個神經中樞,整個應用軀幹都受這個神經中樞的控制,若是應用出了問題,即可以在中樞中進行行爲的回溯,對症下藥。

同時,在前端很是流行的 Flux 應用架構也一樣採用了單向數據流模型,由其衍生出的各類狀態管理框架則在不斷地體現着這個模型的優越性。

與 雙向綁定 共存

說到這兒,咱們仍是得提一提雙向綁定,儘管 React 0.15 版開始就不提供這種數據綁定方式了,但它依然是被其餘框架所採用的現代前端開發的關鍵技術之一。

在 Vue 和 Angular 中,雙向綁定是一個很常見的交互處理方案,對於各種表單控件,它有很強的即時同步能力。然而,這也意味着,它的工做頻率會很是的高,對於一些規模較小的應用來講,這種雞毛蒜皮兒的小事兒影響可能不大,但應用一旦擴展起來,狀態樹將會愈來愈複雜,咱們就應該儘可能減小這種可控性較差的實踐。

譬如,Vuex 的誕生,在技術棧層面從新梳理了 Vue 的狀態管理方式,而 Vuex 的模式也是由 Flux 思想演變過來的,一樣具備單向數據流的特色。這時候,Vue 的開發者們能夠從新思考雙向綁定與總體狀態的結合形式,以在保證應用穩定性的狀況下最大化發揮這種高效數據處理方式的能力。

最後,咱們再進一步的考察一下 Vue 和 Angular 雙向綁定的本質看看會發生什麼,咱們以 input 爲例:

  • Vue:在 Vue 中,實現 input (包括 textarea) 標籤雙向綁定的源碼以下:

    // input 和 textarea 是比較基礎的表單組件,除此以外還有 genRadioModel、genCheckBoxModel 等方法進行對應的標籤綁定
    function genDefaultModel ( el: ASTElement, value: string, modifiers: ?ASTModifiers ): ?boolean {
      const type = el.attrsMap.type
      const { lazy, number, trim } = modifiers || {}
      const needCompositionGuard = !lazy && type !== 'range'
      
      // v-model.lazy 的實如今這裏
      const event = lazy
        ? 'change'
        : type === 'range'
          ? RANGE_TOKEN
          : 'input'
    
      let valueExpression = '$event.target.value'
      if (trim) {
        valueExpression = `$event.target.value.trim()`
      }
      if (number) {
        valueExpression = `_n(${valueExpression})`
      }
    
      // 看這裏,生成的 code 被傳入到了下面的 addHanlder 中 
      let code = genAssignmentCode(value, valueExpression)
      // 若是有輸入法守衛,就增長一個判斷,當正在輸入的時候不觸發 code
      if (needCompositionGuard) {
        code = `if($event.target.composing)return;${code}`
      }
    
      // 給該標籤設置 value 屬性
      addProp(el, 'value', `(${value})`)
      // 給該標籤添加事件處理函數
      addHandler(el, event, code, null, true)
      if (trim || number || type === 'number') {
        addHandler(el, 'blur', '$forceUpdate()')
      }
    }
    複製代碼

    總體下來,就是一個爲該元素添加 handler 的過程,深刻 genAssignmentCode 彷佛能夠找到數據綁定的答案,來看看:

    /** * Cross-platform codegen helper for generating v-model value assignment code. */
    export function genAssignmentCode ( value: string, assignment: string ): string {
      const res = parseModel(value)
      if (res.key === null) {
        return `${value}=${assignment}`
      } else {
        return `$set(${res.exp}, ${res.key}, ${assignment})`
      }
    }
    複製代碼

    一目瞭然,該方法的做用就是生成將genDetaultModel 中的 valueExpression 賦值給要綁定的 value ,要麼直接觸發 setter 以啓動依賴檢測,要麼經過 $set 方法通知檢測器——最終都是狀態樹更新引起數據下流帶來的視圖影響,本質上,這依然是單向數據流。那麼是否說明 MVVM 的本質實際上都是單向數據流呢?咱們繼續往下

    • Angular:Angular 經過 ngModel 指令進行雙向綁定,其源碼以下(篇幅較長只提取了重要部分,完整源碼可戳這裏):

      @Directive({
        selector: '[ngModel]:not([formControlName]):not([formControl])',
        providers: [formControlBinding],
        exportAs: 'ngModel'
      })
      export class NgModel extends NgControl implements OnChanges,OnDestroy {
        public readonly control: FormControl = new FormControl();
      
        /** * @description * Tracks the value bound to this directive. */
        // 根據註釋,這裏即指令要跟蹤的值
        @Input('ngModel') model: any;
      
        @Input('ngModelOptions')
        options !: {name?: string, standalone?: boolean, updateOn?: FormHooks};
      
        @Output('ngModelChange') update = new EventEmitter();
      
        // 定義 ngOnChange( Angular 對 change 事件的封裝) 的 handler
        ngOnChanges(changes: SimpleChanges) {
            this._checkForErrors();
            if (!this._registered) this._setUpControl();
            if ('isDisabled' in changes) {
                this._updateDisabled(changes);
            }
      
            if (isPropertyUpdated(changes, this.viewModel)) {
                // 調用 _updateValue 來應用新的值
                this._updateValue(this.model);
                this.viewModel = this.model;
            }            
         }           
      
                    
        // 用 control.setValue 給綁定的屬性賦值
        private _updateValue(value: any): void {
            resolvedPromise.then(
            () => { this.control.setValue(value, {emitViewToModelChange: false}); });
                   }
      
        }
      }
      複製代碼

      像以前同樣,咱們來看看這個 control.setValue 方法幹了些什麼:

      setValue(value: any, options: {
          onlySelf?: boolean,
          emitEvent?: boolean,
          emitModelToViewChange?: boolean,
          emitViewToModelChange?: boolean
        } = {}): void {
          (this as{value: any}).value = this._pendingValue = value;
          if (this._onChange.length && options.emitModelToViewChange !== false) {
            this._onChange.forEach(
                (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
          }
          this.updateValueAndValidity(options);
        }
      複製代碼

      從這裏知道,setValue 方法先是將新值下發給了 change 事件的訂閱者們,而後調用了 updateValueAndValidity

      可見,除了將新值賦值給 model 外,ngModel 還「手動」調用了相關的方法進行後續工做,說明在 Angular 中,ngModel 實現的是一個真正的雙向通道。

      綜上所述,在 Angular 中,單向數據流和雙向數據流共同存在。但更需注意的是,上面實現雙向綁定的過程當中用到的 control 對象,是 FormControl 類的一個實例,也就是說,Angular 將這種模式內聚到了表單控制器之中,使咱們有了明確的問題域,這便是在框架層面就將雙向綁定的場景進行了規劃,從而爲龐大而複雜的應用作了準備——聊到 Ng 的時候不就是幾乎在聊這樣的應用嗎?

總結

總結一下本文說了些什麼:

  • 函數即組件
    • 函數式 React 組件的本質
    • 這是一種聲明式編程的實踐
    • 聲明式編程使得代碼更加靈活、複用性更強
    • 理解這一切,構建更好的 React 應用
  • 單向數據流
    • 這是目前前端世界最流行的數據流形式
    • 它使得應用的數據和行爲能更好的被監聽和捕獲
    • 提高了應用表現的一致性
    • 它能夠與雙向數據流共存,方式的合理能夠發揮它們各自最大的效能

以上貫穿 React 開發的兩個最基本的概念,能夠說「函數即組件」是 React 應用的各個器官;「單向數據流」就是這個應用的血液管道,支持着各個組件呈現出它們應該的樣子;而開發者,就是大腦,爲應用注入了靈魂。

理解它們,咱們就知道了 React 的基本形態;而能在開發過程當中正確地實踐它們,應用將會更加優秀。

Hold it if you agree with it~!

若是您還沒有嘗試 React,或許本文並不能讓您立刻着手開發,若不嫌棄,還有後文。

若是您已經在 React 的世界中自由翱翔,但願本文能對您有益,或是獲得您的批評。

Thanks

相關文章
相關標籤/搜索