filter过滤器

在分析过滤器的实现原理之前,我们需要先回顾一下过滤器的用法,包括:注册方式、使用方式。这样做的目的,是为了方便我们更好地理解过滤器的实现原理,同时也为我们分析过滤器提供了一种思路指引。

过滤器的注册和使用

过滤器的注册

同指令一样,过滤器的注册分两种方式:全局注册和局部注册。

全局注册过滤器,可以使用Vue.filter()来实现,例如:

Vue.filter('reverse', function (value) {
  return value.split('').reverse().join('')
})

全局注册的过滤器,都会在Vue.options['filters']属性上面。

局部注册过滤器,需要撰写在组件的filters选项中,例如:

export default {
  name: 'App',
  filters: {
    reverse (value) {
      return value.split('').reverse().join('')
    }
  }
}

局部注册的过滤器,都会在组件的vm.$options['filters']属性上面。

过滤器的使用

过滤器的使用有两种场景:插值表达式和v-bind表达式。

<!-- v-bind表达式 -->
<template>
  <div :msg="msg | reverse"></div>
</template>

<!-- 双括号插值表达式 -->
<template>
  <div>{{msg | reverse}}</div>
</template>

在使用过滤器的时候,还可以给过滤器传递参数:

<template>
  <div>{{msg | reverse('default msg')}}</div>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      msg: ''
    }
  },
  filters: {
    reverse (value, defaultValue) {
      if (!value) {
        return defaultValue
      }
      return value.split('').reverse().join('')
    }
  }
}
</script>

如果存在多个过滤器,还可以串联起来一起使用:

<template>
  <div>{{msg | filterA | filterB}}</div>
</template>

串联的过滤器,会从左到右依次执行。以上面代码为例,filterA过滤器的处理结果会传递给filterB过滤器的第一个参数value

插值表达式过滤器的解析

我们使用如下代码来分析插值表达式过滤器的解析:

new Vue({
  el: '#app',
  data () {
    return {
      msg: 'ABCD'
    }
  },
  filters: {
    reverse (value) {
      return value.split('').reverse().join('')
    }
  },

  template: '<div>{{msg | reverse}}</div>'
})

我们都知道,过滤器是用来处理文本的,那么在过滤器解析这个小节,我们来回顾一下parse阶段是如何处理文本的。

const template = '<div>{{msg | reverse}}</div>'
const ast = parse(template.trim(), options)

parse执行的时候,会调用parseHTML方法。在这个方法的while循环中,会首先调用一次parseStartTag,也就是匹配div的开始标签。然后把template模板截取一次,截取后其值如下:

const template = '{{msg | reverse}}</div>'

在第二次while循环的时候,会通过div结束标签的位置,把文本内容截取出来,如下:

const test = '{{msg | reverse}}'

截取文本后,会触发一次chars钩子函数,在这个钩子函数中通过parseText来处理文本内容:

chars (text, start, end) {
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
    child = {
      type: 2,
      expression: res.expression,
      tokens: res.tokens,
      text
    }
  }
}

我们来看一下parseText方法的代码:

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

虽然parseText方法的代码有点长,但我们关注的核心只有两点:tagRE插值正则表达式和parseFilters方法,因此parseText方法精简后代码如下:

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  // ...省略代码
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // ...省略代码
    // tag token
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  // ...省略代码
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

while循环条件中,它执行了一次tagRE匹配并把匹配结果赋值给了match,此时的match结果如下:

const match = [
  0: '{{msg | reverse}}',
  1: "msg | reverse",
  index: 0,
  input: "{{msg | reverse}}"
]

因为match结果为一个数组,所以在while循环内部,它会调用parseFilters方法来解析过滤器,我们来看一下parseFilters方法的代码:

