元件化的前世今生

duanhao發表於2021-09-09

很多年以前,我們寫網頁的時候都是這樣的:根據設計稿寫好一個頁面的html和css,然後再去寫js來做一些互動。如果遇到同樣功能的程式碼,最簡單粗暴的方式是複製貼上,如果為了更好的複用性,就封裝個jquery的外掛,需要用的時候就引入外掛,呼叫初始化的方法,傳入引數,比如一個日曆、一個輪播圖。在那個web野蠻生長的年代,這樣的外掛產生了很多,那個時代的必須會自定義jquery的外掛。那時候也有一些元件庫,比如extjs、bootstrap、jquery ui等。

但是這種元件的方案或者說jquery本身就有很多問題:

  • 瀏覽器端效率最低的就是dom操作,因為會觸發reflow,repaint,jquery是操作dom的一個庫,基於jquery封裝的外掛當然也避免不了頻繁的操作dom,所以這樣的的外掛如果程式碼寫的時候不注意,效率很可能會比較低。

  • jquery只是一個庫,而不是決定程式碼組織方式的框架,沒有固定的程式碼規範,每個人都會有自己的編碼風格,雖然可以規定一些規範,但畢竟不是強制的。如果團隊成員,專案規模比較小的時候還好,隨著專案、團隊規模的擴大,這樣的程式碼會越來越難以維護和複用。

現在的元件化的方案已經在那個時代的基礎上前進了很大一步。

一些常見的邏輯,我們會把他們封裝成函式或者類,比如BaseXxx、XxxUtils,牽扯到ui的元件複用的不只是邏輯,還有模板和樣式。也就是說一個元件需要封裝的就是關聯的html、css、js。

我們可以先想想如果我們自己去做一個元件化的框架,我們會怎麼做(主要考慮如何設計)。

如何去設計一個元件化的框架

模板,樣式,互動邏輯

元件最基礎的就是這三部分。樣式我們可以不做封裝,透過全域性引入然後加個名稱空間的方式來區分元件。模板可以掛載到dom樹上透過選擇器來取,或者直接傳入一段模板字串。互動邏輯的部分,我們會透過事件繫結呼叫元件上的一些方法。

 class Component{    constructor({el,template,onXxx}){      this.el  = el;      this.template = template;      this.onXxx = onXxx;      this.render();      this.bindEvents();
    }
    render(){         var ele = document.querySelector(this.el);
         ele.innerHTML = this.template;
   }
    bindEvents(){        this.el.querySelector('xx').addEventListener('click', this.onXxx)
    }
 }

現在我們的元件有了最初的模型,模板,邏輯,事件繫結,可以傳引數來進行一些定製

模板引擎

現在我們把需要把資料填充到模板需要用拼接字串的方式,這樣的程式碼寫起來很是繁瑣,針對這個問題,已經有了成熟的解決方案,我們可以選用某一個模板引擎,像ejs,jsmart,jade之類的。但是我們需要的是一個能和我們的元件結合緊密的一個模板引擎,我們需要自己實現一個,這樣,我們可以直接直接取元件中的資料,呼叫元件的某個方法,甚至自己擴充套件一些模板的功能。
比如,我們如果想實現這樣一個模板引擎,

  
                                                           
${goods.name}${goods.price}${goods.amount}

看上去是不是比較像jsp的語法,其實jsp就是一個專用的模板引擎,他有page,session,application,request,response等隱式物件,可以直接取幾個域中的資料,而且也可以支援自定義標籤和自定義el函式。
想想該怎麼實現。一種思路是透過xml的解析,xml解析方式有dom和sax兩種,就是分析出有什麼標籤有什麼屬性。然後對應的屬性做什麼操作。屬性和對應操作我們給封裝起來,叫做指令。開發者可以自己去註冊一些自定義的指令。模板在解析的時候解析出對應的屬性就會執行對應的操作。

透過模板解析的方式來初始化

