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
}