likes
comments
collection
share

vue2源码解析之过滤器

作者站长头像
站长
· 阅读数 15

基本使用

过滤器有三种用法,可以单独使用,可以串联使用还可以传递额外的参数;

/*
单独使用
*/
{{ message | capitalize }}

// 串联使用
/*
在这个例子中,把message作为参数传递给filterA函数执行,把
filterA函数执行的结果作为参数传递给filterB执行。
*/
{{ message | filterA | filterB }}

// 传递参数
/*
`capitalize` 被定义为接收三个参数的过滤器函数。其中 `message` 
的值作为第一个参数,普通字符串 `'arg1'` 作为第二个参数,表
达式 `arg2` 的值作为第三个参数
*/
{{ message | capitalize('arg1', 'arg2') }}

源码解析

// 源码位置 src/core/render.js

import { installRenderHelpers } from './render-helpers/index'
export function renderMixin (Vue: Class<Component>) {
    installRenderHelpers(Vue.prototype)
}

// 源码位置 src/core/render-helpers/index
import { resolveFilter } from './resolve-filter'
export function installRenderHelpers (target: any) {
  target._f = resolveFilter
}

// 源码位置 src/core/render-helpers/resolve-filter

import { identity, resolveAsset } from 'core/util/index'
export function resolveFilter (id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}

从上面可以看出,过滤器的处理函数被一层层包裹,最总的处理函数为resolveAsset; resolveAsset源码预览

// 源码位置 src/core/util/options.js

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
}

resolveAsset接收四个参数,分别是当前实例的options,type为filters,id为当前过滤器名称,warnMissing为true; 首先判断传递的过滤器名称是否是字符串,不是直接返回

if (typeof id !== 'string') {
    return
}

从options上查找到filters属性,此属性下的值为当前实例的所有的过滤器集合

// 找出当前实例上的所有的过滤器
const assets = options[type]

接着从assets这个集合里面的自身属性进行查找,分别通过传递的过滤器名称,驼峰形式的过滤器名称,首字母大写的过滤器名称查找,找到就返回,

// 判断此过滤器是不是自有过滤器,是就直接返回
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]

如果没有找到,就直接在原型上按照顺序进行查找;最后进行返回

const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
return res

最终编译成渲染函数之后的字符串

// 单独使用
_f('过滤器名')(参数)
// 串联使用
_f('过滤器1')(_f('过滤器2')(参数))

解析过滤器

过滤器的使用方式有两种,分别是在大括号中使用和在v-bind中使用;

<!-- 在双花括号中 -->  
{{ message | capitalize }}  
  
<!-- 在 `v-bind` 中 -->  
<div v-bind:id="rawId | formatId"></div>

那么vue在解析字符串模板的时候就需要分别针对这两种形式进行解析;

  • v-bind:

v-bind属于标签属性,因此,在解析字符串模板的函数parseHTML内部,如果遇到了标签属性就通过parseAttrs函数进行解析,在parseAttrs函数内部如果遇到了过滤器就通过parseFilters函数进行解析(模板解析在模板编译阶段讲解过);

function processAttrs (el) {
    ...
    if (bindRE.test(name)) { // v-bind
        name = name.replace(bindRE, '')
        // 解析value返回一个_f函数执行的表达式
        value = parseFilters(value)
        ...
    }
    ...
}
  • {{ }}中

{{}}属于文本,因此,在解析字符串模板的函数parseHTML内部,如果遇到文本就调用parseText函数进行解析,在parseText函数内部如果遇到过滤器就调用parseFilters函数进行解析;


export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
    ...
    const exp = parseFilters(match[1].trim())
    ...
}

