vue2源码解析之过滤器
基本使用
过滤器有三种用法,可以单独使用,可以串联使用还可以传递额外的参数;
/*
单独使用
*/
{{ message | capitalize }}
// 串联使用
/*
在这个例子中,把message作为参数传递给filterA函数执行,把
filterA函数执行的结果作为参数传递给filterB执行。
*/
{{ message | filterA | filterB }}
// 传递参数
/*
`capitalize` 被定义为接收三个参数的过滤器函数。其中 `message`
的值作为第一个参数,普通字符串 `'arg1'` 作为第二个参数,表
达式 `arg2` 的值作为第三个参数
*/
{{ message | capitalize('arg1', 'arg2') }}
源码解析
// 源码位置 src/core/render.js
import { installRenderHelpers } from './render-helpers/index'
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)
}
// 源码位置 src/core/render-helpers/index
import { resolveFilter } from './resolve-filter'
export function installRenderHelpers (target: any) {
target._f = resolveFilter
}
// 源码位置 src/core/render-helpers/resolve-filter
import { identity, resolveAsset } from 'core/util/index'
export function resolveFilter (id: string): Function {
return resolveAsset(this.$options, 'filters', id, true) || identity
}
从上面可以看出,过滤器的处理函数被一层层包裹,最总的处理函数为resolveAsset; resolveAsset源码预览
// 源码位置 src/core/util/options.js
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
}
resolveAsset接收四个参数,分别是当前实例的options,type为filters,id为当前过滤器名称,warnMissing为true; 首先判断传递的过滤器名称是否是字符串,不是直接返回
if (typeof id !== 'string') {
return
}
从options上查找到filters属性,此属性下的值为当前实例的所有的过滤器集合
// 找出当前实例上的所有的过滤器
const assets = options[type]
接着从assets这个集合里面的自身属性进行查找,分别通过传递的过滤器名称,驼峰形式的过滤器名称,首字母大写的过滤器名称查找,找到就返回,
// 判断此过滤器是不是自有过滤器,是就直接返回
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]
return res
最终编译成渲染函数之后的字符串
// 单独使用
_f('过滤器名')(参数)
// 串联使用
_f('过滤器1')(_f('过滤器2')(参数))
解析过滤器
过滤器的使用方式有两种,分别是在大括号中使用和在v-bind中使用;
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
那么vue在解析字符串模板的时候就需要分别针对这两种形式进行解析;
- v-bind:
v-bind属于标签属性,因此,在解析字符串模板的函数parseHTML内部,如果遇到了标签属性就通过parseAttrs函数进行解析,在parseAttrs函数内部如果遇到了过滤器就通过parseFilters函数进行解析(模板解析在模板编译阶段讲解过);
function processAttrs (el) {
...
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '')
// 解析value返回一个_f函数执行的表达式
value = parseFilters(value)
...
}
...
}
- {{ }}中
{{}}属于文本,因此,在解析字符串模板的函数parseHTML内部,如果遇到文本就调用parseText函数进行解析,在parseText函数内部如果遇到过滤器就调用parseFilters函数进行解析;
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
...
const exp = parseFilters(match[1].trim())
...
}
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
}
console.log('filters', filters, exp, lastFilterIndex)
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}`
}
}
首先定义了一些变量
// exp是否在 ' ' 中
let inSingle = false
// exp是否在 " " 中
let inDouble = false
// exp是否在 ` ` 中
let inTemplateString = false
// exp是否在 \ \ 中
let inRegex = false
// 发现一个 { 则curly加1,发现一个 } 则curly减1,直到culy为0 说明 { ... }闭合
let curly = 0
// 发现一个 [ 则curly加1,发现一个 ] 则curly减1,直到culy为0 说明 [ ... ]闭合
let square = 0
// 发现一个 ( 则curly加1,发现一个 ) 则curly减1,直到culy为0 说明 ( ... )闭合
let paren = 0
// 解析游标,每循环过一个字符串游标加1
let lastFilterIndex = 0
// c: 当前字符 prev:上个字符 i:当前索引 expression当前表达式 filters:存储过滤器数组
let c, prev, i, expression, filters
遍历字符串,从前往后开始一个一个匹配,匹配出那些特殊字符,如'
,"
,`,{
,}
,[
,]
,(
,)
,|
,如果遇到'
,"
,`,就继续找到下个同样的结束字符,并且其中的(),[],{}都是闭合的,那么匹配到的|才是过滤器;如果表达式不存在就进行截取获取到表达式,如果存在就为过滤器,进行添加到数组中
/*
0x22 ----- "
0x27 ----- '
0x28 ----- (
0x29 ----- )
0x2f ----- /
0x5C ----- \
0x5B ----- [
0x5D ----- ]
0x60 ----- `
0x7C ----- |
0x7B ----- {
0x7D ----- }
*/
// 遍历字符串
for (i = 0; i < exp.length; i++) {
// 存储上个字符
prev = c
// 获取到当前字符的ASCII码
c = exp.charCodeAt(i)
// 如果是单引号存在
if (inSingle) {
// 如果当前字符为单引号并且上个字符不是\,则设置inSingle = false
if (c === 0x27 && prev !== 0x5C) inSingle = false
// 如果是双引号存在
} else if (inDouble) {
// 如果当前为双引号并且上个字符不是\,则设置inDouble = false
if (c === 0x22 && prev !== 0x5C) inDouble = false
// 如果模板字符串存在
} else if (inTemplateString) {
// 如果当前字符为`并且上个字符不为\,则设置inTemplateString = false
if (c === 0x60 && prev !== 0x5C) inTemplateString = false
// 如果正则存在
} else if (inRegex) {
// 如果当前字符为/并且上个字符不为\ 则设置inRegex = false
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
) {
// 如果表达式为undefined
if (expression === undefined) {
// first filter, end of expression
// 游标移动一个
lastFilterIndex = i + 1
// 截取表达式
expression = exp.slice(0, i).trim()
} else { // 否则存储过滤器
pushFilter()
}
} else {
switch (c) {
// 如果是双引号就inDouble为true
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
}
}
}
}
function pushFilter () {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
比如遇到{{ message | filter1 | filter2(66)}}
,那么经过上面的处理之后为expression=message,filters=['filter1','filter2(66)']
;
接着遍历过滤器数组,把返回的结果重新赋值给expression;这样做的目的就是可以实现串联调用的方式;
if (filters) {
// 遍历过滤器,把每个过滤器设置为_f字符串函数
for (i = 0; i < filters.length; i++) {
expression = wrapFilter(expression, filters[i])
}
}
wrapFilter函数,分别对有参数和无参数的过滤器进行了处理
function wrapFilter (exp: string, filter: string): string {
// 如果包含括号表示有传递参数
const i = filter.indexOf('(')
// 没有括号表示没有参数
if (i < 0) {
// 直接通过_f()()进行包裹
return `_f("${filter}")(${exp})`
} else { // 找出过滤器名和参数
const name = filter.slice(0, i)
const args = filter.slice(i + 1)
// _f()(参数,参数)形式包裹
return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
}
}
最终的形式
_f(filter2)(_f(filter1)(message),66)
转载自:https://juejin.cn/post/7236915296963117112