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。
接着,将新的experssion
与filters
数组中下一个过滤器再调用wrapFilter
函数,如果下一个过滤器有参数,那么先取出过滤器id
,再取出其带有的参数,生成第二个过滤器的_f
函数调用字符串,即_f("filter2")(_f("filter1")(message),arg)。
这样就最终生成了用户所写的过滤器的_f
函数调用字符串。
vue内部实现过滤器的过程大致是这样的。