【vue源碼篇】filter源碼詳解

filter 註冊 && 使用

在 Vue.js 中容許你自定義過濾器,能夠用 filter 格式化一些常見的文本,在項目中咱們會這樣使用 filter :前端

<!-- 在雙花括號中 --
{{ message | format }}

<!-- 在 v-bind 中 --
<div v-bind:id="message | format"</div 複製代碼

註冊 filter 有 2 種方式,全局 filter、組件 filter。vue

全局 filter 在實例化 Vue 以前定義:ios

Vue.filter('format', function(name) {
  return Hello, ${name} !
})
複製代碼

這樣註冊的 filter 能夠在每一個組件中調用,而不用在組件內再次註冊。git

在組件的中的 filter 咱們會這樣註冊:es6

filters: {
  format: function (name) {
   return Hello, ${name} !
  }
}
複製代碼

組件內註冊的 filters 只能在當前組件中調用。github

若是想獲取對應的 filter,能夠調用 filter 函數傳入對象 filter name,會返回已註冊的過濾器:express

var myFilter = Vue.filter('format')
複製代碼

filter 初始化

filter 的初始化是在 Vue 構造函數實例化的時候:api

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)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製代碼

關於 filter 的代碼在 renderMixin 函數,在 src/core/instance/render.js 在文件中:數組

export function renderMixin (Vue: Class<Component) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
  ...
  }
}
複製代碼

renderMixin 函數首先會調用 installRenderHelpers 而且傳入 Vue.prototype,而後給 Vue 添加 $nextTick、_render 方法,而在 installRenderHelpers 函數中會給 Vue.prototype 增長不少輔助函數。bash

installRenderHelpers 函數在 src/core/instance/render-helpers/index.js 文件中:

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
}
複製代碼

關於 filter 就是 target._f = resolveFilter,resolveFilter 就是解析 filter 的輔助函數,咱們來看一下 resolveFilter,在 src/core/instance/render-helpers/resolve-filter.js 文件中:

/** * Runtime helper for resolving filters */
export function resolveFilter(id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}
複製代碼

resolveFilter 函數接收一個參數 id,在內部調用了 resolveAsset 函數,而且傳入 this.$options 選項、'filters' 字符串、id filter 的 key、true。

/** * Return same value */
export const identity = (_: any) = _
複製代碼

identity 是一個返回原值的函數。

resolveAsset 函數主要用來處理 Vue 實例內部的 directives、 components 、filters,在 src/core/util/options.js 文件中:

/** * Resolve an asset. * This function is used because child instances need access * to assets defined in its ancestor chain. */
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 首先會以 type 也就是 type 爲 key 從 options 取出 assets 對象,此時就是 filter 對象。

首先調用 hasOwn 函數判斷 assets 是否有對應的 key,也就是檢測 id 是否已經註冊,若是已經註冊,將對象 filter 返回。

接着會嘗試以駝峯、中劃線形式取出 filter,最後將取出的 filter = res 返回。

filter 調用

接下來咱們來看看這個 filter 函數是如何在 Vue 實例中調用的,咱們知道在 installRenderHelpers 函數中,咱們將 resolveFilter 函數賦值給了 Vue.prototype 的 _f:

export function installRenderHelpers (target: any) {
  ...
  target._f = resolveFilter
  ...
}
複製代碼

咱們將 resolveFilter 掛載到了 Vue.prototype 上,在什麼狀況會調用這個 _f 函數呢?

解析 filter

vue 在對模板進行編譯的時候,會將模板字符解析成抽象語法樹 AST,那是如何解析 filter 的呢?

經過 parseFilters 函數來解析 filter,函數在 src/compiler/parser/filter-parser.js 文件中:

function parseFilters(exp) {
  var inSingle = false // 當前字符是否在 ' 單引號中的標識
  var inDouble = false // 當前字符是否在 " 雙引號中的標識
  var inTemplateString = false // 當前字符是否在 ` es6 模板的標識
  var inRegex = false // 當前字符是否在 / 正則中的標識
  var curly = 0 // 匹配到 { +1 匹配到 } -1
  var square = 0 // 匹配到 [ +1 匹配到 ] -1
  var paren = 0 // 匹配到 ( +1 匹配到 ) -1
  var lastFilterIndex = 0
  var c, prev, i, expression, filters

  for (i = 0; i < exp.length; i++) {
    // 保存上次循環的 c,初始爲 undefined
    prev = c
    // 調用 charCodeAt 方法返回 Unicode 編碼,課經過 String.fromCharCode() 反轉
    c = exp.charCodeAt(i)
    if (inSingle) {
      // 當前 c 是 ' ,而且 prev 不是 \ ,單引號部分結束
      if (c === 0x27 && prev !== 0x5c) {
        inSingle = false
      }
    } else if (inDouble) {
      // 當前 c 是 " ,而且 prev 不是 \ ,雙引號部分結束
      if (c === 0x22 && prev !== 0x5c) {
        inDouble = false
      }
    } else if (inTemplateString) {
      // 當前 c 是 ` ,而且 prev 不是 \ ,es6 模板部分結束
      if (c === 0x60 && prev !== 0x5c) {
        inTemplateString = false
      }
    } else if (inRegex) {
      // 當前 c 是 / ,而且 prev 不是 \ ,正則部分結束
      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,提取 | 前面部分 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) {
        // /
        var j = i - 1
        var p = void 0
        // find first non-whitespace prev char
        // 找到第一個不是空字符串的 p,中斷循環
        for (; j = 0; j--) {
          p = exp.charAt(j)
          if (p !== ' ') {
            break
          }
        }
        // var validDivisionCharRE = /[\w).+\-_$\]]/;
        // p 不爲空,而且不是字母 數組 + - _ $ ] 之一,說明是正則
        if (!p || !validDivisionCharRE.test(p)) {
          inRegex = true
        }
      }
    }
  }

  if (expression === undefined) {
    expression = exp.slice(0, i).trim()
  } else if (lastFilterIndex !== 0) {
    pushFilter()
  }

  function pushFilter() {
    // 將 exp.slice(lastFilterIndex, i).trim(),也就是 filter name 插入 filters 數組
    ;(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
    lastFilterIndex = i + 1
  }

  if (filters) {
    // 遍歷 filters 數組,循環處理 expression
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i])
    }
  }

  return expression
}
複製代碼

parseFilters 內部會循環處理 exp 字符串,會將 filters 處理成數組,這也是咱們可使用串聯 filter 的緣由,經過循環調用,而後將 filter 函數調用值返回。

