实现一个插值表达式处理函数

613 阅读2分钟

前言

在 vue 中我们可以在模板里使用插值表达式,可以很方便地访问 vue 实例上定义的响应式数据。现在有一个场景,需要实现一个 render 函数:该函数接受两个参数,一个是模版,一个是数据对象。模版通过插值表达式引用数据对象上的数据,通过 render 函数能够把模版转换为真实的字符串。

let template = '大家好,我的名字叫做{{ name }},我是一名{{ job }}。'

let data = {
  name: 'Henry',
  gender: 'male',
  job: 'software engineer'
}

// 实现 render 函数
function render (template, data) {
    // ...
}

简版

简化版只需要考虑访问到 key-value 这种简单场景,其中 value 类型都是 String。

function render (template = '', data) {
  if (data === null) return

  const MUSTACHE_REG = /{{.*?}}/ig
  const KEY_REG = /(?<={{(\s*?)).*?(?=(\s*?)}})/ig
  
  return template.replace(MUSTACHE_REG, function (match = '') {
    let key = match.match(KEY_REG)[0].trim()
    return data[key]
  })
}

这种方式对于数组就不适用了,会返回 undefined,考虑到插值表达式还能执行一些运算,所以这么写是不行的。

使用 eval 函数

eval()  函数会将传入的字符串当做 JavaScript 代码进行执行。

但是这仍然存在一个问题,就是 eval 执行字符串的时候,里面的变量上下文该如何获取?这时候就可以使用 with 函数,把 data 传递进去,这样就避免了访问数据时还得加上 data. 前缀。

let template = "大家好,我的名字叫做${ name.includes('H') ? name : '' },我是一名${ job },今年${ age[1] }岁。"

let data = {
  name: 'Henry',
  gender: 'male',
  job: 'software engineer',
  age: [24, 25]
}

function render (template = '', data) {
  if (data === null) return

  const MUSTACHE_REG = /\$\{.*?\}/ig
  const KEY_REG = /(?<=\$\{(\s*?)).*?(?=(\s*?)\})/ig
  
  return template.replace(MUSTACHE_REG, function (match = '') {
    let content = match.match(KEY_REG)[0].trim()
    let result
    with (data) {
      result = eval(content)
    }
    return result
  })
}

console.log(render(template, data)) // 大家好,我的名字叫做Henry,我是一名software engineer,今年25岁。

使用 eval 最大的问题就是存在安全隐患。eval() 是一个危险的函数,它使用与调用者相同的权限执行代码。如果你用 eval() 运行的字符串代码被恶意方(不怀好意的人)修改,最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。更重要的是,第三方代码可以看到某一个 eval() 被调用时的作用域,这也有可能导致一些不同方式的攻击。相似的 Function 就不容易被攻击。

eval() 通常比其他替代方法更慢,因为它必须调用 JS 解释器,而许多其他结构则可被现代 JS 引擎进行优化。

使用 new Function

更加稳妥的做法使用 new Function 的方式来执行插值表达式里的字符串,这时候需要开发者自行拼接函数体代码。

let template = "大家好,我的名字叫做 ${ name.includes('H') ? name : '' },我是一名 ${ job },今年 ${ age[1] } 岁。"

let data = {
  name: 'Henry',
  gender: 'male',
  job: 'software engineer',
  age: [24, 25]
}

function render (template = '', data) {
  if (data === null) return

  const MUSTACHE_REG = /\$\{.*?\}/ig
  const KEY_REG = /(?<=\$\{(\s*?)).*?(?=(\s*?)\})/ig
  
  return template.replace(MUSTACHE_REG, function (match = '') {
    let content = match.match(KEY_REG)[0].trim()
    let result = new Function('obj', 'with (obj) { return ' + content + '}')(data)
    return result
  })
}

console.log(render(template, data)) // 大家好,我的名字叫做 Henry,我是一名 software engineer,今年 25 岁。

到此为止就实现了一个插值表达式处理函数了。

不使用正则

function render(template, data) {
    let result = '';
    let i = 0;
    while (i < template.length) {
        if (template[i] === '$' && template[i + 1] === '{') {
            let endBraceIndex = template.indexOf('}', i);
            if (endBraceIndex !== -1) {
                let key = template.slice(i + 2, endBraceIndex).trim();
                if (data.hasOwnProperty(key)) {
                    result += data[key];
                }
                i = endBraceIndex + 1;
            } else {
                result += template[i];
                i++;
            }
        } else {
            result += template[i];
            i++;
        }
    }
    return result;
}

// let template = '大家好,我的名字叫做${ name },我是一名${ job }。';
let template = '大家好,我的名字叫做{{ name }},我是一名{{ job }}。';
let data = {
  name: 'Henry',
  gender: 'male',
  job: 'software engineer'
};

// console.log(render(template, data));

function replace (template = '', obj = {}) {
    if (!template) return ''

    let i = 0, len = template.length
    let result = ''
    while (i < len) {
        if (template[i] === '{' && template[i + 1] === '{') {
            let endBraceIndex = template.indexOf('}}', i)
            if (endBraceIndex !== -1) {
                let key = template.slice(i + 2, endBraceIndex).trim()
                const value = obj[key]
                result += value
                i = endBraceIndex + 2
            } else {
                result += template[i]
                i++
            }
        } else {
            result += template[i]
            i++
        }
    }

    return result
}

console.log(666, replace(template, data))