作者:Erik
原文地址:https://efe.baidu.com/blog/san-perf/
感謝 Erik 嘔心瀝血產出此文,大家可以關注他的 微博 和他交流
友情提示:此文很長,但含量極高,推薦大家收藏點贊後時長翻出來閱讀
希望大家給 SAN 點個 Star
baidu/san?github.com
一個 MVVM 框架的性能進化之路
性能一直是 框架選型 最重要的考慮因素之一。San 從設計之初就希望不要因為自身的短板(性能、體積、兼容性等)而成為開發者為難的理由,所以我們在性能上投入了很多的關注和精力,效果至少從 benchmark 看來,還不錯。
將近 2 年以前,我發了一篇 San - 一個傳統的MVVM組件框架。對 San 設計初衷感興趣的同學可以翻翻。我一直覺得框架選型的時候,瞭解它的調性是非常關鍵的一點。
不過其實,大多數應用場景的框架選型中,知名度 是最主要的考慮因素,因為 知名度 意味著你可以找到更多的人探討、可以找到更多周邊、可以更容易招聘熟手或者以後自己找工作更有優勢。所以本文的目的並不是將你從三大陣營(React、Vue、Angular)拉出來,而是想把 San 的性能經驗分享給你。這些經驗無論在應用開發,還是寫一些基礎的東西,都會有所幫助。
在正式開始之前,慣性先厚臉皮求下 Star。
考慮下面這個還算簡單的組件:
const MyApp = san.defineComponent({ template: ` <div> <h3>{{title}}</h3> <ul> <li s-for="item,i in list">{{item}} <a on-click="removeItem(i)">x</a></li> </ul> <h4>Operation</h4> <div> Name: <input type="text" value="{=value=}"> <button on-click="addItem">add</button> </div> <div> <button on-click="reset">reset</button> </div> </div> `,
initData() { return { title: List, list: [] }; },
addItem() { this.data.push(list, this.data.get(value)); this.data.set(value, ); },
removeItem(index) { this.data.removeAt(list, index); },
reset() { this.data.set(list, []); } });
在視圖初次渲染完成後,San 會生成一棵這樣子的樹:
那麼,在這個過程裏,San 都做了哪些事情呢?
在組件第一個實例被創建時,template 屬性會被解析成 ANode。
ANode 的含義是抽象節點樹,包含了模板聲明的所有信息,包括標籤、文本、插值、數據綁定、條件、循環、事件等信息。對每個數據引用的聲明,也會解析出具體的表達式對象。
{ "directives": {}, "props": [], "events": [], "children": [ { "directives": { "for": { "item": "item", "value": { "type": 4, "paths": [ { "type": 1, "value": "list" } ] }, "index": "i", "raw": "item,i in list" } }, "props": [], "events": [], "children": [ { "textExpr": { "type": 7, "segs": [ { "type": 5, "expr": { "type": 4, "paths": [ { "type": 1, "value": "item" } ] }, "filters": [], "raw": "item" } ] } }, { "directives": {}, "props": [], "events": [ { "name": "click", "modifier": {}, "expr": { "type": 6, "name": { "type": 4, "paths": [ { "type": 1, "value": "removeItem" } ] }, "args": [ { "type": 4, "paths": [ { "type": 1, "value": "i" } ] } ], "raw": "removeItem(i)" } } ], "children": [ { "textExpr": { "type": 7, "segs": [ { "type": 1, "literal": "x", "value": "x" } ], "value": "x" } } ], "tagName": "a" } ], "tagName": "li" } ], "tagName": "ul" }
ANode 保存著視圖聲明的數據引用與事件綁定信息,在視圖的初次渲染與後續的視圖更新中,都扮演著不可或缺的作用。
無論一個組件被創建了多少個實例,template 的解析都只會進行一次。當然,預編譯是可以做的。但因為 template 是用才解析,沒有被使用的組件不會解析,所以就看實際使用中值不值,有沒有必要了。
在組件第一個實例被創建時,ANode 會進行一個 預熱 操作。看起來, 預熱 和 template解析 都是發生在第一個實例創建時,那他們有什麼區別呢?
接下來,讓我們看看預熱到底生成了什麼?
aNode.hotspot = { data: {}, dynamicProps: [], xProps: [], props: {}, sourceNode: sourceNode };
上面這個來自 preheat-a-node.js 的簡單代碼節選不包含細節,但是可以看出, 預熱 過程生成了一個 hotspot 對象,其包含這樣的一些屬性:
hotspot
預熱 的主要目的非常簡單,就是把在模板信息中就能確定的事情提前,只做一遍,避免在 渲染/更新 過程中重複去做,從而節省時間。預熱 過程更多的細節見 preheat-a-node.js。在接下來的部分,對 hotspot 發揮作用的地方也會進行詳細說明。
視圖創建是個很常規的過程:基於初始的 數據 和 ANode,創建一棵對象樹,樹中的每個節點負責自身在 DOM 樹上節點的操作(創建、更新、刪除)行為。對一個組件框架來說,創建對象樹的操作無法省略,所以這個過程一定比原始地 createElement + appendChild 慢。
因為這個過程比較常規,所以接下來不會描述整個過程,而是提一些有價值的優化點。
在 預熱 階段,我們根據 tagName 創建了 sourceNode。
tagName
sourceNode
if (isBrowser && aNode.tagName && !/^(template|slot|select|input|option|button)$/i.test(aNode.tagName) ) { sourceNode = createEl(aNode.tagName); }
ANode 中包含了所有的屬性聲明,我們知道哪些屬性是動態的,哪些屬性是靜態的。對於靜態屬性,我們可以在 預熱 階段就直接設置好。See preheat-a-node.js
each(aNode.props, function (prop, index) { aNode.hotspot.props[prop.name] = index; prop.handler = getPropHandler(aNode.tagName, prop.name);
// ...... if (prop.expr.value != null) { if (sourceNode) { prop.handler(sourceNode, prop.expr.value, prop.name, aNode); } } else { if (prop.x) { aNode.hotspot.xProps.push(prop); } aNode.hotspot.dynamicProps.push(prop); } });
在 視圖創建過程 中,就可以從 sourceNode clone,並且只對動態屬性進行設置。See element.js#L115-L150
var sourceNode = this.aNode.hotspot.sourceNode; var props = this.aNode.props;
if (sourceNode) { this.el = sourceNode.cloneNode(false); props = this.aNode.hotspot.dynamicProps; } else { this.el = createEl(this.tagName); }
// ... for (var i = 0, l = props.length; i < l; i++) { var prop = props[i]; var propName = prop.name; var value = isComponent ? evalExpr(prop.expr, this.data, this) : evalExpr(prop.expr, this.scope, this.owner);
// ... prop.handler(this.el, value, propName, this, prop);
// ... }
不同屬性對應 DOM 的操作方式是不同的,屬性的 預熱 提前保存了屬性操作函數(preheat-a-node.js#L133),屬性初始化或更新時就無需每次都重複獲取。
prop.handler = getPropHandler(aNode.tagName, prop.name);
對於 s-bind,對應的數據是 預熱 階段無法預知的,所以屬性操作函數只能在具體操作時決定。See element.js#L128-L137
s-bind
for (var key in this._sbindData) { if (this._sbindData.hasOwnProperty(key)) { getPropHandler(this.tagName, key)( // 看這裡看這裡 this.el, this._sbindData[key], key, this ); } }
所以,getPropHandler 函數的實現也進行了相應的結果緩存。See get-prop-handler.js
getPropHandler
var tagPropHandlers = elementPropHandlers[tagName]; if (!tagPropHandlers) { tagPropHandlers = elementPropHandlers[tagName] = {}; }
var propHandler = tagPropHandlers[attrName]; if (!propHandler) { propHandler = defaultElementPropHandlers[attrName] || defaultElementPropHandler; tagPropHandlers[attrName] = propHandler; }
return propHandler;
視圖創建過程中,San 通過 createNode 工廠方法,根據 ANode 上每個節點的信息,創建組件的每個節點。
createNode
ANode 上與節點創建相關的信息有:
節點類型有:
因為每個節點都通過 createNode 方法創建,所以它的性能是極其重要的。那這個過程的實現,有哪些性能相關的考慮呢?
首先,預熱 過程提前選擇好 ANode 節點對應的實際類型。See preheat-a-node.js#L58 preheat-a-node.js#L170 preheat-a-node.jsL185preheat-a-node.jsL190
在 createNode 一開始就可以直接知道對應的節點類型。See create-node.js#L24-L26
if (aNode.Clazz) { return new aNode.Clazz(aNode, parent, scope, owner); }
另外,我們可以看到,除了 Component 之外,其他節點類型的構造函數參數簽名都是 (aNode, parent, scope, owner, reverseWalker),並沒有使用一個 Object 包起來,就是為了在節點創建過程避免創建無用的中間對象,浪費創建和回收的時間。
(aNode, parent, scope, owner, reverseWalker)
function IfNode(aNode, parent, scope, owner, reverseWalker) {} function ForNode(aNode, parent, scope, owner, reverseWalker) {} function TextNode(aNode, parent, scope, owner, reverseWalker) {} function Element(aNode, parent, scope, owner, reverseWalker) {} function SlotNode(aNode, parent, scope, owner, reverseWalker) {} function TemplateNode(aNode, parent, scope, owner, reverseWalker) {}
function Component(options) {}
而 Component 由於使用者可直接接觸到,初始化參數的便利性就更重要些,所以初始化參數是一個 options 對象。
考慮上文中展示過的組件:
let myApp = new MyApp(); myApp.attach(document.body);
當我們更改了數據,視圖就會自動刷新。
myApp.data.set(title, SampleList);
我們可以很容易的發現,data 是:
data
fire
listen
data 是變化可監聽的,所以組件的視圖變更就有了基礎出發點。
San 最初設計的時候想法很簡單:模板聲明包含了對數據的引用,當數據變更時可以精準地只更新需要更新的節點,性能應該是很高的。從上面組件例子的模板中,一眼就能看出,title 數據的修改,只需要更新一個節點。但是,我們如何去找到它並執行視圖更新動作呢?這就是組件的視圖更新機制了。其中,有幾個關鍵的要素:
nextTick
children
在節點樹更新的遍歷過程中,每個節點通過 _update({Array}changes) 方法接收數據變化信息,更新自身的視圖,並向子節點傳遞數據變化信息。component.js#L688 是組件向下遍歷的起始,但從最典型的 Element的_update方法 可以看得更清晰些:
_update({Array}changes)
// 節選 Element.prototype._update = function (changes) { // ......
// 先看自身的屬性有沒有需要更新的 var dynamicProps = this.aNode.hotspot.dynamicProps; for (var i = 0, l = dynamicProps.length; i < l; i++) { var prop = dynamicProps[i]; var propName = prop.name;
for (var j = 0, changeLen = changes.length; j < changeLen; j++) { var change = changes[j];
if (!isDataChangeByElement(change, this, propName) && changeExprCompare(change.expr, prop.hintExpr, this.scope) ) { prop.handler(this.el, evalExpr(prop.expr, this.scope, this.owner), propName, this, prop); break; } } }
// ......
// 然後把數據變化信息通過 children 往下傳遞 for (var i = 0, l = this.children.length; i < l; i++) { this.children[i]._update(changes); } };
下面這張圖說明瞭在節點樹中,this.data.set(title, hello) 帶來的視圖刷新,遍歷過程與數據變化信息的傳遞經過了哪些節點。左側最大的點是實際需要更新的節點,紅色的線代表遍歷過程經過的路徑,紅色的小圓點代表遍歷到的節點。可以看出,雖然需要進行視圖更新的節點只有一個,但所有的節點都被遍歷到了。
this.data.set(title, hello)
從上圖中不難發現,與實際的更新行為相比,遍歷確定更新節點的消耗要大得多。所以為遍歷過程減負,是一個必要的事情。San 在這方面是怎麼做的呢?
首先,預熱 過程生成的 hotspot 對象中,有一項 data,包含了節點及其子節點對數據引用的摘要信息。See preheat-a-node.js
然後,在視圖更新的節點樹遍歷過程中,使用 hotspot.data 與數據變化信息進行比對。結果為 false 時意味著數據的變化不會影響當前節點及其子節點的視圖,就不會執行自身屬性的更新,也不會繼續向下遍歷。遍歷過程在更高層的節點被中斷,節省了下層子樹的遍歷開銷。See element.js#241 changes-is-in-data-ref.js
hotspot.data
Element.prototype._update = function (changes) { var dataHotspot = this.aNode.hotspot.data; if (dataHotspot && changesIsInDataRef(changes, dataHotspot)) { // ... } };
有了節點遍歷中斷的機制,title 數據修改引起視圖變更的遍歷過程如下。可以看到,灰色的部分都是由於中斷,無需到達的節點。
有沒有似曾相識的感覺?是不是很像 React 中的 shouldComponentUpdate?不過不同的是,由於模板聲明包含了對數據的引用,San可以在框架層面自動做到這一點,組件開發者不需要人工去幹這件事了。
在視圖創建過程的章節中,提到過在 預熱 過程中,我們得到了:
<input type="text" value="{=value=}">
在上面這個例子中,dynamicProps 只包含 value,不包含 type。
dynamicProps
value
type
所以在節點的屬性更新時,我們只需要遍歷 hotspot.dynamicProps,並且直接使用 prop.handler 來執行屬性更新。See element.js#L259-L277
hotspot.dynamicProps
prop.handler
Element.prototype._update = function (changes) { // ......
// ...... };
Immutable 在視圖更新中最大的意義是,可以無腦認為 === 時,數據是沒有變化的。在很多場景下,對視圖是否需要更新的判斷變得簡單很多。否則判斷的成本對應用來說是不可接受的。
但是,Immutable 可能會導致開發過程的更多成本。如果開發者不藉助任何庫,只使用原始的 JavaScript,一個對象的賦值會寫的有些麻煩。
var obj = { a: 1, b: { b1: 2, b2: 3 }, c: 2 };
// mutable obj.b.b1 = 5;
// immutable obj = Object.assign({}, obj, {b: Object.assign({}, obj.b, {b1: 5})});
San 的數據操作是通過 data 上的方法提供的,所以內部實現可以天然 immutable,這利於視圖更新操作中的一些判斷。See data.js#L209
由於視圖刷新是根據數據變化信息進行的,所以判斷當數據沒有變化時,不產生數據變化信息就行了。See data.js#L204 for-node.jsL570 L595 L679 L731
San 期望開發者對數據操作細粒度的使用數據操作方法。否則,不熟悉 immutable 的開發者可能會碰到如下情況。
// 假設初始數據如下 /* { a: 1, b: { b1: 2, b2: 3 } } */
var b = this.data.get(b); b.b1 = 5;
// 由於 b 對象引用不變,會導致視圖不刷新 this.data.set(b, b);
// 正確做法。set 操作在 san 內部是 immutable 的 this.data.set(b.b1, 5);
上文中我們提到,San 的視圖更新機制是基於數據變化信息的。數據操作方法 提供了一系列方法,會 fire changeObj。changeObj 只有兩種類型: SET 和 SPLICE。See data-change-type.js data.js#L211 data.js#L352
// SET changeObj = { type: DataChangeType.SET, expr, value, option };
// SPLICE changeObj = { type: DataChangeType.SPLICE, expr, index, deleteCount, value, insertions, option };
San 提供的數據操作方法裏,很多是針對數組的,並且大部分與 JavaScript 原生的數組方法是一致的。從 changeObj 的類型可以容易看出,最基礎的方法只有 splice 一個,其他方法都是 splice 之上的封裝。
splice
基於數據變化信息的視圖更新機制,意味著數據操作的粒度越細越精準,視圖更新的負擔越小性能越高。
// bad performance this.data.set(list[0], { name: san, id: this.data.get(list[0].id) });
// good performance this.data.set(list[0].name, san);
我們看個簡單的例子:下圖中,我們要把第一行的列表更新成第二行,需要插入綠色部分,更新黃色部分,刪除紅色部分。
San 的 ForNode 負責列表的渲染和更新。在更新過程裏:
假設數據變化信息為:
[ // insert [2, 3], pos 1 // update 4 // remove 7 // remove 10 ]
在遍曆數據變化信息前,我們先初始化一個和當前 children 等長的數組:childrenChanges。其用於存儲 children 裏每個子節點的數據變化信息。See for-node.js#L352
同時,我們初始化一個 disposeChildren 數組,用於存儲需要被刪除的節點。See for-node.js#L362
接下來,_updateArray 循環處理數據變化信息。當遇到插入時,同時擴充 children 和 childrenChanges 數組。
當遇到更新時,如果更新對應的是某一項,則對應該項的 childrenChanges 添加更新信息。
當遇到刪除時,我們把要刪除的子節點從 children 移除,放入 disposeChildren。同時,childrenChanges 裏相應位置的項也被移除。
遍曆數據變化信息結束後,執行更新行為分成兩步:See for-node.js#L772-L823
this._disposeChildren(disposeChildren, function () { doCreateAndUpdate(); });
下面,我們看看常見的列表更新場景下, San 都有哪些性能優化的手段。
在遍曆數據變化信息時,遇到添加項,往 children 和 childrenChanges 中填充的只是 undefined 或 0 的佔位值,不初始化新節點。See for-node.js#L518-L520
undefined
0
var spliceArgs = [changeStart + deleteCount, 0].concat(new Array(newCount)); this.children.splice.apply(this.children, spliceArgs); childrenChanges.splice.apply(childrenChanges, spliceArgs);
由於 San 的視圖是非同步更新的,當前更新週期可能包含多個數據操作。如果這些數據操作中創建了一個項又刪除了的話,在遍曆數據變化信息過程中初始化新節點就是沒有必要的浪費。所以創建節點的操作放到後面 執行更新 的階段。
前文中提過,視圖創建的過程,對於 DOM 的創建是挨個 createElement 並 appendChild 到 parentNode 中的。但是在刪除的時候,我們並不需要把整棵子樹上的節點都挨個刪除,只需要把要刪除子樹的根元素從 parentNode 中 removeChild。
createElement
appendChild
parentNode
removeChild
所以,對於 Element、TextNode、ForNode、IfNode 等節點的 dispose 方法,都包含一個隱藏參數:noDetach。當接收到的值為 true 時,節點只做必要的清除操作(移除 DOM 上掛載的事件、清理節點樹的引用關係),不執行其對應 DOM 元素的刪除操作。See text-node.js#L118 node-own-simple-dispose.js#L22 element.js#L211 etc...
dispose
noDetach
true
if (!noDetach) { removeEl(this.el); }
另外,在很多情況下,一次視圖更新週期中如果有數組項的刪除,是不會有對其他項的更新操作的。所以我們增加了 isOnlyDispose 變數用於記錄是否只包含數組項刪除操作。在 執行更新 階段,如果該項為 true,則完成刪除動作後不再遍歷 children 進行子項更新。See for-node.js#L787
if (isOnlyDispose) { return; }
// 對相應的項進行更新 // 如果不attached則直接創建,如果存在則調用更新函數 for (var i = 0; i < newLen; i++) { }
數據變化(添加項、刪除項等)可能會導致數組長度變化,數組長度也可能會被數據引用。
<li s-for="item, index in list">{{index + 1}}/{{list.length}} item</li>
在這種場景下,即使只添加或刪除一項,整個列表視圖都需要被刷新。由於子節點的更新是在 執行更新 階段通過 _update 方法傳遞數據變化信息的,所以在 執行更新 前,我們根據以下兩個條件,判斷是否需要為子節點增加 length 變更信息。See for-node.js#L752-L767
首先,當數組長度為 0 時,顯然整個列表項直接清空就行了,數據變化信息可以完全忽略,不需要進行多餘的遍歷。See for-node.js#L248-L251
其次,如果一個元素裏的所有元素都是由列表項組成的,那麼元素的刪除可以暴力清除:通過一次 parentNode.textContent = 完成,無需逐項從父元素中移除。See for-node.js#L316-L332
parentNode.textContent =
// 代碼節選 var violentClear = !this.aNode.directives.transition && !children // 是否 parent 的唯一 child && len && parentFirstChild === this.children[0].el && parentLastChild === this.el ;
if (violentClear) { parentEl.textContent = ; }
想像下面這個列表數據子項的變更:
myApp.data.set(list[2], two);
對於 ForNode 的更新:
從上圖的更新過程可以看出,子項更新的更新過程能精確處理最少的節點。數據變更時精準地更新節點是 San 的優勢。
對於整列表變更,San 的處理原則是:儘可能重用當前存在的節點。原列表與新列表數據相比:
我們採用瞭如下的處理過程,保證原列表與新列表重疊部分節點執行更新操作,無需刪除再創建:
San 鼓勵開發者細粒度的使用數據操作方法,但總有無法精準進行數據操作,只能直接 set 整個數組。舉一個最常見的例子:數據是從服務端返回的 JSON。在這種場景下,就是 trackBy 發揮作用的時候了。
我就是我,是顏色不一樣的煙火。 -- 張國榮《我》
<ul> <li s-for="p in persons trackBy p.name">{{p.name}} - {{p.email}}</li> </ul>
trackBy 也叫 keyed,其作用就是當列表數據 無法進行引用比較 時,告訴框架一個依據,框架就可以判斷出新列表中的項是原列表中的哪一項。上文提到的:服務端返回的數據,是 無法進行引用比較 的典型例子。
這裡我們不說 trackBy 的整個更新細節,只提一個優化手段。這個優化手段不是 San 獨有的,而是經典的優化手段。
可以看到,我們從新老列表的頭部和尾部進行分別遍歷,找出新老列表頭部和尾部的相同項,並把他們排除。這樣剩下需要進行 trackBy 的項可能就少多了。對應到常見的視圖變更場景,該優化手段都能發揮較好的作用。
從 benchmark 的結果能看出來,San 在 trackBy 下也有較好的性能。
在這個部分,我會列舉一些大多數人覺得知道、但又不會這麼去做的優化寫法。這些優化寫法貌似對性能沒什麼幫助,但是積少成多,帶來的性能增益還是不可忽略的。
call 和 apply 是 JavaScript 中的魔法,也是性能的大包袱。在 San 中,我們儘可能減少 call 和 apply 的使用。下面列兩個點:
比如,對 filter 的處理中,內置的 filter 由於都是 pure function,我們明確知道運行結果不依賴 this,並且參數個數都是確定的,所以無需使用 call。See eval-expr.js#L164-L172
if (owner.filters[filterName]) { value = owner.filters[filterName].apply( owner, [value].concat(evalArgs(filter.args, data, owner)) ); } else if (DEFAULT_FILTERS[filterName]) { value = DEFAULT_FILTERS[filterName](value); }
再比如,Component 和 Element 之間應該是繼承關係,create、attach、dispose、toPhase 等方法有很多可以復用的邏輯。基於性能的考慮,實現中並沒有讓 Component 和 Element 發生關係。對於復用的部分:
看到這裡的你不知是否記得,在 創建節點 章節中,提到節點的函數簽名不合併成一個數組,就是為了防止中間對象的創建。中間對象不止是創建時有開銷,觸發 GC 回收內存也是有開銷的。在 San 的實現中,我們儘可能避免中間對象的創建。下面列兩個點:
數據操作的過程,直接傳遞表達式層級數組,以及當前指針位置。不使用 slice 創建表達式子層級數組。See data.js#L138
function immutableSet(source, exprPaths, pathsStart, pathsLen, value, data) { if (pathsStart >= pathsLen) { return value; }
// ...... }
data 創建時如果傳入初始數據對象,以此為準,避免 extend 使初始數據對象變成中間對象。See data.js#L23
function Data(data, parent) { this.parent = parent; this.raw = data || {}; this.listeners = []; }
函數調用本身的開銷是很小的,但是調用本身也會初始化環境對象,調用結束後環境對象也需要被回收。San 對函數調用較為頻繁的地方,做了避免調用的條件判斷。下面列兩個點:
element 在創建子元素時,判斷子元素構造器是否存在,如果存在則無需調用 createNode 函數。See element.js#L167-L169
var child = childANode.Clazz ? new childANode.Clazz(childANode, this, this.scope, this.owner) : createNode(childANode, this, this.scope, this.owner);
ANode 中對定值表達式(數字、bool、字元串字面量)的值保存在對象的 value 屬性中。evalExpr 方法開始時根據 expr.value != null 返回。不過在調用頻繁的場景(比如文本的拼接、表達式變化比對、等等),會提前進行一次判斷,減少 evalExpr 的調用。See eval-expr.js#L203 change-expr-compare.js#L77
evalExpr
expr.value != null
buf += seg.value || evalExpr(seg, data, owner);
另外,還有很重要的一點:San 裏雖然實現了 each 方法,但是在視圖創建、視圖更新、變更判斷、表達式取值等關鍵性的過程中,還是直接使用 for 進行遍歷,就是為了減少不必要的函數調用開銷。See each.js eval-expr.js etc...
each
// bad performance each(expr.segs.length, function (seg) { buf += seg.value || evalExpr(seg, data, owner); });
// good performance for (var i = 0, l = expr.segs.length; i < l; i++) { var seg = expr.segs[i]; buf += seg.value || evalExpr(seg, data, owner); }
使用 for...in 進行對象的遍歷是非常耗時的操作,San 在視圖創建、視圖更新等過程中,當運行過程明確時,儘可能不使用 for...in 進行對象的遍歷。一個比較容易被忽略的場景是對象的 extend,其隱藏了 for...in 遍歷過程。
function extend(target, source) { for (var key in source) { if (source.hasOwnProperty(key)) { var value = source[key]; if (typeof value !== undefined) { target[key] = value; } } }
return target; }
從一個對象創建一個大部分成員都一樣的新對象時,避免使用 extend。See for-node.jsL404
extend
// bad performance change = extend( extend({}, change), { expr: createAccessor(this.itemPaths.concat(changePaths.slice(forLen + 1))) } );
// good performance change = change.type === DataChangeType.SET ? { type: change.type, expr: createAccessor( this.itemPaths.concat(changePaths.slice(forLen + 1)) ), value: change.value, option: change.option } : { index: change.index, deleteCount: change.deleteCount, insertions: change.insertions, type: change.type, expr: createAccessor( this.itemPaths.concat(changePaths.slice(forLen + 1)) ), value: change.value, option: change.option };
將一個對象的成員賦予另一個對象時,避免使用 extend。See component.jsL113
// bad performance extend(this, options);
// good performance this.owner = options.owner; this.scope = options.scope; this.el = options.el;
性能對於一個框架來說,是非常重要的事情。應用開發的過程通常很少會關注框架的實現;而如果框架實現有瓶頸,應用開發工程師其實是很難解決的。開發一時爽,調優火葬場的故事,發生得太多了。
San 在性能方面做了很多工作,但是看下來,其實沒有什麼非常深奧難以理解的技術。我們僅僅是覺得性能很重要,並且儘可能細緻的考慮和實現。因為我們不希望自己成為應用上的瓶頸,也不希望性能成為開發者在選型時猶豫的理由。
如果你看到這裡,覺得 San 還算有誠意,或者覺得有收穫,給個 Star 唄。