export function parseFilters (exp: string): string {
  let inSingle = false
  let inDouble = false
  let inTemplateString = false
  let inRegex = false
  let curly = 0
  let square = 0
  let paren = 0
  let lastFilterIndex = 0
  let c, prev, i, expression, filters

  for (i = 0; i < exp.length; i++) {
    prev = c
    c = exp.charCodeAt(i)
    if (inSingle) {
      if (c === 0x27 && prev !== 0x5C) inSingle = false
    } else if (inDouble) {
      if (c === 0x22 && prev !== 0x5C) inDouble = false
    } else if (inTemplateString) {
      if (c === 0x60 && prev !== 0x5C) inTemplateString = false
    } else if (inRegex) {
      if (c === 0x2f && prev !== 0x5C) inRegex = false
    } else if (
      c === 0x7C && // pipe
      exp.charCodeAt(i + 1) !== 0x7C &&
      exp.charCodeAt(i - 1) !== 0x7C &&
      !curly && !square && !paren
    ) {
      if (expression === undefined) {
        // first filter, end of expression
        lastFilterIndex = i + 1
        expression = exp.slice(0, i).trim()
      } else {
        pushFilter()
      }
    } else {
      switch (c) {
        case 0x22: inDouble = true; break         // "
        case 0x27: inSingle = true; break         // '
        case 0x60: inTemplateString = true; break // `
        case 0x28: paren++; break                 // (
        case 0x29: paren--; break                 // )
        case 0x5B: square++; break                // [
        case 0x5D: square--; break                // ]
        case 0x7B: curly++; break                 // {
        case 0x7D: curly--; break                 // }
      }
      if (c === 0x2f) { // /
        let j = i - 1
        let p
        // find first non-whitespace prev char
        for (; j >= 0; j--) {
          p = exp.charAt(j)
          if (p !== ' ') break
        }
        if (!p || !validDivisionCharRE.test(p)) {
          inRegex = true
        }
      }
    }
  }
  if (expression === undefined) {
    expression = exp.slice(0, i).trim()
  } else if (lastFilterIndex !== 0) {
    pushFilter()
  }
  function pushFilter () {
    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
    lastFilterIndex = i + 1
  }
  if (filters) {
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i])
    }
  }
  return expression
}

你可能会被parseFilters方法长长的代码吓到,尤其是for循环中的代码,但其实for循环所做的事情其实并不复杂。

我们先跳过for循环,直接看for循环结束后的结果:

let filters = ['reverse']
let expression = 'msg'

for循环结束后,我们得到了一个filters过滤器数组和expression表达式,然后在parseFilters方法的最末尾,它会遍历filters数组,调用wrapFilter来处理表达式:

function wrapFilter (exp: string, filter: string): string {
  const i = filter.indexOf('(')
  if (i < 0) {
    // _f: resolveFilter
    return `_f("${filter}")(${exp})`
  } else {
    const name = filter.slice(0, i)
    const args = filter.slice(i + 1)
    return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
  }
}

wrapFilter方法的代码很简单,以我们的例子为例调用完毕后,expression的值如下:

const expression = '_f("reverse")(msg)'

那么,让我们结合过滤器其它使用案例,来分析一下wrapFilter方法的返回结果。

// 串联过滤器
let filters = ['filterA', 'filterB']
let expression = 'msg'
const result = '_f("filterB")(_f("filterA")(msg))'

// 参数过滤器
let filters = ['reverse("default msg")']
let expression = 'msg'
const result = '_f("reverse")(msg,"default msg")'

搞清楚了wrapFilter方法后,我们可能会有如下两个问题:

  1. _f函数是什么?
  2. parseFilters方法的for循环到底是如何正确的解析出filters数组和expression表达式的?

_f函数

_f函数其实就是resolveFilter函数的缩写形式,在codegen代码生成章节我们介绍过,现在来回顾一下:

export function installRenderHelpers (target: any) {
  target._s = toString          // 转字符串
  target._l = renderList        // 处理v-for列表
  target._t = renderSlot        // 处理插槽
  target._m = renderStatic      // 处理静态节点
  target._f = resolveFilter     // 处理过滤器
  target._v = createTextVNode   // 创建文本VNode
  target._e = createEmptyVNode  // 创建空VNode
}

