淺析前端框架如何更新視圖

目錄

1、緣起
2、Prototype 與 jQuery
  Prototype
  jQuery
3、模板引擎
  實現原理
  jquery.tmpl
4、Virtual DOM
  簡史
  初探
  傳統 diff
  React
  Vue
複製代碼

1、緣起

1994 年,網景公司成立,發佈了第一款商業瀏覽器 Navigator。以後,微軟也推出了自家的 IE 瀏覽器。javascript

同年,W3C 小組成立,負責 HTML 的發展路徑,其宗旨是促進通用協議的發展。html

以後的 1995 年,JavaScript 誕生了。前端

有傳聞說是網景工程師布蘭登·艾克(Brendan Eich)只花了 10 天時間就設計出來的。但也由於工期過短的緣故,還存在許多瑕疵,所以一直被 「正統」 傳序員所嫌棄。java

早期 JavaScript 沒有包管理機制,也沒有像 Java、C++ 那樣的打輔助用的 SDK,內置的方法也不多。node

還有就是性能問題,關於使用 eval 仍是 Function,使用哪一種循環方式,該用parseInit 仍是 ~~ 等等的討論都是爲了提高那一點點的性能。react

JavaScript 主要語言特徵:jquery

  • 借鑑 C 語言的基本語法;
  • 借鑑 Java 語言的數據類型和內存管理;
  • 借鑑 Scheme 語言,將函數提高到"第一等公民"(first-class citizen)的地位;
  • 借鑑 Self 語言,使用基於原型(Prototype)的繼承機制。

wodeguo.jpg

2、Prototype 與 jQuery

Prototype、jQuery 等 js 庫的出現,在完善 JavaScript 的語言特性的同時也提升了 JavaScript 的性能。webpack

這兩個 js 庫均採用直接操做 Dom 的方式更新頁面。git

Prototype

這裏說的 Prototype 不是咱們如今熟知的對象的原型,而是一個名爲 Prototype 的 js 基礎類庫。由 Ruby 語言的大牛 Sam Stephenson 所寫。github

在 prototype.js 中,prototype 對象是實現面向對象的一個重要機制。同時 Prototype 還創造了 Function.prototype.bind,並在數組上增長了一大堆方法,其中不少方法也已經被標準化了。

jQuery

2006 年,jQuery 發佈。jQuery 發掘出大量的 DOM/BOM 兼容方案,解決了最頭疼的瀏覽器兼容性問題。

2009 年,jQuery 成功研發出 Sizzle 選擇器引擎,使其在性能上力壓一衆競品,Dojo、Prototype、ExtJS、MooTools 等。同時在處理 DOM 兼容上,發掘出大量的 DOM/BOM 兼容方案。

jQuery 以 DOM 爲中心,開發者能夠選一個或多個 DOM,轉變爲 jQuery 對象,而後進行 鏈式操做。

開發者們已開始注重先後端分離,並要求不能污染 Object 原型對象,不能污染 window 全局變量。jQuery 僅佔用兩個全局變量。jQuery 精巧的源碼實現使其大小壓縮後不到 30KB,網上涌現出大量關於其源碼詳解的書藉。

jQuery 的出現也大大下降了前端門檻,讓更多人進入了這個行業,我也是經過 jQuery 入的前端這個坑。

當時還有很多段子,「超市收銀員邊工做邊看前端書籍,一個月後就直接去互聯網公司作前端了」,諸如此類。

3、模板引擎

實現原理

在咱們使用 jQuery 時須要解決大段 HTML 的生成問題,雖然有 $.html$.append$before 等方法,可是爲了更好地管理不一樣的 HTML,咱們想將 HTML 分離出來,讓 HTML 獨立到不一樣的文件中,而後直接插數據。

1994 年 PHP 誕生,實現了將動態內容嵌入 HTML,提高了編寫效率和可讀性,其界定符、循環語句等的發明,直接或間接地影響了 JavaScript 前端模板引擎的出現。

模板引擎能夠簡單用一個公式裏描述:HTML = template(vars)

