Vue filter詳解

Vue filter源碼詳解

1.解析表達式

reportDate | DFormat('YYYY-MM-DD') | SDefault爲例。

parseFilters 解析函數位於node_modules/vue/src/compiler/parser/filter-parser.js。主要作用是將表達式轉換爲可立即執行的函數字符串。

//匹配任意非空字符、)、.、+、-、_、$、]
//排除一些例如a++,a--,../path,(a+b)之類的影響
const validDivisionCharRE = /[\w).+\-_$\]]/

export function parseFilters(exp: string): string {
    let inSingle = false    //當前字符是否在單引號中
    let inDouble = false    //當前字符是否在雙引號中
    let inTemplateString = false    //當前字符是否在es6中的`模板標識符中
    let inRegex = false     //當前字符是否在正在表達式/中
    let curly = 0           //匹配到{時+1,匹配到}時-1
    let square = 0          //匹配到[時+1,匹配到]時-1
    let paren = 0           //匹配到(時+1,匹配到)時-1
    let lastFilterIndex = 0 //最後一個過濾器的起始座標
    //c當前字符、prev上次遍歷的字符、i遍歷下標、expression表達式、filters過濾器
    let c, prev, i, expression, filters

    for (i = 0; i < exp.length; i++) {
        //新一輪遍歷時,修改上一輪循環的字符爲遍歷完成的最後一個字符
        prev = c
        //返回指定下標的Unicode編碼
        let a = exp.charAt(i)
        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) {
            //當前字符不是`,並且上一個字符不是\,則`es6模板部分結束
            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
                //第一次解析filter,記錄最後一個filter的起始位置爲|後的一位
                lastFilterIndex = i + 1
                //提取|前的部分作爲表達式,即提取過濾器的第一個參數
                expression = exp.slice(0, i).trim()
            } else {
                //將解析到的filter放入預置的filters數組中
                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
                //找到第一個不爲空的字符p
                for (; j >= 0; j--) {
                    p = exp.charAt(j)
                    if (p !== ' ') break
                }
                //如果p存在且通過了特殊字符校驗,那麼則認爲之後的/之後的內容爲正則表達式內容
                if (!p || !validDivisionCharRE.test(p)) {
                    inRegex = true
                }
            }
        }
    }

    
    //遍歷完成後,表達式即爲過濾器第一個參數,並且獲得了最後一個過濾器的起始座標lastFilterIndex
    if (expression === undefined) {
        expression = exp.slice(0, i).trim()
    } else if (lastFilterIndex !== 0) {
        //如果存在過濾器則進入這一步驟,此時除了最後一個過濾器,
        //其他過濾器均被解析放入filters中了,所以將最後一個過濾器也放入
        pushFilter()
    }

    function pushFilter() {
        (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
        lastFilterIndex = i + 1
    }

    if (filters) {
        //遍歷已經解析出來的filters數組,依次處理每個filter
        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('(')
    //不帶參數的filter
    if (i < 0) {
        // _f: resolveFilter
        return `_f("${filter}")(${exp})`
    } else {
        //帶參數的filter
        const name = filter.slice(0, i)
        const args = filter.slice(i + 1)
        //將第一個表達式作爲過濾器的第一個參數,其他參數依次拼接
        return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
    }
}

解析後的結果爲:_f("defaultStr")(_f("DFormat")(reportDate,'YYYY-MM-DD'))

_f是一個註冊函數,用於註冊我們書寫的過濾器方法。

2.註冊過濾器(resolveFilter)

2.1Vue初始化

Vue初始化後會調用initGlobalAPI 添加靜態方法,位於node_modules/vue/src/core/global-api/index.js

/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

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

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

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {//在這裏添加filter、component、directive中的方法
    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(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)//初始化各組件方法
}

export const ASSET_TYPES = ['component', 'directive', 'filter']

這裏初始化爲Vue註冊了config對象和部分插件方法。

initAssetRegisters位於node_modules/vue/src/core/global-api/assets.js

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      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
      }
    }
  })
}

當type爲filter是即註冊過濾器方法,id即爲過濾器方法名,definition爲過濾器方法。

註冊過後即可使用如下方式定義全局filter:

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

需注意,filter方法並不是在實例化的時候才創建的,而是實例化之前即創建好了,所以filter內的this無法獲取到vue實例。

2.2初始化綁定_f

installRenderHelpers位於node_modules/vue/src/core/instance/render-helpers/index.js

/* @flow */

import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util'
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
import { renderList } from './render-list'
import { renderSlot } from './render-slot'
import { resolveFilter } from './resolve-filter'
import { checkKeyCodes } from './check-keycodes'
import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-scoped-slots'
import { bindDynamicKeys, prependModifier } from './bind-dynamic-keys'

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
  target._d = bindDynamicKeys
  target._p = prependModifier
}

_f是獲取具體過濾器的函數,其綁定在Vue原型上。

2.3調用具體的filter

resolveFilter位於node_modules/vue/src/core/instance/render-helpers/resolve-filter.js

//原值返回函數
export const identity = (_: any) = _

//註冊filter
function resolveFilter(id) {    
    return resolveAsset(
        //這裏的this是vm,resolveFilter是註冊在Vue原型上的方法
        this.$options, 'filters', id, true
    ) || identity
}

resolveAsset位於node_modules/vue/src/core/util/options.js

//執行具體的指令,過濾器的type爲filters,返回一個函數
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
}

3.流程圖

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章