Knockout, Vue 和 AvalonJS 等 MVVM 框架實現中是否用到 eval 或 Function?

如果不用 eval() 或者 Function 構造函數,如何解析模板中 inline 的表達式呢? 我一直在考慮這個問題。

例如 (Knockout 語法):

&Hello&


Vue 和 Knockout 都用了 new Function(),avalon 應該也是。和 eval 本質上差不多。不想用 eval 就得自己寫一個 js parser 解析表達式到 AST,然後對 AST 求值,Angular 就是這麼做的。

new Function 的優點是實現可以比較簡單,自己搞 parser 實現比較複雜,最後代碼量也大很多,但是可以在求值過程中進行各種檢查,對表達式的語法進行限制。比如 Angular 的表達式允許的語法其實是 JS 語法的一個子集,同時還有各種針對 XSS 的檢查。相比之下基於 new Function 的方案都是基於正則的字元串重寫,把表達式改寫成需要的樣子然後做成一個 function。過程中也可以進行一定的過濾和限制,但不如基於 AST 的限制來得強大。最後,new Function 的性能應該是比 AST 求值要好一些的。

基於 AST 的方案也可以直接用現成的 js parser 實現,比如 esprima。Polymer 的 parser 就是一個修改過的輕量版的 esprima,(不過貌似嫌太慢,所以 0.8+ 直接選擇了放棄支持表達式...)。Vue 也有一個不利用 new Function 的版本,就是直接用 esprima 實現的,可以用於有 CSP (Content Security Policy) 不允許 eval 的環境下。

參考源碼:

Vue vue/expression.js at dev · yyx990803/vue · GitHub

Knockout knockout/bindingProvider.js at master · knockout/knockout · GitHub

Angular angular.js/parse.js at master · angular/angular.js · GitHub


avalon用到new Function, ko則用eval,其他的不知。

因為我們需要將頁面上的這些綁定屬性或叫指令,轉換為一個視圖刷新函數。 我們的VM就是一個數據源, 到時會千方百計將VM作為參數,傳入這個視圖刷新函數中的。

一般來說, 我們在會domReady時掃描整個DOM樹,將這些屬性收集起來,轉換為一個對象,然後根據屬性名,得到處理函數,比如ng-show, ng-hide, ms-visible,這些名字,我們很易就聯想到,它是使用el.style.display 實現的。這些處理函數一早就定義好了,我們只需要配對就行。

問題是這些變數,如何從一片代碼中挑出來,如果是使用with(ko就是這麼干),是很易實現這個刷新函數。 剩下就有兩個方法:

一是使用一個完整的parser,像ng就實現一個近2000行的parser,一個個字元地分析其語義與詞法,最後整合成一個函數體的字元串,通過new Function。