我們元件用的時候,需要new一個元件的物件,傳入需要的引數。比如:

  new Component({     template:"

title

content

",     onXxx: function(){}  });

想一下,我們如果想不透過js來初始化,想透過下面這種方式來初始化該怎麼做,

我們之前自己實現了一個模板引擎,除了自定義指令的解析,當然也會把自定義元件的解析加進去。這樣一棵元件樹,我們只需要呼叫一次初始化方法,然後在解析元件樹模板的過程中,把一個個元件初始化,組裝好。這一些都是使用者感知不到的,使用者只需要寫模板。

雙向繫結MVVM

現在我們的元件還是避免不了要大量的操作dom,這必定會有很多的效能問題。能不能把dom操作也給封裝起來,開發者不需要再去操作dom,只需要管理好資料就可以了呢。
想一下後端開發,最頻繁的就是增刪改查,這樣的sql語句是經常要寫的,於是後端有了orm框架,比如hibernate,對映好實體類和資料表,類的屬性和欄位的關係之後,只需要呼叫hibernate提供的Session類的增刪改查的方法就好了,sql語句會自動生成,比如mybatis,對映好方法和寫在xml中的sql語句的關係,之後只要呼叫對應的方法就可以了,不需要自己去寫sql語句。
資料庫中的表和java的實體類建立了對映關係就能夠做到開發時不需要寫sql語句,那麼我們建立好資料和dom,也就是model和view之間的關係是不是也就可以不寫任何一句dom操作的程式碼,只去管理資料呢,然後view會自動同步呢。
當然是可以的,從model到view的繫結,我們可以監聽model的變化,變化的時候就去通知view中的Observer,然後那個Observer去操作dom,去更新檢視。
監聽model的變化,很容易想到的是es5中的Object.defineProperty這個api,他可以定義set方法,攔截對物件屬性的賦值操作。

 //觀察者的佇列
  var observers = [];
  observers.push(new Observer({...}));  var obj = {};  var value = "";  Object.defineProperty(obj, 'name', {    get: function() {      return value;
    },    set: function(val) {
       value = val;      //資料改變,通知觀察者,去更新view
      var target = this;
       observers.forEach(function(observer,index){
              observer.notify(target);
       });
    }
  });

當然es6提供的Proxy這個更高層次的封裝類也可以。

  // 觀察者的佇列
  var observers = [];
  observers.push(new Observer({...})); let obj = {}; let proxy = new Proxy(obj, {  get: function (target, key, receiver) {    return Reflect.get(target, key, receiver);
  },  set: function (target, key, value, receiver) {     Reflect.set(target, key, value, receiver);    
     for(let observer in observers){
         observer.notify(this);
     }
 }
})

至於從view到model的繫結,其實就是監聽使用者輸入的一些操作,監聽表單的事件,然後去根據使用者輸入的資料和對映關係,去同步model。

生命週期函式

我們把dom操作給封裝了,也就是把dom元素的增刪改給自動化了,元件對應的dom元素的建立和銷燬或者是重新繪製更新dom的時候,想做一些操作,就不能做了,所以我們要在這些時刻暴露一些鉤子,讓開發者可以在這些時候去做一些操作。比如元件的dom初次渲染完的時候要去請求資料,比如元件銷燬的時候要做一些資源釋放的工作避免記憶體洩漏等。主要的生命週期鉤子函式就這麼四類,建立前後,掛載到dom前後,更新前後,從dom中移除(銷燬)前後。
生命週期的名字可以叫beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed
也可以叫componentWillMountcomponentDidMountcomponentWillUpdatecomponentDidUpdatecomponentWillUnmountcomponentDidUnmount等。

虛擬dom和diff演算法

現在我們的元件渲染是直接渲染到dom元素,並且是全域性的渲染。model改變不大的時候,也會全域性重新渲染一次,會有很多不必要的dom操作,效能損耗。我們知道,計算機領域很多問題都可以加一箇中間層來解決,這裡也一樣,我們可以不直接渲染到真實dom元素,用js物件來模擬真實dom元素,每次渲染渲染成這樣的一顆虛擬dom元素組成的樹。

  {      name: 'a',      props: {
        
      },      children: [
          {              name: 'a-1',              props:{},              children:[]
          },
          {              name: 'a-2',              props:{},              children:[]
          },
          {              name: 'a-3',              props:{},              children:[]
          }
      ]
  }

這樣可以把上一次的渲染結果保留,下次渲染的時候和上一次的渲染結果做對比,比較有沒有變化,有變化的話找出變化的部分,區域性增量的渲染改變的部分。這樣能避免不必要的dom操作帶來的效能開銷。比較的過程我們可以叫他diff演算法。
引入了虛擬dom這一層,雖然會增大計算量和記憶體消耗,但是卻減少了大量的dom操作。效能會有明顯的提升。

Immutable

我們會在model變化以後去更新view,但是model有沒有變化需要和之前的model做對比,model是一個物件,可能層次比較深,深層的比較是比較慢的,這裡又會有效能的問題。針對這一問題,我們應該怎麼去最佳化呢?
我們都知道字串是常量。jvm的記憶體空間分為堆、棧、方法區、靜態域4個部分,方法區中有個字串常量池,來存放字串。也就是我們建立一個字串,如果常量池中有的話,他會直接把引用返回給你,如果沒有的話會建立一個字串然後放入常量池中。對字串的修改會建立一個新的字串,而不是直接修改原字串。程式語言基本都是這樣處理字串的,好處也是很明顯的,設想一下,如果有一個長度為1000的字串,要和另一個字串做比較,那麼如果字串不是常量,那麼完成比較就要要遍歷字串的每一個字元,複雜度為o(n)。但如果我們把字串設計為常量,比較時只需要比較兩個字串的記憶體地址,那麼複雜度就降到了o(1)。這種最佳化的思路是典型的空間換時間。

元件的model我們也可以實現為不可變(immutable)的,這樣比較的時候只需要比較兩個model的引用就可以了,會使效能又有一個大的提高。

fiber

想一想我們的元件化框架還有哪裡有問題。
我們知道瀏覽器中每個頁面是單執行緒的,渲染和js計算共用一個執行緒,會相互阻塞。
model改變後要生成虛擬dom,生成虛擬dom、虛擬dom之間的diff可能會計算比較長的時間,如果這時候頁面上有個動畫在同時搶佔著主執行緒,那麼勢必會導致動畫的卡頓。每個痛點的解決,都能會帶來效能的提升,為了追求極致的效能,這個問題我們也要想辦法解決。

虛擬dom是一顆樹形的結構,生成或比較一般都是遞迴的過程。我們知道所有的遞迴都可以改成迴圈的方式,只要我們可以一個佇列來儲存中間狀態。把遞迴改成迴圈後,就可以非同步化分段執行了。先執行一段計算,然後把執行狀態儲存,釋放主執行緒去做渲染,渲染完之後再去做之後的計算。這樣就完美的解決了瀏覽器環境下計算和渲染之間相互阻塞的問題了,效能有了進一步的提升。
這種資源的競爭在計算機中隨處可見,就像cpu的程式排程,每個程式的計算都要用到cpu,作業系統就需要用一種合理的方式來分配cpu資源。cpu排程策略有很多幾種,比如分時,按照優先順序等等,都是把一個大的計算量給分成多次來執行,暫停執行的時候把上下文資訊儲存下來,得到cpu的時候再恢復上下文繼續執行。
計算量分段,類似切菜,我們把這種排程策略叫fiber,即纖維化。
沒有fiber之前的虛擬dom計算是這樣的


圖片描述

fiber之後是這樣的


圖片描述

完美解決了瀏覽器的單執行緒下單次計算量過大會阻塞渲染的問題。

Component-Native

之前為了減少不必要的渲染,我們加了箇中間層-虛擬dom,除了可以帶來效能的提示之外,我們可以有一些別的思考,比如我可不可以不只渲染成dom元素,渲染成安卓、ios原生的元件?
經過思考,我們覺得這是可行的,邏輯依然用js來寫,透過jscore來執行js,js需要呼叫的原生api由框架封裝,提供給js。渲染部分,建立原生元件和和模板中元件的對映關係,渲染的時候生成對應的原生元件。邏輯的部分可以複用,除了渲染的是原生的元件,別的功能依然都有。


圖片描述


思路是可行的,但是實現這些元件、提供供js呼叫的原生api,工作量肯定比較大,而且會有很多坑。

全域性狀態管理

元件之間可以透過傳遞引數來通訊。如果只是父子元件通訊比較簡單,但是如果需要通訊的兩個元件之間間隔的層次比較多,或者是兄弟元件,那麼之間互相通訊就很麻煩了,需要多層的傳遞或者是透過父元件做中轉。針對這個問題,有沒有什麼別的思路呢?
其實可以引入一箇中介者來解決,就像婚姻中介,如果男方自己去找女方,或者女方自己去找男方都不太方便,這時候可以找一箇中介,男方和女方分別在那裡註冊自己的資訊,然後等中介有訊息的時候通知自己。這樣男方和女方就不需要相互聯絡,只要和婚姻中介聯絡就可以了。


圖片描述


類似的,我們可以建立一個store來儲存全域性的資訊,元件在store那裡註冊,當一個元件向store傳送訊息的時候,監聽store的元件就能收到訊息,從store中取出變化後的資料。


圖片描述

其他

關於元件的想象空間還有很大。未來可能會能夠渲染到所有的端,渲染過程中的每一個環節,每一個痛點都有相應的最佳化方案。效能、功能都可以不斷地提升。只要我們不要停止思考、停止敲程式碼的雙手。

現在主流的元件化的框架

我們從jquery外掛出發,思考了很多我們想要的元件化框架的樣子,回到現實,我們看一下現在主流的元件化的框架有哪些,他們各自都有哪些特性。

react

  • react支援jsx的語法,可以html和js混著寫,而不像模板引擎,需要去另外學習一套模板的語法。

  • 有了jsx,可以直接用

      ReactDOM.render(        ,       document.getElementById("container")
      )

透過解析jsx來初始化,而不需要手動去new一個元件物件。

  • react提供了從model到view的單向的繫結,state發生了變化,就會去render

  • react也提供了完善的生命週期函式供開發者在元件建立、更新、銷燬前後進擴充套件一些功能。而且提供了componentWillReceiveProps和shouldComponentUpdate兩個用於最佳化效能的生命週期函式。

componentWillReceiveProps是在元件接收到新的props,還沒有render之前呼叫,在這裡去呼叫setState更新狀態,不會觸發額外的render。shouldComponentUpdate是在state或props變化之後呼叫的,根據返回的結果決定是不是呼叫render, 可以和Immutable.js結合,來避免state的深層比較帶來的效能損耗。。

  • react 有虛擬dom這一層,並且會透過最佳化到的o(n)的diff演算法來進行虛擬dom的對比。

  • react是reconsiler(排程者),react-dom是renderer。react 16使用了fiber這個新的排程演算法。使得大計算量被拆解,提高了應用的可訪問性和效能。

  • react-native提供了可以渲染成安卓、ios元件的renderer,同時提供了原生的api供js呼叫。

  • 可以結合redux來做狀態管理

vue

  • vue提供了內建的專用的模板引擎,有指令、過濾器、插值表示式等功能,有內建的指令過濾器,也可以註冊自己擴充套件的指令過濾器。而且提供了render函式,可以結合babel來實現jsx的編譯。

  • vue提供了雙向繫結MVVM

  • vue有完善的生命週期函式,包括create前後,mount前後,update前後和destory前後

  • vue2.x加入了虛擬dom,可以減少不必要的渲染

  • vue社群有weex這個做原生渲染的框架

  • vue可以結合vuex來做全域性狀態管理

angular2

  • 支援模板的語法,指令、過濾器、插值表示式

  • decorator的方式來宣告元件

  • 支援IOC

  • 支援元件化

  • 支援雙向繫結MVVM

  • 建立、更新、銷燬前後的生命週期函式

  • 和typescript結合緊密

其他元件化的框架

實現元件化的框架很多,比如Avalon、Ember、Konckout等等,都有各自的特點

WebComponents

元件化是一個趨勢,現在有很多實現元件化的框架,W3C提出了web compoenents的標準:。這個標準主要由4種技術組成,html  import、shadow dom、custom  elment和html  template。新的標準肯定會有相容性的問題,goole推出了Polymer這個基於web components規範的元件化框架。

總結

從最開始的jquery外掛,到現在的各種元件化的框架、web components標準,元件化已經是一種必然的趨勢,我們不僅要會去設計、封裝元件,更要去了解元件的發展的前世今生,這樣才不會在框架的海洋中迷失。



作者:_神說要有光_
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1343/viewspace-2814116/,如需轉載,請註明出處,否則將追究法律責任。

相關文章