模板引擎的實現須要解決 模板存放、模板獲取、模板解析編譯 的問題

  • 模板存放:模板通常放置在 textarea/input 等表單控件,或 script 等標籤中
  • 模板獲取:一般會給模板設置 id,經過 id 獲取
  • 模板解析編譯
    • 須要將模板中的 JS 語句和 html 分離出來,不一樣模板引擎所用的分隔符也不太同樣,常見的有 {{...}} 或是 <%...%>
    • 經過區別一些特殊符號好比 each、if 等來將字符串拼接成函數式的字符串,將兩部分各自通過處理後,再次拼接到一塊兒
    • 最後將拼接好的字符串採用 new Function() 的方式轉化成所須要的函數。

jquery.tmpl

這裏以 jquery.tmpl 爲例,先來個小栗子

...
<body>
  <div id="div_demo"></div>
</body>
<!-- 模板1,測試${}、{{=}}標籤的使用 -->
<script id="demo" type="text/x-jquery-tmpl"> <div style="margin-bottom:10px;">   <span>${id}</span>   <span style="margin-left:10px;">{{= name}}</span>   <span style="margin-left:10px;">${age}</span>   <span style="margin-left:10px;">${number}</span> </div> </script>
<script type="text/javascript"> //手動初始化數據 var users = [ { id: 1, name: "xiaoming", age: 12, number: "001" }, { id: 2, name: "xiaowang", age: 13, number: "002" }, ]; //調用模板進行渲染 $("#demo").tmpl(users).appendTo("#div_demo"); </script>
...
複製代碼

jquery.tmpl 使用的模板存放於 id 爲 demo 的 script 標籤內

模板的讀取依靠 jQuery 的選擇器,直接以模板爲主體,調用 tmpl 解析數據,調用 jQuery 自帶的 appendTo 方法插入到父節點中