resolveFilter方法定义在src/core/instance/render-helpers/resolve-filter.js文件中:

import { identity, resolveAsset } from 'core/util/index'
export function resolveFilter (id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}

resolveFilter中,它又调用了resolveAsset方法。在介绍resolveAsset方法之前,我们先来说明resolveFilter方法是做什么的:resolveFilter是用来从组建实例中获取指定id(name)的过滤器的。

还记得我们在分析Vue源码的过程中,经常提到的一个ASSET_TYPES数组吗?

const ASSET_TYPES = ['component', 'directive', 'filter']

因为Vue对于componentdirectivefilter的处理方式极其相似,所以resolveAsset方法的作用就是在组件的$options选项上,获取指定id(name)的组件、指令或过滤器。其代码也不是很复杂,如下:

export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  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
}

for循环逻辑解析

在分析之前,我们先看几个特殊字符:

// 成对特殊字符
0x22 === "
0x27 === '
0x28 === (
0x29 === )
0x5B === [
0x5D === ]
0x60 === `
0x7B === {
0x7D === }

// 过滤器管道符号
0x7C === |

假如for循环时,我们有这样一个exp字符串:

const exp = 'msg | reverse'

对于以上例子,for循环遍历过程如下:

  1. i=0,此时的字符为m,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  2. i=1,此时的字符为s,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  3. i=2,此时的字符为g,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  4. i=3,此时的字符为空格符,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  5. i=4,此时的字符为|,不是成对特殊字符,但是过滤器管道符号,走如下代码后expression被赋值为msglastFilterIndex5
else if (
  c === 0x7C && // pipe
  exp.charCodeAt(i + 1) !== 0x7C &&
  exp.charCodeAt(i - 1) !== 0x7C &&
  !curly && !square && !paren
) {
  if (expression === undefined) {
    // first filter, end of expression
    lastFilterIndex = i + 1
    expression = exp.slice(0, i).trim()
  } else {
    // ...省略代码
  }
}
  1. i=5,此时的字符为空格符,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  2. i=6,此时的字符为r,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  3. i=7,此时的字符为e,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  4. i=8,此时的字符为v,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  5. i=9,此时的字符为e,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  6. i=10,此时的字符为r,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  7. i=11,此时的字符为s,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  8. i=12,此时的字符为e,不是成对特殊字符,也不是过滤器管道符号,继续下一次循环。
  9. i=13for循环结束,走如下代码逻辑:
if (expression === undefined) {
  // ...省略代码
} else if (lastFilterIndex !== 0) {
  pushFilter()
}

function pushFilter () {
  (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
  lastFilterIndex = i + 1
}

以上过程完毕后,就得到了我们想要的结果:

let filters = ['reverse']
let expression = 'msg'

虽然for循环的解析过程分析完毕了,但你可能依然不是很满意,你可能还有如下几个问题:

  1. 成对特殊字符有什么作用?
  2. 串联的过滤器是如何正确解析的?

一、成对特殊字符有什么作用?
我们在for循环遍历的过程中,如果遇到了成对的特殊符号,必须等这些符号闭合以后,才能截取expression表达式,例如:

const exp = '(age > 18 ? "yes" : "no") | reverse'

只有()成对闭合时,我们才能把(age > 18 ? "yes" : "no")当做一个整体赋值为expression表达式。

二、串联的过滤器是如何正确解析的?
对于这个问题,很容易解答,其中起到关键作用的就是lastFilterIndex变量,例如:

const exp = 'msg | filterA | filterB'

在第一次匹配到|符号时,lastFilterIndex的值为5,第二次匹配到|符号时此时的i索引值为14,那么索引5-14之前的字符串就是第一个过滤器的名字,既:filterA

以上匹配规则可以使用一句话来总结:两个管道符号中间的字符串就是过滤器的名字

parseFilters分析完毕后,我们来看一下parseText的返回结果:

const tokens = ['_s(_f("reverse")(msg))']
const rawTokens = [
  { '@binding': "_f("reverse")(msg)" }
]

parseText存在返回结果时,parse的过程基本已经结束,我们拿到的ast对象如下:

const ast = [
  {
    type: 1,
    tag: 'div',
    children: [
      {
        type: 2,
        text: '{{msg | reverse}}',
        expression: '_s(_f("reverse")(msg))',
        tokens: [
          { '@binding': "_f("reverse")(msg)" }
        ]
      }
    ]
  }
]

接下来,调用generate进行代码生成,由于这个过程我们已经在之前的章节中详细分析过了,所以这里我们直接写结果:

const code = generate(ast, options)
// code 打印结果
{
  render: "with(this){return _c('div',[_v(_s(_f("reverse")(msg)))])}",
  staticRenderFns: []
}

v-bind表达式过滤器的解析

我们使用如下示例代码来分析v-bind表达式过滤器的解析过程:

new Vue({
  el: '#app',
  data () {
    return {
      msg: 'ABCD'
    }
  },
  filters: {
    reverse (value) {
      return value.split('').reverse().join('')
    }
  },
  template: '<div :msg="msg | reverse"></div>'
})

由于v-bind是一种特殊的指令,所以前半部分的parse解析的过程完全是按照指令的模式在进行,在触发div结束标签之前,ast解析结果如下:

const ast = {
  type: 1,
  tag: 'div',
  attrsList: [
    { name: ':msg', value: "msg | reverse" }
  ],
  attrsMap: {
    ':msg': 'msg | reverse'
  },
  rawAttsMap: {
    ':msg': { name: ':msg', value: "msg | reverse" }
  }
}

当触发div结束标签钩子函数end的时候,调用processElement来处理:

export function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  // ...省略代码
  processAttrs(element)
  return element
}

紧接着,我们来看非常熟悉的processAttrs方法:

import { parseFilters } from './filter-parser'
export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/
export const bindRE = /^:|^\.|^v-bind:/
export const onRE = /^@|^v-on:/
function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      el.hasBindings = true
      // modifiers省略代码
      if (bindRE.test(name)) {
        name = name.replace(bindRE, '')
        value = parseFilters(value)
        // v-bind省略代码
      } else if (onRE.test(name)) {
        // v-on省略代码
      } else {
        // normal directives省略代码
      }
    } else {
      // ...省略代码
    }
  }
}

代码分析:三个正则表达式的作用我们在之前的指令章节已经分析过,因为在这个章节我们主要分析v-bind表达式过滤器的解析过程,所以bindRE正则表达式匹配成功。此时的value值为msg | reverse,调用parseFilters来处理过滤器。其中这里的parseFilters和我们之前提到的插值表达式过滤器的parseFilters是同一个,所以value解析结果如下:

const name = 'msg'
const value = '_f("reverse")(msg)'

parse过程完全结束时,我们可以得到如下ast对象:

const ast = {
  type: 1,
  tag: 'div',
  attrs: [
    { name: 'msg', value: '_f("reverse")(msg)' }
  ],
  attrsList: [
    { name: ':msg', value: "msg | reverse" }
  ],
  attrsMap: {
    ':msg': 'msg | reverse'
  },
  rawAttsMap: {
    ':msg': { name: ':msg', value: "msg | reverse" }
  }
}

最后,当调用generate进行代码生成render函数后,其结果如下:

const code = generate(ast, options)
// code 打印结果
{
  render: "with(this){return _c('div',{attrs:{"msg":_f("reverse")(msg)}})}",
  staticRenderFns: []
}

小结

在这一小节,我们首先回顾了过滤器的两种注册方式:全局注册局部注册,以及常见的使用场景:插值表达式过滤器v-bind表达式过滤器

随后,我们详细分析了插值表达式过滤器的解析过程,并对parseFilters方法进行了深入的分析。

最后,我们还对v-bind表达式过滤器的解析流程进行了分析,它的前半部分解析过程就是指令的解析过程,只是在最后多了一步对value调用parseFilters的过程。

最后更新时间:
贡献者: wangtunan