深入浅出Vue.filter(揭秘过滤器中的魔法)

4,826 阅读4分钟

简介

大家好,我是六六。在开发中我们经常会使用到过滤器,作用就是来格式化一下我们的文本。但是过滤器原理是什么呢?今天我来为大家揭秘一下,向大家传授一下这过滤器神奇的魔法。当然如果有不对的地方,也请大家多多指教了。

目录

  • 过滤器用法
  • 过滤器原理概述
  • 串联过滤器与参数
  • resolveFilter原理
  • 过滤器的解析
  • 总结

1. 过滤器的用法

Vue.filter( id, [definition] )

参数:

  • {string} id
  • {Function} [definition]

用法:

注册或获取全局过滤器。

// 注册
Vue.filter('my-filter', function (value) {
  // 返回处理后的值
})

// getter,返回已注册的过滤器
var myFilter = Vue.filter('my-filter')

当然了也可以局部注册:

filters:{
    myFilter:function(value){
        value+='hello world'
        return value
    }
}

过滤器我们已经注册好了,接下来开始使用了,可以用在两个地方

 <--在双括号中-->
{{message|myFilter}}
<-- 在v-bind中-->
<div v-bind:id='Id|myFilter'></div>

通常情况下,我们也可以串联使用:

{{message|filter1|filter2}}

当然了,也可以传入参数:

{{message|filter1('arg1','arg2')}}

注意注意,实际上我们传入了三个参数,还有一个参数当然使我们要过滤的message,永远是作为第一个参数的

2.过滤器原理概述

假如模板是这个样子的:

{{message|myFilter}}

这个过滤器在模板编译就会变成这个样子:

_s(_f('myFilter')(message))

看到这个不要慌,其实也是蛮简单的,慢慢来说:

_f函数:

_f这个函数是resolveFilter的别名,作用就是找到我们注册过的'myFilter'这个过滤器,并且返回出去。更通俗的讲,找到我们写的过滤器,并将参数传入进去,执行就ok了。其实就是函数执行嘛。(前提是要找到这个函数哦)

_s函数:

这个函数是toString函数的别名,我们来看看这个函数:

 /**
   * Convert a value to a string that is actually rendered.
   */
  function toString (val) {
    return val == null
      ? ''
      : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
        ? JSON.stringify(val, null, 2)
        : String(val)
  }

函数具体细节大家慢慢看,也不是很难的,就是对过滤器执行出来的值在进行一次过滤。
在Vue中,也定了类似这样的很多别名,我们看一下:

function installRenderHelpers (target) {
    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;
  }

有兴趣的大家也可以好好研究,好了回归我们的过滤器,我们来总结一下过滤器的原理: 其实就是执行myFilter过滤器函数并把message当做参数传入,接着我们拿到这个参数在传入toString函数,最终toString函数执行后的结果就会保存到VNode中的text属性中,最后渲染成视图。

3. 串联过滤器与参数

3.1 串联过滤器

{{message|filter1|filter2}}

模板非常简单明了,那么编译后是什么样子呢?

_s(_f('filter2')(_f('filter1')(message)))

从代码中看出,首先执行filter1过滤器,将message传入,返回的结果又当做参数传入filter2,最终返回结果。

3.2 过滤器接受参数

{{message|filter1|filter2('a')}}

编译之后:

_s(_f('filter2')(_f('filter1')(message,'a')))

message是作为第一个参数的,接下来就是函数执行而已,没有什么难度的。

4. resolveFilter原理

之前说过这个函数作用就是帮助我们找到过滤器,那么究竟是怎么找到的呢?这一章慢慢来讲。
在源码中找到这个函数的定义:

  /**
   * Runtime helper for resolving filters
   */
  function resolveFilter (id) {
    return resolveAsset(this.$options, 'filters', id, true) || identity
  }

这有一行代码,很好理解。通过resolveAsset函数能找到就返回过滤器,找不到就返回identity.

4.1 identity是什么?

 /**
   * Return the same value.
   */
  var identity = function (_) { return _; };

其实identitiy函数很简单,就是传入什么值,就返回什么值。

4.2 resolveAsset大揭秘

先说作用,再看源码,resolveAsset作用就是找到模板中写的过滤器。

  /**
   * Resolve an asset.
   * This function is used because child instances need access
   * to assets defined in its ancestor chain.
   */
  function resolveAsset (
    options,
    type,
    id,
    warnMissing
  ) {
    /* istanbul ignore if */
    if (typeof id !== 'string') {
      return
    }
    var assets = options[type];
    // check local registration variations first
    if (hasOwn(assets, id)) { return assets[id] }
    var camelizedId = camelize(id);
    if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
    var PascalCaseId = capitalize(camelizedId);
    if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
    // fallback to prototype chain
    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
    if (warnMissing && !res) {
      warn(
        'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
        options
      );
    }
    return res
  }
  • 首先判断传入的id是否为字符串,如果不是终止函数执行,默认就返回undefined,也就会执行identitiy函数了。
  • 然后声明了变量assets保存了options['filters'],就是过滤器的集合
  • 通过hasOwn函数检查assets自身是否含有id属性的值,有的话当然直接返回了。
  • 如果没有使用camelize函数将id驼峰化后重新检查自身含有id属性
  • 如果还没有使用capitalize将id首字母大写,继续寻找
  • 还找到,按照之前顺序重新再找一遍。

5. 过滤器的解析

我们知道过滤器是如何查找的,现在只剩下过滤器是如何编译的了。在Vue中,src/compiler/parser/filter-parser.js中提供了一个parseFilters函数,是专门用来解析过滤器的,来看看如何实现的:

 function parseFilters(exp) {
     // filters为一个数组,
    let filters = exp.split('|')
    // 需要格式化的文本表达式
    let expression = filters.shift().trim()
    let i
    if (filters) {
      for (i = 0; i < filters.length; i++) {
        debugger;
        //进入wrapFilter进行拼接
        expression = wrapFilter(expression, filters[i].trim())
      }
    }
    return expression
  }
  function wrapFilter(exp, filter) {
    const i = filter.indexOf('(')
    // 无参数
    if (i < 0) {
      return `_f("${filter}")(${exp})`
    } else {
      const name = filter.slice(0, i)
      const args = filter.slice(i + 1)
      return `_f("${name}")(${exp},${args}`
    }
  }
  let a = parseFilters(`message|filter1|filter2`)
  let b = parseFilters(`message|filter1|filter2('a')`)
  console.log(a)

打印结果:

_f("filter2")(_f("filter1")(message))
_f("filter2")(_f("filter1")(message),'a')

解析原理:
parseFilters:首先分别拿到过滤器名字和参数,循环调用wrapFilter函数传入表达式和过滤器名称拼接字符串。
wrapFilter:通过indexOf('(')判断是否有参数,进行不同条件的拼接字符串,最终返回。
大家也可以手动敲一敲,原理也不是很难。但是在Vue中,多了很多很多边界条件判断,将近100行,所以比上面的例子稍微复杂一点。

6. 总结

简单的概述过滤器原理:主要分为两个阶段:

  • 编译:在编译阶段把带有过滤器的模板编译成函数调用。
  • 执行:拿到编译后的模板,通过_f查询注册的过滤器,进行过滤器函数执行。

本文章参考《Vue.js深入浅出》以及Vue源码