The filter filter
If you think it is good, please send me a Star at GitHub
Before analyzing the implementation principle of filters, we need to review the usage of filters, including: registration mode, use mode. The purpose of doing this is to facilitate us to better understand the implementation principle of filters, but also to provide a way of thinking for us to analyze filters.
Registration and use of filters
Registration of filters
As with directives, filters can be registered in two ways: global and local.
Global registration filters can be implemented using vue.filter (), for example:
Vue.filter('reverse'.function (value) {
return value.split(' ').reverse().join(' ')})Copy the code
Options [‘filters’] property for globally registered filters.
Local registration filters that need to be written in the filters option of the component, for example:
export default {
name: 'App'.filters: {
reverse (value) {
return value.split(' ').reverse().join(' ')}}}Copy the code
$options[‘filters’] property of the component.
Use of filters
There are two scenarios for using filters: interpolation and V-bind.
<! -- v-bind -->
<template>
<div :msg="msg | reverse"></div>
</template>
<! -- Double parenthesis interpolation -->
<template>
<div>{{msg | reverse}}</div>
</template>
Copy the code
When using a filter, you can also pass parameters to the filter:
<template> <div>{{msg | reverse('default msg')}}</div> </template> <script> export default { name: 'App', data () { return { msg: '' } }, filters: { reverse (value, defaultValue) { if (! value) { return defaultValue } return value.split('').reverse().join('') } } } </script>Copy the code
If multiple filters exist, they can also be used in series:
<template>
<div>{{msg | filterA | filterB}}</div>
</template>
Copy the code
Cascaded filters are executed from left to right. In the example above, the result of the filterA filter is passed to the first parameter value of the filterB filter.
Parsing of interpolation filters
We use the following code to analyze interpolation filter parsing:
new Vue({
el: '#app',
data () {
return {
msg: 'ABCD'}},filters: {
reverse (value) {
return value.split(' ').reverse().join(' ')}},template: '<div>{{msg | reverse}}</div>'
})
Copy the code
We all know that filters are used to process text, so in the filter parsing section, we’ll review how the parse stage processes text.
const template = '<div>{{msg | reverse}}</div>'
const ast = parse(template.trim(), options)
Copy the code
When parse executes, the parseHTML method is called. In the method’s while loop, parseStartTag is first called, which matches the start tag of the div. The template template is then intercepted once with the following values:
const template = '{{msg | reverse}}</div>'
Copy the code
During the second while loop, the text content of the div closing tag is captured as follows:
const test = '{{msg | reverse}}'
Copy the code
After intercepting the text, the chars hook function is fired, where the text content is processed through parseText:
chars (text, start, end) {
if(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
child = {
type: 2.expression: res.expression,
tokens: res.tokens,
text
}
}
}
Copy the code
Let’s look at the code for the parseText method:
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g
export function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if(! tagRE.test(text)) {return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp}) `)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
Copy the code
Although the parseText method is a bit long, we focus on only two core points: tagRE interpolation regular expressions and the parseFilters method, so the parseText method looks like this:
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g
export function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
/ /... Omit code
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
/ /... Omit code
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp}) `)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
/ /... Omit code
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
Copy the code
In the condition of the while loop, it performs a tagRE match and assigns the result of the match to match, which looks like this:
const match = [
0: '{{msg | reverse}}'.1: "msg | reverse".index: 0.input: "{{msg | reverse}}"
]
Copy the code
Since the match result is an array, inside the while it calls the parseFilters method to parse the filter. Let’s look at the code for the parseFilters method:
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
}
if (filters) {
for (i = 0; i < filters.length; i++) {
expression = wrapFilter(expression, filters[i])
}
}
return expression
}
Copy the code
You might be surprised by the long code in the parseFilters method, especially in the for loop, but what the for loop does isn’t that complicated.
Let’s skip the for loop and look at what happens when the for loop ends:
let filters = ['reverse']
let expression = 'msg'
Copy the code
After the for loop completes, we get an array of filters and an expression. Then at the end of the parseFilters method, it iterates through the array and calls wrapFilter to process the 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}`}}Copy the code
The wrapFilter method is very simple. In our example, the value of expression is as follows:
const expression = '_f("reverse")(msg)'
Copy the code
So, let’s analyze the results of the wrapFilter method in conjunction with other filter use cases.
// series filter
let filters = ['filterA'.'filterB']
let expression = 'msg'
const result = '_f("filterB")(_f("filterA")(msg))'
// Parameter filter
let filters = ['reverse("default msg")']
let expression = 'msg'
const result = '_f("reverse")(msg,"default msg")'
Copy the code
After we understand the wrapFilter method, we may have the following two questions:
_f
What is the function?parseFilters
methodsfor
How exactly is the loop resolved correctlyfilters
Array andexpression
Of the expression?
_f function
The _f function is a short form for resolveFilter, which was introduced in the codeGen code generation section.
export function installRenderHelpers (target: any) {
target._s = toString // Turn to string
target._l = renderList // Process the V-for list
target._t = renderSlot // Process the slot
target._m = renderStatic // Handle static nodes
target._f = resolveFilter // Handle filters
target._v = createTextVNode // Create a text VNode
target._e = createEmptyVNode // Create an empty VNode
}
Copy the code
The resolveFilter method is defined in SRC /core/instance/render-helpers/resolve-filter.js:
import { identity, resolveAsset } from 'core/util/index'
export function resolveFilter (id: string) :Function {
return resolveAsset(this.$options, 'filters', id, true) || identity
}
Copy the code
In resolveFilter, it in turn calls the resolveAsset method. The resolveFilter method is used to get the filter with the specified ID (name) from the build instance.
Remember the array of ASSET_TYPES we used to refer to when we were analyzing the Vue source code?
const ASSET_TYPES = ['component'.'directive'.'filter']
Copy the code
Because Vue handles component, directive, and filter similarly, the resolveAsset method fetches the component, directive, or filter with the specified ID (name) from the $options TAB of the component. The code is not very complicated, as follows:
export function resolveAsset (
options: Object, type: string, id: string, warnMissing? : boolean) :any {
/* istanbul ignore if */
if (typeofid ! = ='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
}
Copy the code
For loop logic parsing
Before we analyze it, let’s look at a few special characters:
// Pair special characters
0x22= = ="0 x27 = = = '0 x28 = = = 0 x5b x29 = = = (0) = = = x5d = = = 0 and 0 x60 = = = ` 0 x7b = = = {0 x7d = = =} / / 0 x7c = = = | filter pipe symbolCopy the code
Suppose we have an exp string for the for loop:
const exp = 'msg | reverse'
Copy the code
For the example above, the for loop loops as follows:
i=0
, the character ism
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=1
, the character iss
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=2
, the character isg
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=3
, the character is a space character, not a pair of special characters, nor a filter pipe symbol, continue the next loop.i=4
, the character is|
, not a pair of special characters, but a filter pipe symbol, after the following codeexpression
To be an assignmentmsg
.lastFilterIndex
for5
:
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 {
/ /... Omit code}}Copy the code
i=5
, the character is a space character, not a pair of special characters, nor a filter pipe symbol, continue the next loop.i=6
, the character isr
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=7
, the character ise
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=8
, the character isv
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=9
, the character ise
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=10
, the character isr
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=11
, the character iss
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=12
, the character ise
, not a pair of special characters, not a filter pipe symbol, continue the next loop.i=13
.for
The loop ends with the following code logic:
if (expression === undefined) {
/ /... Omit code
} else if(lastFilterIndex ! = =0) {
pushFilter()
}
function pushFilter () {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
Copy the code
After the above process is complete, we get the result we want:
let filters = ['reverse']
let expression = 'msg'
Copy the code
Now that you’re done parsing the for loop, you may still be unsatisfied. You may have a few questions:
- What does pairing special characters do?
- How are series filters properly resolved?
What are the functions of special characters? If we encounter pairs of special symbols during the for loop loop, we must wait until these symbols are closed before intercepting expression, for example:
const exp = '(age > 18 ? "yes" : "no") | reverse'
Copy the code
Only when () is closed in pairs can we put (age > 18? “Yes” : “no”) as a whole is assigned to expression.
Two, how is the series filter correctly resolved? The key to this problem is the lastFilterIndex variable, for example:
const exp = 'msg | filterA | filterB'
Copy the code
In the match to | symbol for the first time, lastFilterIndex has a value of 5, the second match to | symbol at this point I index value of 14, then the index 5-14 before the string is the name of the first filter, both: filterA.
The above matching rules can be summed up in one sentence: the string between the two pipe symbols is the name of the filter.
After parseFilters are parsed, let’s look at the result returned by parseText:
const tokens = ['_s(_f("reverse")(msg))']
const rawTokens = [
{ '@binding': "_f("reverse")(msg)"}]Copy the code
By the time parseText returns a result, the parse process is almost complete and we have the following AST object:
const ast = [
{
type: 1.tag: 'div'.children: [{type: 2.text: '{{msg | reverse}}'.expression: '_s(_f("reverse")(msg))'.tokens: [{'@binding': "_f("reverse")(msg)"}]}]Copy the code
Next, we call generate for code generation. Since this process has been analyzed in detail in previous chapters, we will write the result directly here:
const code = generate(ast, options)
// code prints the result
{
render: "with(this){return _c('div',[_v(_s(_f("reverse")(msg)))])}".staticRenderFns: []}Copy the code
V-bind expression filter parsing
We use the following sample code to analyze the parsing of the V-bind expression filter:
new Vue({
el: '#app',
data () {
return {
msg: 'ABCD'}},filters: {
reverse (value) {
return value.split(' ').reverse().join(' ')}},template: '<div :msg="msg | reverse"></div>'
})
Copy the code
Since V-bind is a special instruction, the first part of the parse process follows the instruction pattern. Before triggering the div end tag, the AST results are as follows:
const ast = {
type: 1.tag: 'div'.attrsList: [{name: ':msg'.value: "msg | reverse"}].attrsMap: {
':msg': 'msg | reverse'
},
rawAttsMap: {
':msg': { name: ':msg'.value: "msg | reverse"}}}Copy the code
When the div end hook function is triggered, processElement is called to handle it:
export function processElement (element: ASTElement, options: CompilerOptions) {
/ /... Omit code
processAttrs(element)
return element
}
Copy the code
Next, let’s look at the very familiar processAttrs method:
import { parseFilters } from './filter-parser'
export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/
export const bindRE = /^:|^\.|^v-bind:/
export const onRE = /^@|^v-on:/
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
el.hasBindings = true
// modifiers omit code
if (bindRE.test(name)) {
name = name.replace(bindRE, ' ')
value = parseFilters(value)
// v-bind omits the code
} else if (onRE.test(name)) {
// v-on omits code
} else {
// Normal directives omit the code}}else {
/ /... Omit code}}}Copy the code
Code analysis: We have analyzed the functions of the three regular expressions in the previous instruction section. Because in this section we mainly analyze the parsing process of the V-bind expression filter, so the bindRE regular expression matches successfully. At this point the value of the value of the MSG | reverse, to invoke parseFilters processing filter. ParseFilters are the same as the interpolation filters we mentioned earlier, so the value resolves as follows:
const name = 'msg'
const value = '_f("reverse")(msg)'
Copy the code
When the parse process is complete, we have the following AST object:
const ast = {
type: 1.tag: 'div'.attrs: [{name: 'msg'.value: '_f("reverse")(msg)'}].attrsList: [{name: ':msg'.value: "msg | reverse"}].attrsMap: {
':msg': 'msg | reverse'
},
rawAttsMap: {
':msg': { name: ':msg'.value: "msg | reverse"}}}Copy the code
Finally, when the code is called generate to generate the render function, the result is as follows:
const code = generate(ast, options)
// code prints the result
{
render: "with(this){return _c('div',{attrs:{"msg":_f("reverse")(msg)}})}".staticRenderFns: []}Copy the code
summary
In this section, we first review the two ways filters can be registered: global and local, and common usage scenarios: interpolation filters and V-bind expression filters.
We then analyze the interpolation filter parsing process in detail and the parseFilters method in depth.
Finally, we examine the parsing process of the V-bind expression filter. The first part of the parsing process is the instruction parsing process, but the last step is the parseFilters call for the value.
If you think it is good, please send me a Star at GitHub