這裏模板的解析結合源碼看一下 (篇幅緣由,省略了部分代碼,完整代碼看這裏 buildTmplFn

function buildTmplFn(markup) {
  return new Function(
    "jQuery",
    "$item",
    "var $=jQuery,call,__=[],$data=$item.data;" +
      "with($data){__.push('" +
      jQuery
        .trim(markup) // 去先後空格
        .replace(/([\\'])/g, "\\$1") // 替換單引號
        .replace(/[\r\t\n]/g, " ") // 替換掉換行、退格符
        .replace(/\$\{([^\}]*)\}/g, "{{= $1}}") // 將 {{}} 語法統統換成 {{= }} 語法
        .replace(
          /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
          function (all, slash, type, fnargs, target, parens, args) {
            ...
            return (
              "');" +
              tag[slash ? "close" : "open"]
                .split("$notnull_1")
                .join(
                  target
                    ? "typeof(" +
                        target +
                        ")!=='undefined' && (" +
                        target +
                        ")!=null"
                    : "true"
                )
                .split("$1a")
                .join(exprAutoFnDetect)
                .split("$1")
                .join(expr)
                .split("$2")
                .join(fnargs || def.$2 || "") +
              "__.push('"
            );
          }
        ) +
      "');}return __;"
  );
}
複製代碼

buildTmplFn 也是經過處理模板字符,最終生成一個可執行的函數。模板的解析依靠正則實現,代碼雖少但卻實現了十分強大的模板能力。

最後返回的函數的函數體以下

var $ = jQuery,
  call,
  __ = [],
  $data = $item.data;
with ($data) {
  // === buildTmplFn 最後一個替換生成以下代碼====
  __.push('<div style="margin-bottom:10px;">   <span>');
  if (typeof id !== "undefined" && id != null) {
    __.push($.encode(typeof id === "function" ? id.call($item) : id));
  }
  __.push('</span>   <span style="margin-left:10px;">');
  if (typeof name !== "undefined" && name != null) {
    __.push($.encode(typeof name === "function" ? name.call($item) : name));
  }
  __.push('</span>   <span style="margin-left:10px;">');
  if (typeof age !== "undefined" && age != null) {
    __.push($.encode(typeof age === "function" ? age.call($item) : age));
  }
  __.push('</span>   <span style="margin-left:10px;">');
  if (typeof number !== "undefined" && number != null) {
    __.push(
      $.encode(typeof number === "function" ? number.call($item) : number)
    );
  }
  __.push(
    "</span> </div>"
    // =======
  );
}
return __;
複製代碼

最後生成的函數被執行,輸出帶有數據的 html 字符串,再插入到指定父節點中。

模板引擎更新視圖的方式即 替換指定 Dom 元素的全部子節點。

固然也存在其弊端,有部分的替換會引發 迴流。而且若是隻是修改個別數據,使用模板時須要從新渲染整片區域,這是沒有必要的,也是耗性能的。

4、Virtual DOM

簡史

時間來到 2009 年 NodeJs 誕生,隨着 NodeJS 的發展冒出一大堆模塊、路由、狀態管理、數據庫、MVC 框架(Backbone.js 也屬於 MVC 框架,強依賴於 jQuery)

以後大公司開始入局,MVVM 框架出現,比較有表明性的如:谷歌的 Angular,微軟的 Knockout.js,蘋果的 Ember.js,Facebook 的 React。

MVVM 的視圖模型是一個值轉換器,包括四個部分:

  • 模型 模型是指表明真實狀態內容的領域模型(面向對象),或指表明內容的數據訪問層(以數據爲中心)。
  • 視圖 就像在 MVC 和 MVP 模式中同樣,視圖是用戶在屏幕上看到的結構、佈局和外觀(UI)。
  • 視圖模型 視圖模型是暴露公共屬性和命令的視圖的抽象。MVVM 沒有 MVC 模式的控制器,也沒有 MVP 模式的 presenter,有的是一個綁定器。在視圖模型中,綁定器在視圖和數據綁定器之間進行通訊。
  • 綁定器 聲明性數據和命令綁定隱含在 MVVM 模式中。在 Microsoft 解決方案堆中,綁定器是一種名爲 XAML 的標記語言。綁定器使開發人員免於被迫編寫樣板式邏輯來同步視圖模型和視圖。在微軟的堆以外實現時,聲明性數據綁定技術的出現是實現該模式的一個關鍵因素。


圖片來源:維基百科

大公司的介入,無疑給開發者帶來巨大影響,畢竟 「迷信」 大公司是這一行的老傳統了,jQuery 由於沒有大公司支撐很快就被邊緣化了。

2013 Facebook 將 React 開源,支持 JSX 語法,一開始這種寫法讓人難以接受,在 2017 年 Facebook 推出 React Native,人們纔開始接受 JSX 這種寫法,也開始研究其背後的 虛擬 DOM 技術。
(因爲 JSX 須要額外編譯,又間接促成了 Babel 與 webpack 的壯大)

谷歌在發佈 Angular 時,同時發佈了一個名爲 Polymer 的框架,使用 Web Components 的瀏覽器自定義組件技術;雖然這個框架最後沒火起來,可是它將 Script、Style、Template 三種內容混在一個文件的設計,成功啓發了一個留美華人,搞出了 Vue.js,這人就是 尤雨溪


打成共識.jpg

最後提一下國內的特點終端——小程序

  • 底層運行的迷你 React 的虛擬 DOM
  • 內置組件是使用 Web Component
  • API 來源於 Hybird 的橋方法
  • 打包使用 webpack
  • 調試臺是 Chrome console 的簡化版
  • WXML、WXSS 的語法高亮也應該是 webpack 或 VS Code 的插件
  • 模塊機制是 Node.js 的 CommonJS

(爲了方便介紹,後文將使用 VD 指代 Virtual DOM)

初探

本質上來講,VD 只是一個簡單的 JS 對象,基礎屬性包括 標籤名(tag)、屬性(props) 和 子元素對象(children)。不一樣的框架對這三個屬性的命名會有點差異,但表達的意思基本是一致的。

如下是 Vue 中的 VD 結構

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  ...
}
複製代碼

下面是截取的 React 的 VD 結構,也就是 Fiber

export type Fiber = {
  tag: WorkTag,
  key: null | string,
  elementType: any,
  type: any,
  stateNode: any,
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ...
|};
複製代碼

兩邊都存在 tag 屬性,不一樣的地方是 Vue 中子節點存放於 children 中,而 React 經過 child 指向子節點,若是存在多個子節點,子節點再經過 sibling 屬性鏈接上其他的子節點,以下圖

reactVD.png

VD 與 Dom 對象具備一一對應的關係,藉助 VD 將頁面的狀態抽象爲 JS 對象,再配合不一樣的渲染工具,便可達到 跨平臺渲染 的效果。

在進行頁面更新的時候真實 DOM 的改變能夠藉助 VD 在內存中進行比較,也就是常說的 diff。

傳統 diff

使用 VD 的框架,通常的設計思路都是 頁面等於頁面狀態的映射,即 UI = render(state)。當須要更新頁面的時候,無需關心 DOM 具體的變換方式,只須要改變 state 便可,剩下的事情(render)將由框架代勞。

當 state 發生變化時,咱們從新生成整個 VD ,觸發比較的操做。
上述過程大體可分爲如下四步:

  • state 變化,生成新的 VD
  • 比較新舊 VD 的異同
  • 生成差別對象(patch)
  • 遍歷差別對象並更新 DOM

這裏咱們講一下傳統 diff 算法,就是將新舊兩棵 VD 樹的節點依次進行對比,最後再進行真實節點的更新。

diff.png

如上圖所示,左側新 VD 上的節點須要一一與右側舊 VD 的節點對比。爲了後續方便計算時間複雜度,咱們假設理想情況下新 VD 樹的節點個數與舊 VD 樹的節點個數都爲 n。

不少文章都會直接告訴你,傳統 diff 算法時間複雜度爲 O(n^3),至於爲何,那是衆說紛紜,這個說法的出處已經無從考證(有了解的小夥伴歡迎留言或私信)

疑惑.jpg

有兩種廣泛的說法:

  • 第一種是常規思路

    • 新 VD 樹任一節點與舊 VD 樹節點對比,時間複雜度爲 O(n)
    • 而新 VD 樹有 n 個節點,所以對比完新舊兩棵樹的全部節點,時間複雜度爲 O(n^2)
    • 遍歷完成後獲得兩棵樹的差別對象,嚴格來說這裏還涉及到最小距離轉換(transform cost)問題,這裏咱們能夠簡單理解爲遍歷舊 VD 樹(當前真實節點)完成更新操做;最終時間複雜度爲 O(n^3)
  • 第二種就複雜了,涉及到兩棵樹的編輯距離問題,講從 1979 到 2011,將樹的編輯距離算法的時間複雜度降到了 O(n^3),詳情戳這裏

最後說一下個人見解,我認爲 O(n^3)這個值應該是取的早期主流 diff 算法的時間複雜度的均值,畢竟咱們也不知道所謂的傳統 diff 算法到底長什麼樣,哪些算法能被稱爲傳統 diff 算法。

React

不打算展開,實在是篇幅不容許,後面再單獨出一篇 React diff 算法的。

React 的 diff 算法有個核心思路,即:結合框架的事務機制將屢次比較的結果合併後一次性更新到頁面,從而有效地減小頁面渲染的次數,提升渲染效率

Vue

這個也不打算展開,理由同上,Vue 還有個 3.0 版,更有的聊了。

Vue 的 diff 算法採用多指針(這裏指索引下標非內存地址),有的文章說雙指針,其實不止,嚴格來說有四個指針:

  • 新 VD 隊列的隊首
  • 新 VD 隊列的隊尾
  • 舊 VD 隊列的隊首
  • 舊 VD 隊列的隊尾

首尾兩個指針向中心移動,藉助原生 JS 的內置方法,「實時」 地更新真實節點

同時與 React 同樣,採用 key 來提高算法效率,藉助 map 以空間換時間來下降 diff 算法的時間複雜度

這些介紹都比較籠統,順手點個關注,來蹲一下 React/Vue diff 算法的解析?

客官來玩.jpg

文章同時發在我的公衆號 淺析前端框架如何更新視圖,歡迎關注 MelonField

參考:

相關文章
相關標籤/搜索