从实现来看当匹配到插值语法的时候,我们直接把文本节点的内容全部替换了,如果我们的文本是这样的格式呢:"this is content: {{msg}} is't over",那么最终渲染的还是只有msg的状态值,其他都丢失了,这样显得有点糟糕,我们就乘胜追击,再完善一下吧。首先分析一下为了实现文本完整的渲染,我们要将静态的文本和插值文本提取出来,然后再拼接起来才是最终符合预期的结果,从左到右依次解析文本,"this is content: {{msg}} is't over"需要分成三部分,分别是["this is content: ", "{{msg}}", " is't over"],msg经过转换后变成["this is content: ", "{hello world!", "is't over"],最后拼接起来回填到文本节点就可以了:
const RE = /{{([^]+?)}}/g; function walk(node, context) { const { nodeType } = node; if (nodeType === 1) { // Element return walkChildren(node, context); } if (nodeType === 3) { // Text const text = node.textContent; let i = 0; // 保存上一个匹配{{}}格式的字符结束索引 if (text.includes('{{')) { // 先判断是否有"{{"字符,有才进行下面的判断 let match = null; const segments = []; // 保存所有截断的文本 while ((match = RE.exec(text))) { segments.push(text.slice(i, match.index)); // {{之前的字符 i = match.index + match[0].length; const exp = match[1].trim(); // 删除表达式前后的空白字符 segments.push(context.scope[exp]); // msg的值求得之后,放入数组中便于后面拼接 } segments.push(text.slice(i)); // 最后一个}}后面的字符 node.textContent = segments.join(''); } } } 支持表达式通过拼接字符串的方式我们完成了渲染的基本要求,但是熟悉vue语法的同学会说,双花括号内部是支持js表达式的,既然实现到这里了,我们就支持一下表达式吧,首先分析一下,表达式里面的标识符指向scope对象的属性值,一个还好说,那么两个怎么通过简单的方式去实现呢,挨个挨个去把标识符提取出来,然后计算再合并么,想想都麻烦,那有没有简单的方式呢,我都这么说了,当然是有的,先看下实现原理吧:
function createFunc(exp) { return new Function(`scope`, `with(scope) { return (${exp}) }`) } const f = foo('a + b'); f({ a: 1, b: 2 });通过createFunc创建一个新的函数,with将exp表达式的作用域限定在scope中,这样当执行a+b的时候,相当于scope.a + scope.b,最后将结果返回,最终执行的函数如下所示:
(function(scope) { with(scope) { return (a + b); } })({a: 1, b: 2})知晓了原理之后,我们就补齐表达式的计算吧:
function createFunc(exp) { return new Function(`scope`, `with(scope) { return (${exp}) }`); } ... function walk(node, context) { const { nodeType } = node; if (nodeType === 1) { // Element return walkChildren(node, context); } if (nodeType === 3) { // Text const text = node.textContent; let i = 0; if (text.includes('{{')) { let match = null; const segments = []; // 保存所有截断的文本 while ((match = RE.exec(text))) { segments.push(text.slice(i, match.index)); i = match.index + match[0].length; const exp = match[1].trim(); // 删除表达式前后的空白字符 segments.push(createFunc(exp)(context.scope)); // createFunc(exp)生成函数,再将scope传入执行 } segments.push(text.slice(i)); node.textContent = segments.join(''); } } } ...现在我们写的第一版框架就完成啦,完整的v1版本代码可点击这里,当然现在功能十分有限,没有其他指令集,没有响应式,不过作为学习petite-vue的第一步,已经迈出去啦,给自己一个赞吧,持之以恒,终会有收获的。这里预告一下第二篇的内容,我们将分析和实现响应式方面的内容。
福禄·研发中心 福袋