一场风雨,两行泪
面试前,学学 正则表达式如何实现模板编译 🤣
面试官问:下面这段代码怎么实现页面渲染?
yamllet str = '我是{{name}},年龄{{age}},性别{{sex}}' let person = { name: '张三', age: 18, sex: '男' }你:这不简单吗?用
replace替换,配合正则表达式匹配模板字符串中的占位符就行了。问: 那你能用函数
compile来实现一下吗?
正文开始~~~
前言: 模板编译的核心流程(重点!!!)
- 模板字符串转 AST:将模板字符串解析为抽象语法树(AST),AST 是一个树形结构,包含模板的语法结构(标签、属性、文本节点等)。
- 优化 AST:通过静态分析,识别静态节点,后续会跳过这些节点的重新渲染。
- 生成渲染函数:将 AST 转换为渲染函数,渲染函数会生成虚拟 DOM(VNode)。
- 更新视图:将虚拟 DOM 渲染到页面,实现数据驱动视图更新。
在深入模板编译之前,我们先回顾一下正则表达式的基础知识,因为它是实现模板编译的核心工具之一。
正则表达式是啥 ?
正则表达式(Regular Expressions)是一种强大的文本处理工具,可以用来匹配、查找和替换字符串中的特定模式。下面解释正则主要语法:
1. 匹配特定字符
- 匹配数字 :
\d:匹配一个数字(0-9)。\d+:匹配一个或多个数字。- 示例:
/\d+/g可以匹配字符串中的所有数字。
- 匹配字母或单词 :
\w:匹配一个字母、数字或下划线([A-Za-z0-9_])。\w+:匹配一个或多个字母、数字或下划线。- 示例:
/\w+/g可以匹配字符串中的所有单词。
- 匹配空白字符 :
\s:匹配一个空白字符(空格、制表符、换行等)。\s+:匹配一个或多个空白字符。- 示例:
/\s+/g可以匹配字符串中的所有空白。
2. 匹配特定位置
- 匹配开头和结尾 :
^:匹配字符串的开头。$:匹配字符串的结尾。- 示例:
/^Hello/匹配以Hello开头的字符串。
- 匹配单词边界 :
\b:匹配单词的边界(单词的开头或结尾)。- 示例:
/\bcat\b/匹配独立的单词cat,而不会匹配category中的cat。
3. 匹配重复模式
- 匹配固定次数 :
{n}:匹配前面的字符恰好n次。- 示例:
/\d{3}/匹配连续的 3 个数字。
- 匹配范围次数 :
{n,m}:匹配前面的字符至少n次,最多m次。- 示例:
/\d{2,4}/匹配 2 到 4 个连续数字。
- 匹配零次或多次 :
*:匹配前面的字符零次或多次。- 示例:
/a*/可以匹配""(空)、"a"、"aa"等。
- 匹配一次或多次 :
+:匹配前面的字符一次或多次。- 示例:
/a+/可以匹配"a"、"aa",但不能匹配""。
- 匹配零次或一次 :
?:匹配前面的字符零次或一次。- 示例:
/a?/可以匹配""或"a"。
4. 匹配字符集合
- 匹配特定字符 :
[abc]:匹配a、b或c中的任意一个字符。- 示例:
/[aeiou]/g匹配字符串中的所有元音字母。
- 匹配字符范围 :
[a-z]:匹配任意小写字母。[A-Z]:匹配任意大写字母。[0-9]:匹配任意数字。- 示例:
/[a-zA-Z]/g匹配所有字母(大小写均可)。
- 排除特定字符 :
[^abc]:匹配除了a、b、c之外的任意字符。- 示例:
/[^0-9]/g匹配所有非数字字符。
5. 分组和捕获
- 分组 :
(abc):将abc作为一个分组。- 示例:
/(\d{2})-(\d{2})/可以匹配12-34,并捕获12和34。
- 非捕获分组 :
(?:abc):分组但不捕获。- 示例:
/(?:\d{2})-(\d{2})/匹配12-34,但只捕获34。
6. 贪婪与非贪婪匹配
- 贪婪匹配 :
- 默认情况下,正则表达式会尽可能多地匹配字符。
- 示例:
/a.*b/匹配"aabab"中的"aabab"。
- 非贪婪匹配 :
- 在量词后加
?,表示尽可能少地匹配字符。 - 示例:
/a.*?b/匹配"aabab"中的"aab"。
- 在量词后加
7. 常见用途的正则表达式
-
匹配 URL:
regs/^(https?://)?([\da-z.-]+).([a-z.]{2,6})([/\w .-]*)*/?$/ -
匹配手机号(中国大陆) :
regs/^1[3-9]\d{9}$/ -
匹配日期(YYYY-MM-DD) :
regs/^\d{4}-\d{2}-\d{2}$/
从上面可以看出正则用途很多,包括:
- 验证输入(如邮箱、手机号)。
- 提取特定格式的文本(如日期、URL)。
- 替换字符串中的内容(如模板引擎)。
模板编译
在前端开发中,模板编译 允许我们将模板字符串(如 {{name}})转换为实际的 HTML 内容,并根据数据动态更新视图。下面我们以 Vue.js 和 React 为例,简单解释模板编译的实现原理。
1. Vue.js 的模板编译
Vue.js 使用模板引擎来实现数据绑定和视图更新。它的模板语法非常直观,允许开发者在 HTML 中直接嵌入 JavaScript 表达式和指令。例如:
html
<div id="app">
{{ message }}
</div>
在这个例子中,{{ message }} 是一个插值表达式,Vue 的模板编译器会将其转换为渲染函数。渲染函数在运行时根据数据的变化动态更新 DOM。
2. React 的 JSX
React 虽然没有内置的模板引擎,但它使用 JSX(JavaScript XML)来描述 UI。JSX 看起来很像 HTML,但实际上它是 JavaScript 的一种扩展语法。例如:
js
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
在这个例子中,<h1>Hello, {props.name}</h1> 是一个 JSX 表达式,它会被 Babel 等工具编译为 React.createElement 函数调用。React 的虚拟 DOM 机制会根据这些函数调用生成实际的 DOM 节点,并在数据变化时高效地更新 DOM。
为什么要使用正则表达式实现模板编译?
它允许我们将模板字符串(如
{{name}})转换为实际的 HTML 内容,并根据数据动态更新视图。使用正则表达式实现模板编译有以下几个优点:
- 灵活性:正则表达式可以灵活地匹配各种复杂的模式,适合处理模板字符串中的占位符。
- 高效性:正则表达式的匹配和替换操作非常高效,适合处理大量的模板字符串。
- 简洁性:使用正则表达式可以大大简化代码,减少重复的逻辑。
使用正则表达式实现模板编译
接下来,我们通过一个简单的例子,使用正则表达式实现模板编译。
1. 基础实现
html
<script>
let str = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '张三',
age: 18,
sex: '男'
};
function compile(template, data) {
let reg = /{{(\w+)}}/;
while (reg.test(template)) {
let key = reg.exec(template)[1]; // 捕获占位符中的内容
let value = data[key]; // 从数据中获取对应的值
template = template.replace(reg, value); // 替换占位符
}
return template;
}
console.log(compile(str, person)); // 输出:我是张三,年龄18,性别男
</script>
思路解析
- 正则匹配 :
- 使用
/{{(\w+)}}/g匹配模板中的占位符,如{{name}}。 (\w+)捕获{{}}中的内容(如name)。
- 使用
- 提取与替换 :
- 用
reg.exec(template)返回数组,[0]是完整匹配(如{{name}}),[1]是捕获内容(如name)。 - 通过
let arr = reg.exec(template)[1];提取变量名,从数据中获取对应值。 - 用
template.replace(reg, value)替换占位符为实际值。
- 用
这时候你为了炫技,又写出其他表达形式
2. 优化实现
js
function compile(template,data) {
let reg = /\{\{([a-z]+)\}\}/g
// 方式一:使用展开运算符
return template.replace(reg,(...args)=>{
return data[args[1]]
})
// 方式二:直接传并且判断path是否还需要替换
return template.replace(reg,(match,path) => {
console.log(match,path);
return path in data ? data[path] : ''
})
}
这里发一下match,path对应内容,大家应该更好理解:
3. 递归实现
为了确保模板中的所有占位符都被替换,我们可以使用递归:
html
<script>
function compile(template, data) {
let reg = /{{(\w+)}}/;
if (reg.test(template)) {
let key = reg.exec(template)[1];
let value = data[key] || '';
template = template.replace(reg, value);
return compile(template, data); // 递归调用
} else {
return template;
}
}
console.log(compile(str, person)); // 输出:我是张三,年龄18,性别男
</script>
我们也看看正则表达式对象执行几次:
性能优化
在实际项目中,模板编译的性能非常重要。给出一些优化建议:
- 缓存编译结果:如果模板和数据没有变化,可以缓存编译结果,避免重复编译。
- 减少正则匹配次数 :使用全局正则表达式(
g标志)一次性匹配所有占位符。 - 使用 AST 优化:对于复杂的模板,可以将其转换为 AST 并进行静态分析,跳过不必要的重新渲染。