vue源码解析:vue过滤器的点点滴滴

vue的过滤器我们并不陌生,是vue开发中最为常见的工具之一。它经常会被用来格式化模板中的文本。过滤器可以单个使用,也可以多个串联一起使用,还可以传参数使用。现在我们来谈谈vue过滤器的实现原理。

用法:

在双花括号插值中和在 v-bind 表达式中 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“|”符号指示。

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

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

定义:

//    组件中创建过滤器
filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}
//    全局API创建过滤器
Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

串联过滤器:

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。

{{ message | filterA | filterB }}

message作为filterA参数,filterA作为filterB的参数。

{{ message | filterA('arg1', arg2) }}

filterA接收了三个参数,message将是第一个参数。

我们知道过滤器有花括号和v-bind俩种过滤器。但是无论是哪一种使用方式,过滤器都是写在模板里面的。既然是写在模板里面,那么它就会被编译,会被编译成渲染函数字符串,然后在挂载的时候会执行渲染函数,从而就会使过滤器生效。所有的过滤器都会被编译成一个渲染函数_f(),也就是对应的resolveFilter()函数。接下来我们就介绍一下resolveFilter()函数。

src/core/instance/render-helpers.js

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

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

可以看到,resolveFilter函数内部只有一行代码,就是调用resolveAsset函数并获取其返回值,如果返回值不存在,则返回identity,而identity是一个返回同参数一样的值。

/**
 * Return same value
 */
export const identity = _ => _

resolveAsset函数是什么呢?源码的src/core/util/options.js中。

export function resolveAsset (options,type,id,warnMissing) {
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // 先从本地注册中查找
  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]
  // 再从原型链中查找
  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函数接收参数:

  • options:当前实例的$options
  • type: filter
  • id: 过滤器id

函数内部首先会判断是否存在过滤器id是不是字符串,如果不是,直接return,获取到当前实例的$options属性中所有的过滤器,赋给变量assets。我们知道定义过滤器有两种方式,一种是定义在组件的选项中,一种是使用Vue.filter定义。之前我们也说过,组件中的所有选项都会被合并到当前实例的$options属性中,并且使用Vue.filter定义的过滤器也会被添加到$options中的filters属性中,所以不管是以何种方式定义的过滤器,我们都可以从$options中的filters属性中获取到。获取到所有的过滤器后,接下来我们只需根据过滤器id取出对应的过滤器函数即可。根据过滤器id查找过滤器首先先从本地注册中查找,先通过hasOwn函数检查assets自身中是否存在,如果存在则直接返回;如果不存在,则将过滤器id转化成驼峰式后再次查找,如果存在则直接返回;如果也不存在,则将过滤器id转化成首字母大写后再次查找,如果存在则直接返回;如果还不存在,则再从原型链中查找,如果存在则直接返回;如果还不存在,则在非生产环境下抛出警告。resolveFilter函数其实就是在根据过滤器id获取到用户定义的对应的过滤器函数并返回,拿到用户定义的过滤器函数之后,就可以调用该函数并传入参数使其生效了。

串联过滤器:

串联过滤器还是先根据过滤器id获取到对应的过滤器函数,然后传入参数调用即可,唯一有所区别的是:对于多个串联过滤器,在调用过滤器函数传递参数时,后一个过滤器的输入参数是前一个过滤器的输出结果。

过滤器参数:

过滤器的第一个参数永远是表达式的值,或者是前一个过滤器处理后的结果,后续其余的参数可以被用于过滤器内部的过滤规则中。

解析过滤器:

创建了过滤器,还是要写在html模版中使用,那么html模版是如何解析的呢?那就是三大解析器之一的parseFilters。下面我们来分析一下此函数是何许人也。

我们知道过滤器有俩种使用方式:

  • 写在 v-bind 表达式中:

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

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

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

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

parseFilters函数分析:src/complier/parser/filter-parser.js

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)。

首先内部定义了一些变量:

 

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

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

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

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

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

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

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

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

vue内部实现过滤器的过程大致是这样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值