前言
- 瀏覽器編譯版本,瀏覽器版本是使用with語法加上proxy代理攔截
- 本地預編譯版本,通過在模版預編譯階段轉換階段,使用轉換插件transformExpression將非白名單標識符掛在在組件代理對象下
瀏覽器編譯版本
render 函數編譯結果
1
2
|
< div >{{test}}</ div > < div >{{Math.floor(1)}}</ div > |
to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
const _Vue = Vue; return function render(_ctx, _cache, $props, $setup, $data, $options) { with (_ctx) { const { toDisplayString: _toDisplayString, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, } = _Vue; return ( _openBlock(), _createBlock( _Fragment, null , [ _createVNode( "div" , null , _toDisplayString(test), 1 /* TEXT */ ), _createVNode( "div" , null , _toDisplayString(Math.floor(1)), 1 /* TEXT */ ), ], 64 /* STABLE_FRAGMENT */ ) ); } }; |
從上面的代碼,我們能發現,變量標識符沒有增加前綴,只是用with語法包裹了一下,延長作用域鏈,那么是如何做到 js 沙箱攔截的呢?例如變量test,理論上說,當前作用域鏈沒有test變量,變量會從上一層作用域查找,直到查找到全局作用域,但是,實際上只會在_ctx上查找,原理很簡單,_ctx是一個代理對象,那么我們如何使用Proxy做攔截,示例代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
const GLOBALS_WHITE_LISTED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI," + "decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array," + "Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt" ; const isGloballyWhitelisted = (key) => { return GLOBALS_WHITE_LISTED.split( "," ).includes(key); }; const hasOwn = (obj, key) => { return Object.prototype.hasOwnProperty.call(obj, key); }; const origin = {}; const _ctx = new Proxy(origin, { get(target, key, reciever) { if (hasOwn(target, key)) { Reflect.get(target, key, reciever); } else { console.warn( `Property ${JSON.stringify(key)} was accessed during render ` + `but is not defined on instance.` ); } }, has(target, key) { // 如果是 全局對象 返回false,不觸發get 攔截,從上一層作用域查找變量 // 如果不是 全局對象 返回true,觸發get 攔截 return !isGloballyWhitelisted(key); }, }); |
代碼很簡單,為什么這么簡單的代碼就能做到攔截?
因為 with 語句會觸發 has 攔截,當 has 返回 true,就會 觸發代理對象 get 攔截,如果返回 false, 則代理對象 get 攔截不會觸發,變量不在當前代理對象查找,直接查找更上一層作用域
本地預編譯版本
1
2
|
< div >{{test}}</ div > < div >{{Math.floor(1)}}</ div > |
to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, } from "vue" ; export function render(_ctx, _cache, $props, $setup, $data, $options) { return ( _openBlock(), _createBlock( _Fragment, null , [ _createVNode( "div" , null , _toDisplayString(_ctx.a), 1 /* TEXT */ ), _createVNode( "div" , null , _toDisplayString(Math.floor(1)), 1 /* TEXT */ ), ], 64 /* STABLE_FRAGMENT */ ) ); } |
從上面的代碼我們可以發現,非白名單標識符都添加了_ctx 變量前綴,那么是如何做到的呢?當本地編譯 template 時,處于轉換階段時會對 變量表達式節點NodeTypes.SIMPLE_EXPRESSION進行添加前綴處理,示例代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
const GLOBALS_WHITE_LISTED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI," + "decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array," + "Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt" ; const isGloballyWhitelisted = (key) => { return GLOBALS_WHITE_LISTED.split( "," ).includes(key); }; const isLiteralWhitelisted = (key)=>{ return 'true,false,null,this' .split( ',' ).includes(key) } export function processExpression( node ) { const rewriteIdentifier = (raw) => { return `_ctx.${raw}` } const rawExp = node.content if (isSimpleIdentifier(rawExp)) { const isAllowedGlobal = isGloballyWhitelisted(rawExp) const isLiteral = isLiteralWhitelisted(rawExp) if (!isAllowedGlobal && !isLiteral) { node.content = rewriteIdentifier(rawExp) } return node } |
當然上面的代碼只是簡化版本,原版插件還做了精確到了__props $setup,減短變量查詢路徑,提高性能,還有通過babel編譯復雜表達式比如:箭頭函數。
總結
整個 vue3 js 沙箱機制就解釋結束了,當初瀏覽器編譯版本困擾了我很久,因為不知道 has 可以攔截 with 語句變量查詢
參考
Proxy handler.has
說說 JS 中的沙箱
動手寫 js 沙箱
到此這篇關于詳解vue3 沙箱機制的文章就介紹到這了,更多相關vue3 沙箱機制內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/6950969989608243231