SoFunction
Updated on 2025-04-04

Detailed explanation of the sandbox mechanism of vue3

Preface

vue3 sandbox is mainly divided into two types

  1. The browser version is built using the with syntax plus proxy intercept
  2. Local precompiled version, by using the conversion plugin transformExpression to hang the non-whitelist identifier under the component proxy object during the conversion stage of the template precompilation stage, using the conversion plug-in transformExpression, to hang the whitelist identifier under the component proxy object

Browser compiled version

The render function compilation result

<div>{{test}}</div>
<div>{{(1)}}</div>

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((1)),
            1 /* TEXT */
          ),
        ],
        64 /* STABLE_FRAGMENT */
      )
    );
  }
};

From the above code, we can find that the variable identifier does not have a prefix added, but just wrapped it with the with syntax to extend the scope chain. So how do you achieve js sandbox interception? For example, the variable test, theoretically speaking, the current scope chain does not have a test variable, and the variable will be searched from the previous scope until the global scope is found, but in fact it will only look up on _ctx. The principle is very simple. _ctx is a proxy object, so how do we use Proxy to intercept, the example code is as follows:

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) =&gt; {
  return GLOBALS_WHITE_LISTED.split(",").includes(key);
};

const hasOwn = (obj, key) =&gt; {
  return (obj, key);
};

const origin = {};
const _ctx = new Proxy(origin, {
  get(target, key, reciever) {
    if (hasOwn(target, key)) {
      (target, key, reciever);
    } else {
      (
        `Property ${(key)} was accessed during render ` +
          `but is not defined on instance.`
      );
    }
  },
  has(target, key) {
    // If it is a global object, return false, and get interception is not triggered, and variables are found from the previous layer scope.    // If it is not a global object, return true, trigger get intercept    return !isGloballyWhitelisted(key);
  },
});

The code is very simple, why can such simple code intercept? Because the with statement will trigger has interception, when has returned true, the proxy object get interception will be triggered. If false is returned, the proxy object get interception will not be triggered. The variable is not found in the current proxy object, and it will directly search for a higher-level scope.

Local precompiled version

<div>{{test}}</div>
<div>{{(1)}}</div>

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((1)),
          1 /* TEXT */
        ),
      ],
      64 /* STABLE_FRAGMENT */
    )
  );
}

From the above code, we can find that non-whitelist identifiers have the _ctx variable prefix added, so how do I do it? When template is compiled locally, the variable expression node NodeTypes.SIMPLE_EXPRESSION will be prefixed when it is in the conversion stage. The sample code is as follows:

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 = 
  if (isSimpleIdentifier(rawExp)) {
    const isAllowedGlobal = isGloballyWhitelisted(rawExp)
    const isLiteral = isLiteralWhitelisted(rawExp)
    if (!isAllowedGlobal && !isLiteral) {
       = rewriteIdentifier(rawExp)
    }
    return node
  }

Of course, the above code is just a simplified version. The original plug-in also made the exact __props $setup, shortening the variable query path, improving performance, and compiling complex expressions such as arrow functions through babel.

Summarize

The entire vue3 js sandbox mechanism has ended. The browser compiled version has troubled me for a long time because I don't know that has it can intercept with statement variable query

The above is the detailed explanation of the sandbox mechanism of vue3. For more information about the sandbox mechanism of vue3, please pay attention to my other related articles!