1. 程式人生 > >Vue原始碼閱讀--過濾器

Vue原始碼閱讀--過濾器

過濾器

作用 :

用於一些常見的文字格式化

使用方式:

過濾器可以用在兩個地方:雙花括號插值和 v-bind 表示式 (後者從 2.1.0+ 開始支援)。過濾器應該被新增在 JavaScript 表示式的尾部,由“管道”符號指示:

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

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
複製程式碼

建立過濾器的方式

  1. Vue.filter('id',function(){}) 全域性過濾器定義
  2. 元件中 filters : { 'id' : function(){} } 元件內部過濾器

原始碼分析

一、編譯階段

parse階段

我們發現對於過濾器的使用方式有兩種:

  • 在屬性中 v-bind:id="xxx | filterA"
  • 在文字雙花括號插值中 {{xxx | filterA | filterB(arg1,arg2)}}
1. 屬性中 v-bind:id="xxx | filterA"

在parse處理開始節點的processAttrs() 中發現了 通過bindRE.test(name)去匹配響應式的屬性,然後通過 parseFilters(value) 去解析值中的過濾器。

if (bindRE.test(name)) { // v-bind
    // 獲取屬性的名稱  移除 : | v-bind:
    name = name.replace(bindRE, '')
        //  處理value 解析成正確的value
    value = parseFilters(value)
    isProp = false
    
}

複製程式碼
compiler\parser\filter-parser.js
/**
 * 表示式中的過濾器解析 方法
 * @param {*} exp 
 */
export function parseFilters(exp: string
): string
{ // 是否在 ''中 let inSingle = false // 是否在 "" 中 let inDouble = false // 是否在 `` let inTemplateString = false // 是否在 正則 \\ 中 let inRegex = false // 是否在 {{ 中發現一個 culy加1 然後發現一個 } culy減1 直到culy為0 說明 { .. }閉合 let curly = 0 // 跟{{ 一樣 有一個 [ 加1 有一個 ] 減1 let square = 0 // 跟{{ 一樣 有一個 ( 加1 有一個 ) 減1 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 ) { /* 如果前面沒有表示式那麼說明這是第一個 管道符號 "|" 再次遇到 | 因為前面 expression = 'message ' 執行 pushFilter() */ 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() } // 獲取當前過濾器的 並將其儲存在filters 陣列中 // filters = [ 'filterA' , 'filterB'] function pushFilter() { (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()) lastFilterIndex = i + 1 } if (filters) { for (i = 0; i < filters.length; i++) { expression = wrapFilter(expression, filters[i]) } } return expression } 複製程式碼

解析過濾器的方法其實很簡單:

  1. 將屬性的值從前往後開始一個一個匹配,關鍵符號 : "|" 並排除 ""、 ''、 ``、 //、 || (字串、正則)中的管道符號 '|' 。

如:

<div v-bind:id="message + 'xxx|bbb' + (a||b) + `cccc` | filterA | filterB(arg1,arg2)"></div>
複製程式碼

字元一個一個往後匹配 如果發現 ` " ' 說明在字串中,那麼直到找到下一個匹配的才結束, /一樣 同時 匹配 () {} [] 這些需要兩邊相等閉合 那麼 | 才有效, 最後一個條件排除 || 即可

  1. 所以上面直到遇到 第一個正確的 | ,那麼前面的表示式 並存儲在 expression 中,後面繼續匹配再次遇到 | ,那麼此時 expression有值, 說明這不是第一個過濾器 pushFilter() 去處理上一個過濾器

/**
    生成過濾器的 表示式字串

    如上面的 
    exp = message
    filters = ['filterA','filterB(arg1,arg2)'] 

    第一步  以exp 為入參 生成 filterA 的過濾器表示式字串  _f("filterA")(message)

    第二步 以第一步字串作為入參 生成第二個過濾器的表示式字串 _f("filterB")(_f("filterA")(message),arg1,arg2)

    => _f("filterB")(_f("filterA")(message),arg1,arg2)

 * @param {string} exp   上一個過濾器的值 沒有就是 表示式的值
 * @param {string} filter
 * @returns {string}
 */
function wrapFilter(exp: string, filter: string): string {
    // 判斷是否存在入參, 即 'filterB(arg1,arg2)'
    const i = filter.indexOf('(')
    if (i < 0) {
        // 如果不是  直接生成  "_f("filterA")(message)"
        // _f: resolveFilter
        return `_f("${filter}")(${exp})`
    } else {
        // 過濾器名稱
        const name = filter.slice(0, i)
        // 過濾器自定義入參
        const args = filter.slice(i + 1)
        // 生成 "_f("filterB")(message,arg1,arg2)"
        return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
    }
}
複製程式碼

此時 exp = message + 'xxx|bbb' + (a||b) + cccc , filter = filterA

  1. 繼續判斷 過濾器是否存在 (), 此時不存在, 那麼filter就是名稱 第一個入參就是前面的 exp。

生成

"_f("filterA")(message + 'xxx|bbb' + (a||b) + `cccc`)"
複製程式碼
  1. 以前面的結果為exp , 發現存在 ( , 然後生成
"_f("filterB")(_f("filterA")(message + 'xxx|bbb' + (a||b) + `cccc`),arg1,arg2)"
複製程式碼
2. 文字雙花括號插值中 {{ message | capitalize }}

文字的處理是在 parse中的chars()方法 其中存在一個解析 {{}} 的方法 parseText()


export function parseText(
    text: string,
    delimiters ? : [string, string]
): TextParseResult | void {
    // 處理 文字內容  如:
    //  {{obj.name}} is {{obj.job}}
    while ((match = tagRE.exec(text))) {
        // ' {{obj.name}} is {{obj.job}} '  => [ 0: '{{obj.name}}' , 1: 'obj.name' ,index : 1, input: ' {{obj.name}} is {{obj.job}} ']
        // match.index 獲取當前匹配的 開始下標
        index = match.index
        // push text token
        // 如果 {{ }}的前面存在 靜態的文字   如   (空格..{{xx}} xxx {{}})那麼需要將這些靜態文字儲存
        if (index > lastIndex) {
            
            rawTokens.push(tokenValue = text.slice(lastIndex, index))
            // 將靜態文字儲存在 tokens 
            tokens.push(JSON.stringify(tokenValue))
        }
        // tag token
        // 解析過濾器
        const exp = parseFilters(match[1].trim())
        //生成當前引數在Vue render中獲取響應式資料的方法  _s('obj.name')  => this['obj.name']
        tokens.push(`_s(${exp})`)

    }
   
}
複製程式碼

發現 其也是通過const exp = parseFilters(match[1].trim()) 去處理 {{}}中的額過濾器。

render階段

我們發現在編譯節點如果遇到過濾器 會將其編譯成 _f(){}的表示式

"_f("filterB")(_f("filterA")(message + 'xxx|bbb' + (a||b) + `cccc`),arg1,arg2)"
複製程式碼
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
}

複製程式碼

其還是通過resolveAsset去獲取 vm.$options的filters中相同的過濾器

image

然後將 _f("filterA")(message + 'xxx|bbb' + (a||b) + cccc),arg1,arg2 作為入參。

重點:

  1. 過濾器的解析方法 parseFilters()

最後

更多部落格,檢視 github.com/imisou/vue.…

作者:HardStone

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)