生命周期
生命周期4个阶段
Vue2官方文档上的图
分成4个阶段:初始化阶段、模板编译阶段、挂载阶段、卸载阶段
初始化阶段:
从new Vue()到created之间的阶段,此阶段主要是在Vue.js实例上初始化一些属性、事件以及响应数据,例如props、methods、data、computed、watch、provide和inject等等。
模板编译阶段:
在created钩子函数到beforeMount钩子函数之间的阶段是模板编译阶段,此阶段主要目的是将模板编译为渲染函数,只存在于完整版中。(原因在于vm.$mount运行时版的实现已经默认存在渲染函数)
挂载阶段:
beforeMount钩子函数到mounted钩子函数之间是挂载阶段,此阶段Vue.js会将其实例挂载到DOM元素上,即将模板渲染到指定的DOM元素中。而且在挂载的过程中,开启Watcher来持续追踪依赖的变化。(在vm.$mount源码分析那里有提到)
卸载阶段:
调用vm.$destroy方法后,Vue.js的生命周期进入卸载阶段
初始化阶段
new Vue()
new Vue( )被调用时,首先会进行一些初始化操作,然后进入模板编译阶段,最后进入挂载阶段
//目录 src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
_init方法
//目录 src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
//挂载到Vue.prototype上
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// merge options
/*
将当前用户传递的options选项与当前构造函数的options选项及其父级实例构造函数的options属性,
合成一个新的options并赋值给$options属性
*/
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
/*
resolveConstructorOptions
获取当前实例中构造函数的options选项及其所有父级的构造函数的options,有父级时因为当前实例
可能是一个子组件
*/
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
initLifecycle
初始化实例属性需要初始化内部使用属性(vm._watcher等,以 _开头的),也初始化供外部使用的属性(vm.$parent等,以$开头的)
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
/*
vm.$parent 需要找到第一个非抽象类型的父级,如果当前组件不是抽象组件而且存在父级,就通过while
自底向上循环,直到找到第一个非抽象类的父级
*/
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
//vm.$root 是自顶向下将根组件的$root依次传递给每一个子组件
vm.$root = parent ? parent.$root : vm
//vm.$children是子组件主动添加到父组件中的,如第14行
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
initInjections/initProvide
用法:inject和provide选项需要一起使用,它们允许祖先组件向其所有子孙后代注入依赖,并在其上下游关系成立的时间里始终生效。
示例:
//provide选项应该是一个对象或返回一个对象的函数。该对象包含可注入器子孙的属性
//inject选项应该是一个字符串数组或对象,对象的key是本地绑定名,value是一个key或对象,用来在可用的注入内容中搜索
//示例一
var Provider={
provide:{
foo:'bar'
}
}
var Child={
inject:['foo'],
created(){
console.log(this.foo)//"bar"
}
}
//示例二 可以使用ES2015 Symbol作为key,但是这只在原生支持Symbol和Reflect.ownKeys的环境下可工作
const s=Symbol()
const Provider={
provide(){
return {
[s]:'foo'
}
}
}
const Child={
inject:{s}
}
//示例三 可以在data/props中访问注入的值
const Child={
inject:['foo'],
props:{
bar:this.foo
}
}
}
//default 默认值
const Child={
inject:{
foo:{ default:'foo' }
}
}
/*
从_init代码可以看出initInjections是data/props之前初始化,initProvide是在data/props之后初始
化,这样做的目的是让用户可以在data/props中使用inject所注入的内容
*/
原理:
provide
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
//内容注入到_provided属性中,如果是函数则执行函数,否则直接赋值
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
inject
export function initInjections (vm: Component) {
//resolveInject是通过用户配置的inject,自底向上搜索可用的的注入内容,并将搜索结果返回
const result = resolveInject(vm.$options.inject, vm)
if (result) {
//通知defineReactive函数不要将内容转换成响应式
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
/*
如果浏览器原生支持Symbol,使用Reflect.ownKeys读出inject中所有key,如果不支持使用Object.keys
获取key。
原因是Reflect.ownKeys可以读取Symbol类型的属性,而且也可以读出包括不可枚举的属性,要用filter
过滤
Object.keys不可读取Symbol类型的属性
*/
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
/*
from属性时provide原属性
Vue.js在实例化的第一步是规格化用户传入的数据(就是在添加$options属性时),即使inject传递的
内容是数组也会被规格化成对象并存放在from属性中
{
inject:[foo]
}
规格化后是
{
inject:{
foo:{
from:'foo'
}
}
}
*/
const provideKey = inject[key].from
let source = vm
while (source) {
//whlie循环自底向上搜索
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
//如果搜索不到,有默认值则使用默认值
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
//默认值支持函数,若为函数则执行它
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
initState
initState是初始化一些状态,包括props、methods、data、computed、watch
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
/*
初始化各种状态的顺序是有讲究的
先初始化props,后初始化data,就可以在data中使用props中的数据
先初始化props、data,所以watch可以观察到里面的数据
*/
initProps
第一步,规格化props,将props规格化为对象格式。
/*
在_init方法中,调用了mergeOptions,在mergeOptions函数中,调用了normalizeProps(child, vm)、normalizeInject(child, vm)
、normalizeDirectives(child)规格化数据
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
...
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
...
}
/**
* Ensure all props option syntax are normalized into the
* Object-based format.
*/
//对props进行规格化处理,规格化之后的props为对象的格式
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
//没有props退出
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
//camelize 将val驼峰化
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} //不是数组类型,调用isPlainObject函数检查它是否为对象类型
else if (isPlainObject(props)) {
//for...in...遍历props(for...in遍历的是键)
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
第二步,初始化props
规格化之后的props从其父组件传入的props数据中或从使用new创建实例时传入的propsData参数中,筛选出需要的数据保存在vm._props中,然后在vm上设置一个代理,实现通过vm.x访问vm._props.x
function initProps (vm: Component, propsOptions: Object) {
//propsData保存父组件传入或用户通过propsData传入的真实props数据
const propsData = vm.$options.propsData || {}
//props是指向vm._props的指针
const props = vm._props = {}
/*
keys是指向vm.$options._propKeys的指针,其作用是缓存props对象中的key,
将来更新props时,只需要遍历
*/
const keys = vm.$options._propKeys = []
//判断当前组件是否是根组件
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
//不是根组件不需要将props数据转换为响应式
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
//将数据设置到vm._props中
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
validateProp是获取props内容的
export function validateProp (
key: string,
propOptions: Object,//子级组件用户设置的props选项
propsData: Object,//父组件或用户提供的props数据
vm?: Component
): any {
const prop = propOptions[key]
//absent表示当前的key在用户提供的props选项中是否存在
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// 处理prop是否为布尔值情况
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
//key在用户提供的props中不存在,而且也没有设默认值,value为false
if (absent && !hasOwn(prop, 'default')) {
value = false
}
//key存在但是为空字符串或者value和key相等,hyphenate将key驼峰转换
else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
//value不存在,如果有默认值则使用默认值,并转换为响应式数据
if (value === undefined) {
//获取默认值
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
//判断prop是否有效,prop验证失败时会产生警告
assertProp(prop, key, value, vm, absent)
}
return value
}
initMethods
initMethods只需循环选项中的methods对象,并将每个属性依次挂载到vm上即可
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
//方法不合法
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
//方法已经在props中声明过了
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
//方法已经在vm中
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
//将方法挂载到vm中
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
initData
data中的数据保存在vm._data中,然后通过设置代理,可以通过vm.x访问到vm._data.x
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
//methods有名称为key的方法了,但是data还是会代理到实例中
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
//props上已经存在与key相同的属性了,不会将data代理到实例中
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
//将data转换为响应式
observe(data, true /* asRootData */)
}
代理proxy 其实就是通过定义对应 key 的 getter/setter 来使得它获取到实际上是其他的值
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
initComputed
computed比较常见的一个特性就是它会缓存结果,只有计算依赖的数据或计算结果发生变化才会重新计算。
computed的实现其实就是在 vm 上的一个特殊的 getter,它结合了 Watcher 来实现缓存和依赖收集的功能。
实现的过程
- 使用Watcher读取计算属性
- 读取计算属性函数中的数据,使用Watcher观察数据的变化(如果计算属性是模板读取,那么使用组件的Watcher观察,如果是用户自定义的watch,那么使用用户自定义地Watcher观察)
- 当数据发生变化时,通知计算属性的Watcher和组件的Watcher(重新渲染模板)
- 计算属性的Watcher把dirty设置为true
- 模板重新读取计算属性的值,因为dirty为true,所以会重新计算一次值
```javascript
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
//watchers是用来保存所有计算属性的watcher实例
const watchers = vm._computedWatchers = Object.create(null)
//isSSR用于判断当前运行环境是否是SSR(服务端渲染)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === ‘function’ ? userDef : userDef.get
if (process.env.NODE_ENV !== ‘production’ && getter == null) {
warn(
Getter is missing for computed property "${key}".
,
vm
)
}
//在非SSR环境中,为计算属性创建内部观察器
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
/*
这里如果vm上已经有一个名为key的属性,那么这个名为key的属性就可能是
data、props、methods,但是只有data、props会有警告提示,methods并没
有提示
*/
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
> **注意,Object.create(null)创建出来的对象是没有原型的,它不存在__proto__属性**
defineComputed就是将key设置到vm上,主要就是要分服务端环境来处理key的getter
```javascript
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
//shouldCache用来判断computed是否应该有缓存,非服务端才要缓存
const shouldCache = !isServerRendering()
//userDef是函数,则为getter函数
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key) //计算属性getter
: createGetterInvoker(userDef)//普通getter
sharedPropertyDefinition.set = noop
}
//否则为对象,将对象的get方法作为getter方法
else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter接受一个key为参数,并返回一个函数为key的getter函数,先获取key对应的watcher,如果watcher存在而且计算属性的返回值也发生了变化,则重新计算得出最新结果
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//dirty属性来标志计算属性是否发生了变化
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
//将读取计算属性的Watcher添加到计算属性所依赖的所有状态的依赖列表
//其实就是让读取计算属性的那个Watcher持续观察计算属性所依赖的状态的变化
watcher.depend()
}
return watcher.value
}
}
}
evaluate()就是执行一下this.get()方法重新计算一下值,然后将dirty设置为false;depend()先遍历this.deps属性(保存了计算属性用到的所有状态的dep实例,每个dep实例保存了它的所有依赖),依次执行dep实例的depend方法,将组件的Watcher依次加入这些dep实例的依赖列表中,实现了组件watcher观察计算属性用到的所有状态的变化。
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.dirty = this.lazy // for lazy watchers
this.value = this.lazy
? undefined
: this.get()
}
...
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
initWatch
示例:
//watch选项的格式 { [key:string]:string|Function|Object|Array }
var vm=new Vue({
data:{
a:1,
b:2,
c:3,
d:4
},
watch:{
a:function(newVal,oldVal){
},
b:{
handler:function(newVal,oldVal){}
deep:true,//深度watcher
immediate:true//侦听开始后会立即调用
}
c:'myMethod'//一个函数名,
d:[
function handle1(newVal,oldVal){},
function handle2(newVal,oldVal){}
]
}
})
原理:
initState初始化时,先判断用户是否设置watch选项并且watch选项不等于浏览器原生的watch(因为Firefox浏览器中Object.prototype上有一个watch方法)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
initWatch的实现其实并不难,就是遍历watch选项,然后分成数组和其他情况处理,如果是数组则遍历数组中的每一项依次处理
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
createWatcher就是将handler分成字符串、对象、函数类型来处理
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
callHook内部原理
作用:callHook的作用时触发用户设置的生命周期钩子,而用户设置的生命周期钩子会在执行new Vue()时,通过参数传递给Vue.js(即可以在Vue.js构造函数中,通过options参数获取到用户设置的生命周期钩子,例如vm.$options.created)
注意:vm.$options.created获取到的是一个数组[fn],数组中包括了钩子函数。
为什么是数组?
因为Vue.mixin方法会将选项写入Vue.option中,影响之后创建的所有Vue.js实例,而Vue.js初始化时会将用户传入的option和构造函数的options合并成一个选项赋值给vm.$options。如果Vue.mixin和用户实例化Vue.js时,设置了同一个生命周期钩子,则触发生命周期时,需要同时执行这两个函数,而转换成数组后,即可在同一个生命周期钩子列表里保存多个生命周期钩子。
源码:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
//取出列表,依次执行
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
指令
过滤器
用法:用来格式化文本。
示例:
<!--双括号-->
{{message|capitalize}}
<!--在v-bind中-->
<div v-bind:id="raw|formatId"></div>
//在组件定义一个本地过滤器,过滤器函数总是将表达式的值作为第一个参数
filters:{
capitalize:function(val){
if(!val)return ''
value=value.toString()
return value.charAt(0).toUpperCase()+value.slice(1)
}
}
//也可以在Vue.js示例之前全局定义过滤器
Vue.filter('capitalize',function(value){
if(!val)return ''
value=value.toString()
return value.charAt(0).toUpperCase()+value.slice(1)
})
//过滤器也可以串联
{{message|filterA|filterB}}
//过滤器函数也可以接收参数
{{message|filterA('arg1','arg2')}}