本文做者:yanxin1563javascript
本文做者:html
errorrikjava
前言node
一個 MVVM 框架的性能進化之路 https://github.com/baidu/san/react
性能一直是 框架選型 最重要的考慮因素之一。San 從設計之初就但願不要由於自身的短板(性能、體積、兼容性等)而成爲開發者爲難的理由,因此咱們在性能上投入了不少的關注和精力,效果至少從 benchmark 看來,還不錯。 git
近 2 年之前,我發了一篇 San - 一個傳統的MVVM組件框架。對 San 設計初衷感興趣的同窗能夠翻翻。我一直以爲框架選型的時候,瞭解它的調性是很是關鍵的一點。github
不過其實,大多數應用場景的框架選型中,知名度 是最主要的考慮因素,由於 知名度 意味着你能夠找到更多的人探討、能夠找到更多周邊、能夠更容易招聘熟手或者之後本身找工做更有優點。因此本文的目的並非將你從三大陣營(React、Vue、Angular)拉出來,而是想把 San 的性能經驗分享給你。這些經驗不管在應用開發,仍是寫一些基礎的東西,都會有所幫助。數組
在正式開始以前,慣性先厚臉皮求下 Star。https://github.com/baidu/san/緩存
視圖建立性能優化
考慮下面這個還算簡單的組件:
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
對象,其包含這樣的一些屬性:
預熱 的主要目的很是簡單,就是把在模板信息中就能肯定的事情提早,只作一遍,避免在 渲染/更新 過程當中重複去作,從而節省時間。預熱 過程更多的細節見 preheat-a-node.js。在接下來的部分,對 hotspot
發揮做用的地方也會進行詳細說明。
視圖建立是個很常規的過程:基於初始的 數據 和 ANode,建立一棵對象樹,樹中的每一個節點負責自身在 DOM 樹上節點的操做(建立、更新、刪除)行爲。對一個組件框架來講,建立對象樹的操做沒法省略,因此這個過程必定比原始地 createElement + appendChild 慢。
由於這個過程比較常規,因此接下來不會描述整個過程,而是提一些有價值的優化點。
在 預熱 階段,咱們根據 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
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
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 上每一個節點的信息,建立組件的每一個節點。
ANode 上與節點建立相關的信息有:
節點類型有:
由於每一個節點都經過 createNode
方法建立,因此它的性能是極其重要的。那這個過程的實現,有哪些性能相關的考慮呢?
首先,預熱 過程提早選擇好 ANode 節點對應的實際類型。See preheat-a-node.js#L58 preheat-a-node.js#L170 preheat-a-node.jsL185 preheat-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 包起來,就是爲了在節點建立過程避免建立無用的中間對象,浪費建立和回收的時間。
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 對象。
考慮上文中展現過的組件:
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', []); } }); let myApp = new MyApp(); myApp.attach(document.body);
當咱們更改了數據,視圖就會自動刷新。
myApp.data.set('title', 'SampleList');
咱們能夠很容易的發現,data
是:
fire
,能夠經過 listen
方法監聽數據變動。See data.jsdata
是變化可監聽的,因此組件的視圖變動就有了基礎出發點。
San 最初設計的時候想法很簡單:模板聲明包含了對數據的引用,當數據變動時能夠精準地只更新須要更新的節點,性能應該是很高的。從上面組件例子的模板中,一眼就能看出,title 數據的修改,只須要更新一個節點。可是,咱們如何去找到它並執行視圖更新動做呢?這就是組件的視圖更新機制了。其中,有幾個關鍵的要素:
data
實例並監聽其數據變化。See component.js#L255nextTick
時批量更新。See component.js#L782children
屬性串聯的節點樹,視圖更新是個自上而下遍歷的過程。在節點樹更新的遍歷過程當中,每一個節點經過 _update({Array}changes)
方法接收數據變化信息,更新自身的視圖,並向子節點傳遞數據變化信息。component.js#L688 是組件向下遍歷的起始,但從最典型的 Element的_update方法 能夠看得更清晰些:
children
往下傳遞。// 節選 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')
帶來的視圖刷新,遍歷過程與數據變化信息的傳遞通過了哪些節點。左側最大的點是實際須要更新的節點,紅色的線表明遍歷過程通過的路徑,紅色的小圓點表明遍歷到的節點。能夠看出,雖然須要進行視圖更新的節點只有一個,但全部的節點都被遍歷到了。
從上圖中不難發現,與實際的更新行爲相比,遍歷肯定更新節點的消耗要大得多。因此爲遍歷過程減負,是一個必要的事情。San 在這方面是怎麼作的呢?
首先,預熱 過程生成的 hotspot
對象中,有一項 data
,包含了節點及其子節點對數據引用的摘要信息。See preheat-a-node.js
而後,在視圖更新的節點樹遍歷過程當中,使用 hotspot.data
與數據變化信息進行比對。結果爲 false 時意味着數據的變化不會影響當前節點及其子節點的視圖,就不會執行自身屬性的更新,也不會繼續向下遍歷。遍歷過程在更高層的節點被中斷,節省了下層子樹的遍歷開銷。See element.js#241 changes-is-in-data-ref.js
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
。
因此在節點的屬性更新時,咱們只須要遍歷 hotspot.dynamicProps
,而且直接使用 prop.handler
來執行屬性更新。See element.js#L259-L277
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; } } } // ...... };
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
之上的封裝。
基於數據變化信息的視圖更新機制,意味着數據操做的粒度越細越精準,視圖更新的負擔越小性能越高。
// 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
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
。
因此,對於 Element、TextNode、ForNode、IfNode 等節點的 dispose
方法,都包含一個隱藏參數:noDetach
。當接收到的值爲 true
時,節點只作必要的清除操做(移除 DOM 上掛載的事件、清理節點樹的引用關係),不執行其對應 DOM 元素的刪除操做。See text-node.js#L118 node-own-simple-dispose.js#L22 element.js#L211 etc...
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
// 代碼節選 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
buf += seg.value || evalExpr(seg, data, owner);
另外,還有很重要的一點:San 裏雖然實現了 each
方法,可是在視圖建立、視圖更新、變動判斷、表達式取值等關鍵性的過程當中,仍是直接使用 for 進行遍歷,就是爲了減小沒必要要的函數調用開銷。See each.js eval-expr.js etc...
// 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
// 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 唄。
---------------------------------
在微信-搜索頁面中輸入「百度App技術」,便可關注微信官方帳號;