parseFilters函数源码预览

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
  }
  console.log('filters', filters, exp, lastFilterIndex)
  if (filters) {
    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('(')
  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}`
  }
}

首先定义了一些变量

  // exp是否在 ' ' 中
  let inSingle = false
  // exp是否在 " " 中
  let inDouble = false
  // exp是否在 ` ` 中
  let inTemplateString = false
  //  exp是否在 \ \ 中
  let inRegex = false
  // 发现一个 { 则curly加1,发现一个 } 则curly减1,直到culy为0 说明 { ... }闭合
  let curly = 0
  // 发现一个 [ 则curly加1,发现一个 ] 则curly减1,直到culy为0 说明 [ ... ]闭合
  let square = 0
  // 发现一个 ( 则curly加1,发现一个 ) 则curly减1,直到culy为0 说明 ( ... )闭合
  let paren = 0
  // 解析游标,每循环过一个字符串游标加1
  let lastFilterIndex = 0
  // c: 当前字符 prev:上个字符 i:当前索引 expression当前表达式 filters:存储过滤器数组
  let c, prev, i, expression, filters

遍历字符串,从前往后开始一个一个匹配,匹配出那些特殊字符,如',",`,{,},[,],(,),|,如果遇到',",`,就继续找到下个同样的结束字符,并且其中的(),[],{}都是闭合的,那么匹配到的|才是过滤器;如果表达式不存在就进行截取获取到表达式,如果存在就为过滤器,进行添加到数组中

/*
0x22 ----- "
0x27 ----- '
0x28 ----- (
0x29 ----- )
0x2f ----- /
0x5C ----- \
0x5B ----- [
0x5D ----- ]
0x60 ----- `
0x7C ----- |
0x7B ----- {
0x7D ----- }
*/
// 遍历字符串
  for (i = 0; i < exp.length; i++) {
    // 存储上个字符
    prev = c
    // 获取到当前字符的ASCII码
    c = exp.charCodeAt(i)
    // 如果是单引号存在
    if (inSingle) {
      // 如果当前字符为单引号并且上个字符不是\,则设置inSingle = false
      if (c === 0x27 && prev !== 0x5C) inSingle = false
      // 如果是双引号存在
    } else if (inDouble) {
      // 如果当前为双引号并且上个字符不是\,则设置inDouble = false
      if (c === 0x22 && prev !== 0x5C) inDouble = false
      // 如果模板字符串存在
    } else if (inTemplateString) {
      // 如果当前字符为`并且上个字符不为\,则设置inTemplateString = false
      if (c === 0x60 && prev !== 0x5C) inTemplateString = false
      // 如果正则存在
    } else if (inRegex) {
      // 如果当前字符为/并且上个字符不为\ 则设置inRegex = false
      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
    ) {
      // 如果表达式为undefined
      if (expression === undefined) {
        // first filter, end of expression
        // 游标移动一个
        lastFilterIndex = i + 1
        // 截取表达式
        expression = exp.slice(0, i).trim()
      } else { // 否则存储过滤器
        pushFilter()
      }
    } else { 
      switch (c) {
        // 如果是双引号就inDouble为true
        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
        }
      }
    }
  }
  
  
  function pushFilter () {
    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
    lastFilterIndex = i + 1
  }

比如遇到{{ message | filter1 | filter2(66)}},那么经过上面的处理之后为expression=message,filters=['filter1','filter2(66)'];

接着遍历过滤器数组,把返回的结果重新赋值给expression;这样做的目的就是可以实现串联调用的方式;

if (filters) {
    // 遍历过滤器,把每个过滤器设置为_f字符串函数
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i])
    }
}

wrapFilter函数,分别对有参数和无参数的过滤器进行了处理

function wrapFilter (exp: string, filter: string): string {
  // 如果包含括号表示有传递参数
  const i = filter.indexOf('(')
  // 没有括号表示没有参数
  if (i < 0) {
    // 直接通过_f()()进行包裹
    return `_f("${filter}")(${exp})`
  } else { // 找出过滤器名和参数
    const name = filter.slice(0, i)
    const args = filter.slice(i + 1)
    // _f()(参数,参数)形式包裹
    return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
  }
}

最终的形式

_f(filter2)(_f(filter1)(message),66)
转载自:https://juejin.cn/post/7236915296963117112
评论
请登录