Skip to content
On this page

1. 前言

在上篇文章中我们说了,无论用户是以什么方式使用过滤器,终归是将解析器写在模板中,既然是在模板中,那它肯定就会被解析编译,通过解析用户所写的模板,进而解析出用户所写的过滤器message | filterA | filterB中哪部分是被处理的表达式,哪部分是过滤器id及其参数。

还记得我们在介绍模板编译篇的解析阶段中说过,用户所写的模板会被三个解析器所解析,分别是HTML解析器parseHTML、文本解析器parseText和过滤器解析器parseFilters。其中HTML解析器是主线,在使用HTML解析器parseHTML函数解析模板中HTML标签的过程中,如果遇到文本信息,就会调用文本解析器parseText函数进行文本解析;如果遇到文本中包含过滤器,就会调用过滤器解析器parseFilters函数进行解析。在之前的文章中,我们只对HTML解析器parseHTML和文本解析器parseText其内部原理进行了分析,没有分析过滤器解析器parseFilters,那么本篇文章就来分析过滤器解析器的内部原理。

2. 在何处解析过滤器

我们一再强调,过滤器有两种使用方式,分别是在双花括号插值中和在 v-bind 表达式中,如下:

javascript
<!-- 在双花括号中 -->
{{ message | capitalize }}

<!--`v-bind`-->
<div v-bind:id="rawId | formatId"></div>

两种不同的使用方式唯一的区别是将过滤器写在不同的地方,既然有两种不同的地方可以书写过滤器,那解析的时候必然要在这两种不同地方都进行解析。

  • 写在 v-bind 表达式中

    v-bind 表达式中的过滤器它属于存在于标签属性中,那么写在该处的过滤器就需要在处理标签属性时进行解析。我们知道,在HTML解析器parseHTML函数中负责处理标签属性的函数是processAttrs,所以会在processAttrs函数中调用过滤器解析器parseFilters函数对写在该处的过滤器进行解析,如下:

    javascript
    function processAttrs (el) {
        // 省略无关代码...
        if (bindRE.test(name)) { // v-bind
            // 省略无关代码...
            value = parseFilters(value)
            // 省略无关代码...
        }
        // 省略无关代码...
    }
  • 写在双花括号中

    在双花括号中的过滤器它属于存在于标签文本中,那么写在该处的过滤器就需要在处理标签文本时进行解析。我们知道,在HTML解析器parseHTML函数中,当遇到文本信息时会调用parseHTML函数的chars钩子函数,在chars钩子函数内部又会调用文本解析器parseText函数对文本进行解析,而写在该处的过滤器它就是存在于文本中,所以会在文本解析器parseText函数中调用过滤器解析器parseFilters函数对写在该处的过滤器进行解析,如下:

    javascript
    export function parseText (text,delimiters){
        // 省略无关代码...
        const exp = parseFilters(match[1].trim())
        // 省略无关代码...
    }

现在我们已经知道了过滤器会在何处进行解析,那么接下来我们就来分析过滤器解析器parseFilters函数,来看看其内部是如何对过滤器进行解析的。

3. parseFilters函数分析

parseFilters函数的定义位于源码的src/complier/parser/filter-parser.js文件中,其代码如下:

javascript
export function parseFilters (exp) {
  let inSingle = false                     // exp是否在 '' 中
  let inDouble = false                     // exp是否在 "" 中
  let inTemplateString = false             // exp是否在 `` 中
  let inRegex = false                      // exp是否在 \\ 中
  let curly = 0                            // 在exp中发现一个 { 则curly加1,发现一个 } 则curly减1,直到culy为0 说明 { ... }闭合
  let square = 0                           // 在exp中发现一个 [ 则curly加1,发现一个 ] 则curly减1,直到culy为0 说明 [ ... ]闭合
  let paren = 0                            // 在exp中发现一个 ( 则curly加1,发现一个 ) 则curly减1,直到culy为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
}

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}`
  }
}

该函数的作用的是将传入的形如'message | capitalize'这样的过滤器字符串转化成_f("capitalize")(message),接下来我们就来分析一下其内部逻辑。

在该函数内部,首先定义了一些变量,如下:

javascript
let inSingle = false
let inDouble = false
let inTemplateString = false
let inRegex = false
let curly = 0
let square = 0
let paren = 0
let lastFilterIndex = 0
  • inSingle:标志exp是否在 ' ... ' 中;
  • inDouble:标志exp是否在 " ... " 中;
  • inTemplateString:标志exp是否在 ` ... ` 中;
  • inRegex:标志exp是否在 \ ... \ 中;
  • curly = 0 : 在exp中发现一个 { 则curly加1,发现一个 } 则curly减1,直到culy为0 说明 { ... }闭合;
  • square = 0:在exp中发现一个 [ 则curly加1,发现一个 ] 则curly减1,直到culy为0 说明 [ ... ]闭合;
  • paren = 0:在exp中发现一个 ( 则curly加1,发现一个 ) 则curly减1,直到culy为0 说明 ( ... )闭合;
  • lastFilterIndex = 0:解析游标,每循环过一个字符串游标加1;

接着,从头开始遍历传入的exp每一个字符,通过判断每一个字符是否是特殊字符(如',",{,},[,],(,),\,|)进而判断出exp字符串中哪些部分是表达式,哪些部分是过滤器id,如下:

javascript
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
}

可以看到,虽然代码稍微有些长,但是其逻辑非常简单。为了便于阅读,我们提供一个上述代码中所涉及到的ASCII码与字符的对应关系,如下:

0x22 ----- "
0x27 ----- '
0x28 ----- (
0x29 ----- )
0x2f ----- /
0x5C ----- \
0x5B ----- [
0x5D ----- ]
0x60 ----- `
0x7C ----- |
0x7B ----- {
0x7D ----- }

上述代码的逻辑就是将字符串exp的每一个字符都从前往后开始一个一个匹配,匹配出那些特殊字符,如',",`,{,},[,],(,),\,|

如果匹配到',",`字符,说明当前字符在字符串中,那么直到匹配到下一个同样的字符才结束,同时, 匹配 (), {}, [] 这些需要两边相等闭合, 那么匹配到的 | 才被认为是过滤器中的|

当匹配到过滤器中的|符时,那么|符前面的字符串就认为是待处理的表达式,将其存储在 expression 中,后面继续匹配,如果再次匹配到过滤器中的 |符 ,并且此时 expression有值, 那么说明后面还有第二个过滤器,那么此时两个|符之间的字符串就是第一个过滤器的id,此时调用 pushFilter函数将第一个过滤器添加进filters数组中。举个例子:

假如有如下过滤器字符串:

javascript
message | filter1 | filter2(arg)

那么它的匹配过程如下图所示:

将上例中的过滤器字符串都匹配完毕后,会得到如下结果:

javascript
expression = message
filters = ['filter1','filter2(arg)']

接下来遍历得到的filters数组,并将数组的每一个元素及expression传给wrapFilter函数,用来生成最终的_f函数调用字符串,如下:

javascript
if (filters) {
    for (i = 0; i < filters.length; i++) {
        expression = wrapFilter(expression, filters[i])
    }
}

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 : args}`
  }
}

可以看到, 在wrapFilter函数中,首先在解析得到的每个过滤器中查找是否有(,以此来判断过滤器中是否接收了参数,如果没有(,表示该过滤器没有接收参数,则直接构造_f函数调用字符串即_f("filter1")(message)并返回赋给expression,如下:

javascript
const i = filter.indexOf('(')
if (i < 0) {
    return `_f("${filter}")(${exp})`
}

接着,将新的experssionfilters数组中下一个过滤器再调用wrapFilter函数,如果下一个过滤器有参数,那么先取出过滤器id,再取出其带有的参数,生成第二个过滤器的_f函数调用字符串,即_f("filter2")(_f("filter1")(message),arg),如下:

javascript
const name = filter.slice(0, i)
const args = filter.slice(i + 1)
return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`

这样就最终生成了用户所写的过滤器的_f函数调用字符串。

4. 小结

本篇文章介绍了Vue是如何解析用户所写的过滤器的。

首先,我们介绍了两种不同写法的过滤器会在不同的地方进行解析,但是解析原理都是相同的,都是调用过滤器解析器parseFilters函数进行解析。

接着,我们分析了parseFilters函数的内部逻辑。该函数接收一个形如'message | capitalize'这样的过滤器字符串作为,最终将其转化成_f("capitalize")(message)输出。在parseFilters函数的内部是通过遍历传入的过滤器字符串每一个字符,根据每一个字符是否是一些特殊的字符从而作出不同的处理,最终,从传入的过滤器字符串中解析出待处理的表达式expression和所有的过滤器filters数组。

最后,将解析得到的expressionfilters数组通过调用wrapFilter函数将其构造成_f函数调用字符串。