preface

There are two types of VUE3 sandboxes

  1. Browser compiled version, browser version is usedwithGrammar andproxyAgent to intercept
  2. Native precompiled versions, using a conversion plug-in during the template precompiled conversion phasetransformExpressionHangs a non-whitelisted identifier under a component proxy object

Browser compiled version

The result of compiling the render function

<div>{{test}}</div>
<div>{{Math.floor(1)}}</div>
Copy the code

to

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 */)); }};Copy the code

The variable identifier is not prefixed, but is wrapped in the with syntax to extend the scope chain. For example, there is no test variable in the current scope chain, and the variable will be searched from the upper scope until the global scope is found. However, in fact, the variable will only be searched on _ctx. The principle is very simple.

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 The ${JSON.stringify(key)} was accessed during render ` +
          `but is not defined on instance.`); }},has(target, key) {
    // Returns false if it is a global object, does not trigger get interception, and looks up variables from the upper scope
    // Return true if the object is not global, triggering get interception
    return! isGloballyWhitelisted(key); }});Copy the code

The code is very simple, why such simple code can do interception? Because the with statement triggers HAS interception, when HAS returns true, proxy get interception is triggered. If false is returned, proxy GET interception is not triggered, and the variable is not looked up in the current proxy object, but directly in the next level of scope

Local precompiled version

<div>{{test}}</div>
<div>{{Math.floor(1)}}</div>
Copy the code

to

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 */)); }Copy the code

From the code above, we can see that non-whitelisted identifiers are prefixed with the _CTx variable. How is this done? When template is compiled locally, the variable expression node NodeTypes.SIMPLE_EXPRESSION is prefixed during the conversion phase, as shown in the following example:

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
  }
Copy the code

Of course, the above code is just a simplified version of the plugin. The original plugin also makes things like __props $setup more accurate, shorting variable query paths to improve performance, and compiling complex expressions like arrow functions through Babel.

conclusion

This is the end of the vue3 JS sandbox. The browser version bothered me for a long time because I didn’t know that HAS could intercept with variable queries

reference

  1. Proxy handler.has
  2. Talk about sandboxes in JS
  3. Write js sandbox