如何移除bindValue方法中的with语句?
先聊聊你的场景和现有问题
你现在做的是Web组件模板的绑定逻辑,把类似这样的模板表达式:
<div>{{name == 'Homer' ? 'No Homers Allowed' : name}}</div>
转换成能在组件属性作用域内执行的函数,核心的bindValue方法里用了with(scope)来让表达式直接访问作用域属性,但with已经被废弃了,而且你还得保证只有模板里实际用到的属性的getter才会触发——要是直接把作用域的属性当变量传给Function构造函数,整个scope的getter都会跑一遍,这就破坏了你做的依赖追踪逻辑。
先看看你现在的核心代码:
/** * @el [Element] Target HTML Element * @attr [String] attribute to bind * @match [Object] Result from regex.exec */ const bindValue = (el, attr, match) => { let binding = getBinding(el, attr); if (attr === "textContent") { let fn = new Function( `scope`, `with(scope) { return ${match[1]} }`, ); binding.attrs[attr].bindings.push((ctx, values) => { return ctx.replace(match[0], fn(values)); }); } };
咱们可以这么改:把表达式里的变量都换成scope.xxx
核心思路是:不用with来注入作用域,而是把模板表达式里的所有用户自定义标识符(比如name)都替换成scope.name,这样既不需要with,又只有实际用到的属性会被访问,对应的getter才会触发。
具体实现步骤
- 用正则匹配表达式里的JS标识符,同时排除JS自带的关键字(比如
if、else这些不能当成属性) - 把表达式里的每个有效标识符替换成
scope.xxx - 用这个处理后的表达式创建Function,完全抛弃
with
修改后的bindValue代码如下:
const bindValue = (el, attr, match) => { let binding = getBinding(el, attr); if (attr === "textContent") { const expr = match[1]; // 匹配JS标识符,同时排除关键字 const identifierRegex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g; const jsKeywords = new Set([ 'if', 'else', 'return', 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'this', 'function', 'class', 'let', 'const', 'var', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'async', 'await' ]); // 把表达式里的所有自定义标识符替换成 scope.xxx const scopedExpr = expr.replace(identifierRegex, (matchedId) => { return jsKeywords.has(matchedId) ? matchedId : `scope.${matchedId}`; }); // 创建不需要with的函数 const fn = new Function( 'scope', `return ${scopedExpr};` ); binding.attrs[attr].bindings.push((ctx, values) => { return ctx.replace(match[0], fn(values)); }); } };
为什么这个方案能满足你的需求?
- 没有
with了:完全符合规范,不会再有废弃语法的警告 - 仅触发用到的属性getter:比如表达式里只有
name,那只会执行scope.name,只有这个属性的getter会跑,其他属性完全不受影响,完美适配你的依赖追踪逻辑 - 功能完全兼容:处理后的表达式和原来用
with的逻辑完全一致,模板里的三元运算、比较逻辑都能正常执行
举个实际例子
原来的表达式是name == 'Homer' ? 'No Homers Allowed' : name,处理后会变成:scope.name == 'Homer' ? 'No Homers Allowed' : scope.name
生成的Function就是:
new Function('scope', `return scope.name == 'Homer' ? 'No Homers Allowed' : scope.name;`)
执行的时候只会访问scope.name,完全符合你的预期。
进阶:复杂表达式用AST解析(可选)
如果你的模板表达式支持复杂语法(比如对象链user.name、方法调用getGreeting()),上面的正则可能会有遗漏,这时候可以用AST解析器(比如acorn)来精准遍历表达式的语法树,替换所有用户自定义的标识符。举个简化示例:
import * as acorn from 'acorn'; import { generate } from 'escodegen'; const getScopedExpr = (expr) => { // 解析表达式成AST const ast = acorn.parse(expr, { ecmaVersion: 'latest' }); // 遍历AST替换标识符 const traverseNode = (node) => { if (node.type === 'Identifier') { const jsKeywords = new Set([/* 关键字集合同上 */]); if (!jsKeywords.has(node.name)) { node.name = `scope.${node.name}`; } } // 递归处理所有子节点 Object.values(node).forEach(child => { if (typeof child === 'object' && child !== null) { traverseNode(child); } }); }; traverseNode(ast.body[0].expression); // 把AST转回代码 return generate(ast).slice(0, -1); // 去掉自动添加的分号 };
然后在bindValue里用const scopedExpr = getScopedExpr(expr);替换正则处理的部分就行,复杂场景下更可靠。
备注:内容来源于stack exchange,提问作者hobberwickey

