Vue filter源码详解
1.解析表达式
以reportDate | DFormat('YYYY-MM-DD') | SDefault
为例。
parseFilters
解析函数位于node_modules/vue/src/compiler/parser/filter-parser.js
。主要作用是将表达式转换为可立即执行的函数字符串。
//匹配任意非空字符、)、.、+、-、_、$、]
//排除一些例如a++,a--,../path,(a+b)之类的影响
const validDivisionCharRE = /[\w).+\-_$\]]/
export function parseFilters(exp: string): string {
let inSingle = false //当前字符是否在单引号中
let inDouble = false //当前字符是否在双引号中
let inTemplateString = false //当前字符是否在es6中的`模板标识符中
let inRegex = false //当前字符是否在正在表达式/中
let curly = 0 //匹配到{时+1,匹配到}时-1
let square = 0 //匹配到[时+1,匹配到]时-1
let paren = 0 //匹配到(时+1,匹配到)时-1
let lastFilterIndex = 0 //最后一个过滤器的起始坐标
//c当前字符、prev上次遍历的字符、i遍历下标、expression表达式、filters过滤器
let c, prev, i, expression, filters
for (i = 0; i < exp.length; i++) {
//新一轮遍历时,修改上一轮循环的字符为遍历完成的最后一个字符
prev = c
//返回指定下标的Unicode编码
let a = exp.charAt(i)
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) {
//当前字符不是`,并且上一个字符不是\,则`es6模板部分结束
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
//第一次解析filter,记录最后一个filter的起始位置为|后的一位
lastFilterIndex = i + 1
//提取|前的部分作为表达式,即提取过滤器的第一个参数
expression = exp.slice(0, i).trim()
} else {
//将解析到的filter放入预置的filters数组中
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
//找到第一个不为空的字符p
for (; j >= 0; j--) {
p = exp.charAt(j)
if (p !== ' ') break
}
//如果p存在且通过了特殊字符校验,那么则认为之后的/之后的内容为正则表达式内容
if (!p || !validDivisionCharRE.test(p)) {
inRegex = true
}
}
}
}
//遍历完成后,表达式即为过滤器第一个参数,并且获得了最后一个过滤器的起始坐标lastFilterIndex
if (expression === undefined) {
expression = exp.slice(0, i).trim()
} else if (lastFilterIndex !== 0) {
//如果存在过滤器则进入这一步骤,此时除了最后一个过滤器,
//其他过滤器均被解析放入filters中了,所以将最后一个过滤器也放入
pushFilter()
}
function pushFilter() {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
if (filters) {
//遍历已经解析出来的filters数组,依次处理每个filter
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('(')
//不带参数的filter
if (i < 0) {
// _f: resolveFilter
return `_f("${filter}")(${exp})`
} else {
//带参数的filter
const name = filter.slice(0, i)
const args = filter.slice(i + 1)
//将第一个表达式作为过滤器的第一个参数,其他参数依次拼接
return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
}
}
解析后的结果为:
_f("defaultStr")(_f("DFormat")(reportDate,'YYYY-MM-DD'))
_f是一个注册函数,用于注册我们书写的过滤器方法。
2.注册过滤器(resolveFilter)
2.1Vue初始化
Vue初始化后会调用initGlobalAPI
添加静态方法,位于node_modules/vue/src/core/global-api/index.js
。
/* @flow */
import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {//在这里添加filter、component、directive中的方法
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)//初始化各组件方法
}
export const ASSET_TYPES = ['component', 'directive', 'filter']
这里初始化为Vue注册了config对象和部分插件方法。
initAssetRegisters
位于node_modules/vue/src/core/global-api/assets.js
。
/* @flow */
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
当type为filter是即注册过滤器方法,id即为过滤器方法名,definition为过滤器方法。
注册过后即可使用如下方式定义全局filter:
Vue.filter('format', function(name) {
return Hello, ${name} !
})
需注意,filter方法并不是在实例化的时候才创建的,而是实例化之前即创建好了,所以filter内的this无法获取到vue实例。
2.2初始化绑定_f
installRenderHelpers
位于node_modules/vue/src/core/instance/render-helpers/index.js
。
/* @flow */
import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util'
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
import { renderList } from './render-list'
import { renderSlot } from './render-slot'
import { resolveFilter } from './resolve-filter'
import { checkKeyCodes } from './check-keycodes'
import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-scoped-slots'
import { bindDynamicKeys, prependModifier } from './bind-dynamic-keys'
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter //过滤器注册函数
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
_f是获取具体过滤器的函数,其绑定在Vue原型上。
2.3调用具体的filter
resolveFilter
位于node_modules/vue/src/core/instance/render-helpers/resolve-filter.js
。
//原值返回函数
export const identity = (_: any) = _
//注册filter
function resolveFilter(id) {
return resolveAsset(
//这里的this是vm,resolveFilter是注册在Vue原型上的方法
this.$options, 'filters', id, true
) || identity
}
resolveAsset
位于node_modules/vue/src/core/util/options.js
。
//执行具体的指令,过滤器的type为filters,返回一个函数
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
}