wrapFilter 函數會接收 expression、filters 進行處理,在 src/compiler/parser/filter-parser.js 文件:

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}`
  }
}
複製代碼

wrapFilter 函數會調用 _f 也就是 resolveFilter 函數,若是 i < 0,說明沒有 ( ,表明沒有傳參,返回 es6 模板字符串 _f("${filter}")(${exp})

若是 i 0,表明傳入了參數,會對 filter 根據 ( 所在位置進行切分,取出 filter 和 args 參數部分,而且進行字符串模板的拼接。

舉個簡單的例子:

<div{{ 'Hello' | say('Sailor') }}</div
複製代碼

此時這個 filter 會進入 else 分支,這裏會對 say('Sailor') 字符串進行切割,name = say,args = 'Sailor'),_f('say')('Hello','Sailor')

if (filters) {
  // 遍歷 filters 數組,循環處理 expression
  for (i = 0; i < filters.length; i++) {
    expression = wrapFilter(expression, filters[i])
  }
}
複製代碼

可是在 vue 中咱們容許使用串聯的 filter,在 parseFilters 函數中咱們會循環處理 expression,而且將以前處理的 expression 做爲參數傳入,也就是說咱們有多是這樣。

<div{{ 'Hello' | say('Sailor') | sing('Sky') }}</div
複製代碼

此時進入 else 分支,會返回 _f('sing')(_f('say')('Hello','Sailor'),'Sky') 字符串。

調用 parseFilters 的函數有 getBindingAttr、processAttrs、parseText 這 3 個函數。

getBindingAttr 函數主要用來解析元素的 attr 屬性值:

export function getBindingAttr( el: ASTElement, name: string, getStatic?: boolean ): ?string {
  const dynamicValue =
    getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
  if (dynamicValue != null) {
    return parseFilters(dynamicValue)
  } else if (getStatic !== false) {
    const staticValue = getAndRemoveAttr(el, name)
    if (staticValue != null) {
      return JSON.stringify(staticValue)
    }
  }
}
複製代碼

getBindingAttr 函數會從 el 也就是抽象語法樹 AST 中取出 attrs ,若是有 : 、v-bind 綁定指令, 返回調用 parseFilters 函數處理後的值,若是傳入的參數 getStatic 不爲 false,嘗試調用 getAndRemoveAttr 返回 staticValue 靜態值,若是 staticValue 不爲 null,返回轉成字符串的 staticValue。

processAttrs 函數用來處理 attr 的屬性:

function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, isProp
  for (i = 0, l = list.length; i < l; i++) {
    ...
      if (bindRE.test(name)) { // v-bind
        name = name.replace(bindRE, '')
        value = parseFilters(value)
    }
    ...
  }
}
複製代碼

processAttrs 會循環處理 attrsList,返回通過 parseFilters 處理的 value。

parseText 函數則是用來處理標籤內的表達式了:

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
  }
}
複製代碼

parseText 函數內部會採用正則匹配出 match,而後調用 parseFilters 獲得 exp,而後將處理後做爲對象返回。

filter 初始化

Vue 初始化後,會調用 initGlobalAPI 爲 Vue 添加靜態方法,initGlobalAPI 函數在 src/core/global-api/index.js 文件中:

function initGlobalAPI(Vue) {
  // config
  var configDef = {}
  configDef.get = function() {
    return config
  }
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = function() {
      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: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(function(type) {
    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$1(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}
複製代碼

能夠看到在 initGlobalAPI 中添加了 config 配置對象、util 工具函數、set、delete、nextTick 方法,initUse 添加插件 use 方法、initMixin、initExtend 添加 mixin 、extend,initAssetRegisters 註冊初始化方法。

其中涉及到了 filter 的是:

ASSET_TYPES.forEach(function(type) {
  Vue.options[type + 's'] = Object.create(null)
})
複製代碼

循環 ASSET_TYPES 數組,初始化 components 、directives 、filters 爲空對象。

export const ASSET_TYPES = ['component', 'directive', 'filter']
複製代碼

ASSET_TYPES 數組是一個常量,保存了 Vue 實例中的 3 個屬性。

接着來看看 initAssetRegisters 函數,在 src/core/global-api/assets.js 文件:

function initAssetRegisters(Vue) {
  /** * Create asset registration methods. */
  ASSET_TYPES.forEach(function(type) {
    Vue[type] = function(id, definition) {
      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
      }
    }
  })
}
複製代碼

咱們接下看只看 filter 部分,在 initAssetRegisters 函數中會給 Vue 添加 filter 函數,函數接收 id 過濾器名 、definition 過濾器處理函數 2 個參數,若是沒有 definition 會返回 this.options[type + 's'][id] 獲取對應的 filter,若是有 definition ,會以 id 爲 key、 definition 爲值,賦值到 options.filters 對象上,進行 filter 的註冊,最後將 definition 返回。

此時 Vue 的 filter 是 functionfunction 會根據傳入的參數返回 filter 函數或者是註冊 filter。

filter 不能訪問 this

通常咱們在全局中會這樣調用 filter 函數:

Vue.filter('format', function(name) {
  return Hello, ${name} !
})
複製代碼

這個 filter 是在沒有建立 Vue 實例以前定義的,在代碼中咱們也沒有看到任何對方法作 this 綁定的動做,而在組件的中的 filter ,也沒有發現有改變 filter 函數 this 指向的代碼,所以註冊的 filter 都不能經過 this 訪問到 Vue 實例。

尤大大的解釋

在 vue 的 Issues #5998 中能夠看到尤大大的解釋,過濾器應該是純函數,建議使用 computed 或者 methods 格式化文本,因此 vue 的源碼中也沒有改變 filter 函數上下文的代碼。

參考文檔

Unicode 字符參考

轉載

若有侵權,請發郵箱至wk_daxiangmubu@163.com 或留言,本人會在第一時間與您聯繫,謝謝!!

關注咱們
長按二維碼關注咱們,瞭解最新前端資訊
相關文章
相關標籤/搜索