二是通過正則處理,其實判定一個東西是否變數,還是有一 定規則。首先它不在注釋裡面,不在字元串裡面,前面存在( [ { + - ,; 這些操作符,我們通過正則就可以將它們幹掉,將它們替換為逗號。此外,我們只需要關注變數最前面的部分,如果後面跟著 . 號,這可能是方法或屬性,這個也可以用正則將它替換為逗號。 經過這些裡面,原來一段字元串,就剩下幾個單詞與逗號。這些單詞可能存在if, for, in, while這個關鍵字,我們可以寫一個hash,包含所有的關鍵字與保留字,這樣也把它幹掉。 最後,還有一個特殊的情況, 正則!正則我們只能幹掉簡單的,一些複雜的正則,正則就干不掉它們,需要詞法分析才行。但不用擔心,我們這時使用VM,一個個hasOwnProperty來匹配。整個流程下來,命中率達到99%(當然比不上一個嚴謹的parser),但從我們的實際使用來看,包括去哪兒,百度,淘寶,銀聯,騰訊遊戲等等公司,還沒有出個什麼叉子的

vm = {
a: 1,
b: 2
}

ms-attr-title="if(a){return b}else{return "4"}"

上面的一段代碼是指

if(a){return b}else{return "4"}

第1次去掉雜質

if,a,return, b,else,return

第2次去掉雜質

a b

這時就按空格得到所有變數,然後在這些變數轉換一個個賦值表達式,放到原來的代碼之上

var a = obj.a;
var b = obj.b;
if(a){return b}else{return "4"}

上面這段東西,為一個字元串,賦給一個body變數,然後通過new Function就得到我們夢魅以求的東西了

var fn = new Function("obj", body)

這個函數,我們通常叫做刷新函數。

function anonymous(obj) {
var a = obj.a;var b = obj.b;if(a){return b ;}else{return "4";}
}

相對於一個複雜的parser,這個也不輕鬆,也需要幾百行代碼,有許多細節需要考慮

avalon/14 parser.share.js at master · RubyLouvre/avalon · GitHub

因此想寫一個MVVM框架,比一個MVC框架難多了,這裡就涉及到編譯原理。各種精巧的數據結構與演算法閃爍其中,光是一個計算屬性就用去一半的設計模式了!

最後歡迎使用 avalon 迷你易用的MVVM框架~!


我是Angular的腦殘粉,Angular是用AST實現的,

AST:抽象語法樹,對於一個語句: 1+3*4,語法樹如圖

+
/
/
1 * =&> 按照自底向上的順序計算結果
/
/
3 4

來個複雜一點的語法 things.length() &< 3

&< / / (call) 3 / / (get) [] / / (get) length / / [context] things

從下至上的每一步都可以轉化為js的語句

var temp=[context]["things"];
var method=temp["length"];
var result=method.call(temp,[]);
return result&<3;

對於Angular這裡的[context]就是$scope.

通過對於表達式字元串的掃描,形成AST. AST一旦形成,會被緩存,下次相同字元串的表達式不需要再編譯.

AST可以完全限制函數作用域,起到安全的效果

alert(navigator.language);
會被轉化成
(call)
/
/
(get) (args)
/
/
[context] alert (get)
/
/
(get) language
/
/
[context] navigator

只要[context]不是window,那麼上面的語句不會輸出瀏覽器用戶的語言,

事實上Angular中的context確實不會是window.

而如果是使用eval執行表達式,很難避免惡意代碼被執行.

Angular的表達式語法是JS語法的子集,對賦值語句有嚴格的限制,保證代碼安全.

關於 @尤雨溪 提到的new Function 和 AST 的速度問題,按照理論無論是構建還是執行,都應該是new Function快一些(new Function是C++翻譯,C++作為解釋器執行),但是不知道有沒有具體的數據支持?不過當eval和new Function被限制的時候,似乎只能使用AST.

Angular的parser是手寫的,文件確實很大(69k),其實parser可以代碼生成,而且生成的parser通常比普通人手寫的更快更小,Github上面zaach/jison · GitHub是一個用JS實現的parser生成器,CoffeeScript當年就是用的它.我曾經嘗試著用JISON構建一個Angular語法集的parser,只需要14k.

此處應有廣告.....


作為一個也在搗鼓 MV* 框架的同學, 前些時候在參考 Angular 的編譯器/解釋器的相關實現, 所以順便說說 (雖然在這方面我是外行, 邊模仿邊理解).

Angular (1.x) 的 parse.js 文件有 @尤雨溪 大大已經貼出來了, 但正如上一段提到的 Angular 不僅有不用 eval 或 new Function 的 interpreter, 也有用上 new Function 的 compiler, 分別在不同的情況下使用, 比如 Chrome 插件, Windows Store App 中應該會更傾向於選擇 interpreter 一些. 其他情況可能處於性能考慮, 會優先選擇 compiler? (具體的選擇邏輯我沒有深入了解, 就這麼一猜.)

AST 相關代碼在其中是共享的, 所以 AST 和 new Function 並不衝突. 讀完之後發現實現非常巧妙 (也可能主流的 AST 都是這麼做的, 空了補補課), 按運算符優先順序從低往高走進行遞歸, 邏輯簡單明了, 建議像我這樣的學渣/非對口專業的同學如果有興趣可以看看, 也算是提高一下姿勢水平了~

順便由於自己比較熟悉正則, 目前的 lexer 是用正則完成的, 為此做了一個維護正則的小工具, 有相應需求的同學可以試試看 (如果你也需要維護長長長的 JS 正則表達式)~ vilic/regex-tools · GitHub 附上由它維護的 lexer drop/compiler.ts at v0.2 · vilic/drop · GitHub


呀呀,我來做個廣告呀,我就不用eval也不用Function呀。因為我們不在模板裡面寫代碼呀,你看,我們多機智。

astamuse/asta4js · GitHub


推薦閱讀:

Vue.js如何優雅的進行form validation?
Vue 中如何使用 MutationObserver 做批量處理?
有什麼UI組件庫可以兼容三大框架,vue,react,angular嗎?
在react 、 vue 、ng 這些框架火起來之前,是哪些框架比較火?它們現在怎麼樣了?

TAG:前端開發 | eval | knockoutjs | Vuejs |