过滤器的奥秘

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-23

Vuejs允许我们自定义过滤器来格式化文本,它可以用在两个地方,双花括号插值 和 v-bind 表达式 (2.1.0+)

<p>{{ message | capitalize }}</p>

<p v-bind:id="rawId | formatId"></p>

<!-- 也可以串联 -->
<p>{{ message | filterA | filterB }}</p>

<!-- 也可以传参,message会作为filterA的第一个参数, 'arg1' 作为第二个,依次类推 -->
<p>{{ message | filterA('arg1', arg2) }}</p>

过滤器的原理概述

<p>{{ message | capitalize }}</p>

这个过滤器会在模板编译阶段编译成下面的样子:

_s(_f('capitalize')(message))

_f函数是 resolveFilter 的别名,其作用是从 this.$options.filters 中找到注册过的过滤器 capitalize 并返回,然后调用,并将 message 作为参数传递。

_s 之前也提到过,是 toString 函数的别名,用其返回的结果直接去渲染视图。

function toString() {
  return val == null
    ? ''
    : typeof val === 'object'
      ? JSON.stringify(val, null, 2)
      : String(val)
}

串联过滤器

<p>{{ message | capitalize | suffix }}</p>

最终会被编译成下面的样子:

_s(_f('suffix')(_f('capitalize')(message)))

表达式 message 的值作为参数传递给 capitalize 过滤器函数中,而 capitalize 的返回结果又传递给 suffix 过滤器函数,也就是说 capitalize 过滤器的输出是 suffix 过滤器的输入。

过滤器接受参数

<p>{{ message | capitalize(true) | suffix('!') }}</p>

会被编译成:

_s(_f('suffix')(_f('capitalize')(message, true), '!'))
// 模板编译之后会将模板中给过滤器设置的参数添加在过滤器函数的参数中,从第二个开始

resolveFilter的内部原理

resolveFilter 用来查找过滤器。

import { identity, resolveAsset } from 'core/util/index'

export function resolveFilter(id) {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}

// core/util/index
export const identity = _ => _ // 该函数返回与参数相同的值

// 不仅用来查找过滤器,还用来查找组件和指令
export function resolveAsset(options, type, id, warnMissing) {
  if (typeof id !== 'string') {
    return
  }

  const assets = options[type]

  // 先检查本地注册的
  // 这个例子中assets[id]就是this.$options.filters[id]
  if (hasOwn(assets, id)) return assets[id]

  // 通过不同形式的格式化来查找
  const camelizedId = camelize(id) // 将名称驼峰化 可以将 a-b 转换成 aB
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]

  const PascalCaseId = capitalize(camelizedId) // 将名称首字母大写
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]


  // 检查原型链
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options)
  }
  return res
}

resolveAsset 函数在查找过滤器的过程中并没有去Vue构造函数中搜索过滤器,是因为在初始化Vuejs实例时,把全局过滤器与组件内注册的过滤器合并到了 this.$options.filters 中,所以只需要在 options[type] 中查找即可。

解析过滤器

在模板编译时,提供了一个 parseFilters 函数专门用来解析过滤器。

function parseFilters(expr) {
  let filters = exp.split('|')
  let expression = filters.shift().trim() // 获取并截掉第一个,即初始参数,前面例子中的 message
  let i
  if (filters) {
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i].trim())
    }
  }

  return expression;
}

function wrapFilter(exp, filter) {
  const i = filter.indexOf('(')
  
  if (i < 0) { // 过滤器没有参数
    return `_f("${filter}")(${exp})`
  } else {  // 有参数
    const name = filter.slice(0, i) // 截取过滤器函数名
    const args = filter.slice(i+1) // 截取剩余的参数部分
    return `_f("${name}")(${exp},${args}` // 没有反括号,因为截取args的时候就截取了末尾的反括号
  }
}

编译过滤器的过程分两步:解析和拼接字符串。