1、緣起
2、Prototype 與 jQuery
Prototype
jQuery
3、模板引擎
實現原理
jquery.tmpl
4、Virtual DOM
簡史
初探
傳統 diff
React
Vue
複製代碼
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)的繼承機制。
Prototype、jQuery 等 js 庫的出現,在完善 JavaScript 的語言特性的同時也提升了 JavaScript 的性能。webpack
這兩個 js 庫均採用直接操做 Dom 的方式更新頁面。git
這裏說的 Prototype 不是咱們如今熟知的對象的原型,而是一個名爲 Prototype 的 js 基礎類庫。由 Ruby 語言的大牛 Sam Stephenson 所寫。github
在 prototype.js 中,prototype 對象是實現面向對象的一個重要機制。同時 Prototype 還創造了 Function.prototype.bind
,並在數組上增長了一大堆方法,其中不少方法也已經被標準化了。
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 入的前端這個坑。
當時還有很多段子,「超市收銀員邊工做邊看前端書籍,一個月後就直接去互聯網公司作前端了」,諸如此類。
在咱們使用 jQuery 時須要解決大段 HTML 的生成問題,雖然有 $.html
、$.append
、$before
等方法,可是爲了更好地管理不一樣的 HTML,咱們想將 HTML 分離出來,讓 HTML 獨立到不一樣的文件中,而後直接插數據。
1994 年 PHP 誕生,實現了將動態內容嵌入 HTML,提高了編寫效率和可讀性,其界定符、循環語句等的發明,直接或間接地影響了 JavaScript 前端模板引擎的出現。
模板引擎能夠簡單用一個公式裏描述:HTML = template(vars)
模板引擎的實現須要解決 模板存放、模板獲取、模板解析編譯 的問題
textarea/input
等表單控件,或 script
等標籤中{{...}}
或是 <%...%>
new Function()
的方式轉化成所須要的函數。這裏以 jquery.tmpl 爲例,先來個小栗子
...
<body>
<div id="div_demo"></div>
</body>
<!-- 模板1,測試${}、{{=}}標籤的使用 -->
<script id="demo" type="text/x-jquery-tmpl"></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 元素的全部子節點。
固然也存在其弊端,有部分的替換會引發 迴流。而且若是隻是修改個別數據,使用模板時須要從新渲染整片區域,這是沒有必要的,也是耗性能的。
時間來到 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 的標記語言。綁定器使開發人員免於被迫編寫樣板式邏輯來同步視圖模型和視圖。在微軟的堆以外實現時,聲明性數據綁定技術的出現是實現該模式的一個關鍵因素。
2013 Facebook 將 React 開源,支持 JSX 語法,一開始這種寫法讓人難以接受,在 2017 年 Facebook 推出 React Native,人們纔開始接受 JSX 這種寫法,也開始研究其背後的 虛擬 DOM 技術。
(因爲 JSX 須要額外編譯,又間接促成了 Babel 與 webpack 的壯大)
谷歌在發佈 Angular 時,同時發佈了一個名爲 Polymer 的框架,使用 Web Components 的瀏覽器自定義組件技術;雖然這個框架最後沒火起來,可是它將 Script、Style、Template 三種內容混在一個文件的設計,成功啓發了一個留美華人,搞出了 Vue.js,這人就是 尤雨溪。
最後提一下國內的特點終端——小程序
- 底層運行的迷你 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
屬性鏈接上其他的子節點,以下圖
VD 與 Dom 對象具備一一對應的關係,藉助 VD 將頁面的狀態抽象爲 JS 對象,再配合不一樣的渲染工具,便可達到 跨平臺渲染 的效果。
在進行頁面更新的時候真實 DOM 的改變能夠藉助 VD 在內存中進行比較,也就是常說的 diff。
使用 VD 的框架,通常的設計思路都是 頁面等於頁面狀態的映射,即 UI = render(state)
。當須要更新頁面的時候,無需關心 DOM 具體的變換方式,只須要改變 state 便可,剩下的事情(render)將由框架代勞。
當 state 發生變化時,咱們從新生成整個 VD ,觸發比較的操做。
上述過程大體可分爲如下四步:
這裏咱們講一下傳統 diff 算法,就是將新舊兩棵 VD 樹的節點依次進行對比,最後再進行真實節點的更新。
如上圖所示,左側新 VD 上的節點須要一一與右側舊 VD 的節點對比。爲了後續方便計算時間複雜度,咱們假設理想情況下新 VD 樹的節點個數與舊 VD 樹的節點個數都爲 n。
不少文章都會直接告訴你,傳統 diff 算法時間複雜度爲 O(n^3),至於爲何,那是衆說紛紜,這個說法的出處已經無從考證(有了解的小夥伴歡迎留言或私信)
有兩種廣泛的說法:
第一種是常規思路
第二種就複雜了,涉及到兩棵樹的編輯距離問題,講從 1979 到 2011,將樹的編輯距離算法的時間複雜度降到了 O(n^3),詳情戳這裏
最後說一下個人見解,我認爲 O(n^3)這個值應該是取的早期主流 diff 算法的時間複雜度的均值,畢竟咱們也不知道所謂的傳統 diff 算法到底長什麼樣,哪些算法能被稱爲傳統 diff 算法。
不打算展開,實在是篇幅不容許,後面再單獨出一篇 React diff 算法的。
React 的 diff 算法有個核心思路,即:結合框架的事務機制將屢次比較的結果合併後一次性更新到頁面,從而有效地減小頁面渲染的次數,提升渲染效率
這個也不打算展開,理由同上,Vue 還有個 3.0 版,更有的聊了。
Vue 的 diff 算法採用多指針(這裏指索引下標非內存地址),有的文章說雙指針,其實不止,嚴格來說有四個指針:
首尾兩個指針向中心移動,藉助原生 JS 的內置方法,「實時」 地更新真實節點
同時與 React 同樣,採用 key
來提高算法效率,藉助 map
以空間換時間來下降 diff 算法的時間複雜度
這些介紹都比較籠統,順手點個關注,來蹲一下 React/Vue diff 算法的解析?
文章同時發在我的公衆號 淺析前端框架如何更新視圖,歡迎關注 MelonField