我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
过滤器的官方文档可以点击这里。
今天和大家说说过滤器的实现原理,主要看过滤器模板字符串的编译和过滤器模板字符串编译出来的代码字符串。
我们以下面的代码为例:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
Vue.filter('prefix', function (value) {
return `~${value}`
})
Vue.filter('suffix', function (value, word) {
return `${value}${word}`
})
let app = new Vue({
el: '#app',
data() {
return {
message: 'hello vue'
}
},
methods: {},
template: `
<div id="app">
<h3>{{message | capitalize}}</h3>
<h3>{{message | suffix('!')}}</h3>
<h3>{{message | capitalize | prefix}}</h3>
</div>
`
})
在例子中, 注册了三个全局过滤器,分别是:capitalize、prefix、suffix。
- capitalize 过滤器的作用是:将字符串的首字母大写;
- prefix 过滤器的作用是:给字符串的前面拼接上 "~" 字符;
- suffix 过滤器的作用是:将传递的字符串参数拼接到目标字符串的后面;
页面的运行效果如下所示:
1,过滤器运行原理
这一小节主要看过滤器模板字符串所编译出来的代码字符串,看看在运行时,过滤器是如何实现功能的。
1-1,没有传递参数的单个过滤器
以第一个 h3 节点为例,模板字符串是
<h3>{{message | capitalize}}</h3>
最终被编译成的 render 代码字符串是
_c('h3',[_v(_s(_f("capitalize")(message)))])
这里可以发现 {{message | capitalize}} 被翻译成了 _f("capitalize")(message),这里的 _f 是 resolveFilter 方法的别名,他的作用是根据过滤器的名称获取对应的过滤器函数。所以这里的 _f("capitalize")(message) 可以看成 capitalize(message),也就是以 message 作为参数执行 capitalize 函数,并将过滤器函数执行的返回值作为 _s 函数的参数,最终的效果就是创建出来的 h3 vnode 节点的子节点是一个文本 vnode 节点,并且该文本 vnode 节点的 text 属性是 "Hello vue",可以发现 "Hello vue" 正是 "hello vue" 经过 capitalize 过滤器函数处理后的结果,这也就实现了过滤器想要的效果。
1-2,传递参数的过滤器
以第二个 h3 节点为例,模板字符串是
<h3>{{message | suffix('!')}}</h3>
最终被编译成的 render 代码字符串是
_c('h3',[_v(_s(_f("suffix")(message,'!')))])
可以发现,这里所编译出的代码字符串和 1-1 小节差不多,唯一的不同是过滤器使用了参数,并且在编译出的代码字符串中,这个使用的参数作为 suffix 过滤器函数的第二个参数,message 作为 suffix 过滤器函数的第一个参数,关于这一点,官方文档也有专门的说明,如下图所示。
1-3,多个过滤器串联
以第三个 h3 节点为例,模板字符串是
<h3>{{message | capitalize | prefix}}</h3>
最终被编译成的 render 代码字符串是
_c('h3',[_v(_s(_f("prefix")(_f("capitalize")(message))))])
通过观察可以发现,多个过滤器串联的实现原理是编译出嵌套的过滤器函数。在模板字符串中,message 变量首先经过 capitalize 过滤器处理,然后再经过 prefix 过滤器处理,因此在编译出的代码字符串中,首先以 message 作为参数执行 capitalize 过滤器函数,然后以 capitalize 过滤器函数的执行结果作为参数执行 prefix 过滤器函数,最后将 prefix 过滤器函数执行的结果作为 _s 方法的参数。
1-4,_f、resolveFilter 方法的运行原理
在上面三小节中,_f 函数非常的重要,它的作用是根据过滤器的名称获取对应的过滤器函数,_f 是 resolveFilter 方法的别名,定义如下:
// _f 函数的定义
import { resolveFilter } from './resolve-filter'
export function installRenderHelpers (target: any) {
target._f = resolveFilter
}
所以我们主要看 resolveFilter 方法的实现,代码如下:
export function resolveFilter (id: string): Function {
return resolveAsset(this.$options, 'filters', id, true) || identity
}
resolveFilter 方法的内部借助 resolveAsset 方法实现功能,并且传递的参数有 this.$options、'filters'、过滤器名称,这个 this.$options 变量的作用是存储当前 Vue 实例能够使用的资源,包括指令、过滤器和组件,关于这部分知识可以看我的这篇文章。接下来看 resolveAsset 方法的实现。
resolveAsset 方法很简单,只要从 this.$options 变量中获取指定的资源,并返回出去即可,源码如下所示,看注释即可。
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
// 如果 id 不是字符串类型的话,直接 return
if (typeof id !== 'string') {
return
}
// options 的数据结构如下所示:
// options:{
// components:{},
// directives:{},
// filters:{}
// }
const assets = options[type]
// 判断 assets 对象本身有没有指定的 原始id、小驼峰id、大驼峰id
// 如果有的话,直接返回
// 判断 assets 对象本身是否存在原始id属性,如果有的话,直接返回
if (hasOwn(assets, id)) return assets[id]
// 判断 assets 对象本身是否存在小驼峰id属性,如果有的话,直接返回
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
// // 判断 assets 对象本身是否存在大驼峰id属性,如果有的话,直接返回
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// 如果 assets 对象本身 原始id、小驼峰id、大驼峰id 属性都不存在的话
// 则判断 assets 的原型链上有没有指定的 原始id、小驼峰id、大驼峰id 属性
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
}
2,过滤器模板字符串的编译
这一小节研究过滤器模板字符串是如何编译成对应的代码字符串的。
对应的源码在 src/compiler/parser/filter-parser.js 文件中,因为在真实的源码中有很多代码是处理边界情况的,不便于理解,所以我进行了主要代码的精简,方法大家理解主体逻辑,源码如下所示:
主线逻辑就是遍历 filters 字符串数组,调用 wrapFilter 方法完成代码字符串的拼接。
function parseFilters(exp) {
// 以 exp == "message | suffix('!')" 为例进行分析
// 以 '|' 为基准切分 exp 字符串
// filters == ["message ", " suffix('!')"]
let filters = exp.split('|')
// 弹出 filters 字符串数组的第一个元素赋值给 expression 变量,并且删除前后的空格
// 这弹出的第一个元素就是过滤器想要处理的数据,数组中剩下的元素都是过滤器字符串和参数
let expression = filters.shift().trim()
// 第一个元素弹出后,filters == [" suffix('!')"]
let i
// 接下来进行过滤器代码字符串的拼接,调用 wrapFilter 实现功能
if (filters) {
// 遍历 filters 数组
for (i = 0; i < filters.length; i++) {
// 调用 wrapFilter 方法进行代码字符串的拼接
// 第一个参数是当前处理过滤器的参数
// 第二个参数是当前处理过滤器的函数名称
//
// wrapFilter 方法的返回值直接赋值给 expression 变量,此时的
// expression 字符串变成了下一轮过滤器的参数
expression = wrapFilter(expression, filters[i].trim())
}
}
// 返回最终拼接好的代码字符串
return expression
}
function wrapFilter(exp, filter) {
// 判断当前处理的过滤器字符串有没有 '(' 字符
// 如果有的话,说明当前的过滤器有参数,例如:"suffix('!')"
// 如果没有的话,则当前的过滤器没有参数
const i = filter.indexOf('(')
if (i < 0) {
// 如果 i < 0 的话,说明过滤器字符串中没有 '(' 字符,
// 也就是说当前的过滤器没有使用参数
// 此时直接 return `_f("${filter}")(${exp})`
return `_f("${filter}")(${exp})`
} else {
// 如果 i 不是小于 0,则说明当前的过滤器使用了参数,此时就需要对参数进行处理。
// 原因是因为在过滤器函数的调用中,过滤器函数要处理的目标数据(例如:message)需要作为第一个参数。
//
// 以 i 下标为分割点,分割 filter 字符串,例如:filter == "suffix('!')",则
// name == "suffix",args == "'!')"
const name = filter.slice(0, i) // 过滤器函数名称
const args = filter.slice(i + 1) // 使用过滤器时,传入的参数
// 接下来进行代码字符串的拼接。注意,这里 exp 作为过滤器函数调用的第一个参数
return `_f("${name}")(${exp},${args}`
}
}
// 测试
let filterRender1 = parseFilters(`message | suffix('!')`)
let filterRender2 = parseFilters(`message | capitalize | suffix('!')`)
console.log(filterRender1) // _f("suffix")(message,'!')
console.log(filterRender2) // _f("suffix")(_f("capitalize")(message),'!')