爲何 filter 裏的 this 綁定的不是 Vue ?

最近工做中遇到一個問題,就是 filter 中的函數沒法使用綁定在 Vue.prototype 的函數。都知道,在 createdmountedmethods 中, this 綁定的都是當前 Vue 實例。恰恰 filter 函數 this 指向的是 Windowhtml

直接上例子:git

<div id="app">
    {{ myArr | filtersOdd | filtersOverFour }}
</div>
複製代碼
Vue.filter('filtersOverFour', function (v) {
    console.log('全局的filter的this:');
    console.log(this);
    return v.filter(item => item > 4);
});

new Vue({
    el: '#app',

    data: function () {
        return {
            myArr: [1,2,3,4,5,6,7,8,9,10]
        };
    },

    filters: {
        filtersOdd: function (arr) {
            console.log('局部filter的this:');
            console.log(this);
            return arr.filter(item => !(item % 2));
        }
    }
})
複製代碼

上面的代碼咱們註冊了一個全局 filter 和一個局部 filter,打印出來的結果以下: github

this 綁定

能夠看到,都是全局 window 對象。下面就進入 filter 的源碼分析一下爲何沒有綁定當前 Vue 實例。咱們從模板編譯開始看。編譯入口這裏省略,想要了解的童鞋能夠點擊連接查看。直接來到 src\compiler\parser\html-parser.jsparseHTML 函數,這裏會遍歷整個模板,filter 屬於文本部分:express

let text, rest, next
if (textEnd >= 0) {
    rest = html.slice(textEnd)
    while (
        !endTag.test(rest) &&
        !startTagOpen.test(rest) &&
        !comment.test(rest) &&
        !conditionalComment.test(rest)
    ) {
        // < in plain text, be forgiving and treat it as text
        next = rest.indexOf('<', 1)
        if (next < 0) break
        textEnd += next
        rest = html.slice(textEnd)
    }
    text = html.substring(0, textEnd)
    advance(textEnd)
}

if (textEnd < 0) {
    text = html
    html = ''
}

if (options.chars && text) {
    options.chars(text)
}
複製代碼

判斷 textEnd 是否大於 0,是的話說明當前位置到 textEnd 都是文本內容。而且若是 < 是純文本中的字符,就繼續找到真正的文本結束的位置,而後前進到結束的位置。接着判斷 textEnd 是否小於零,是的話則說明整個 template 解析完畢了,把剩餘的 html 都賦值給了 text。到這裏咱們就拿到了 :chestnut: 中的文本內容 {{ myArr | filtersOdd | filtersOverFour }}。接下來執行 chars 回調,這個函數在 src\compiler\parser\index.js數組

chars (text: string) {

    // 若是沒有父節點
    if (!currentParent) {
        if (process.env.NODE_ENV !== 'production') {

            // 只有template時報錯
            if (text === template) {
                warnOnce(
                    'Component template requires a root element, rather than just text.'
                )
            } else if ((text = text.trim())) {
                warnOnce(
                    `text "${text}" outside root element will be ignored.`
                )
            }
        }
        return
    }
    // IE textarea placeholder bug
    /* istanbul ignore if */
    if (isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
       ) {
        return
    }
    const children = currentParent.children
    text = inPre || text.trim()
        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
    // only preserve whitespace if its not right after a starting tag
    : preserveWhitespace && children.length ? ' ' : ''
    if (text) {
        let res
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
            children.push({
                type: 2,  // 包含表達式的文本
                expression: res.expression,
                tokens: res.tokens,
                text
            })
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
            children.push({
                type: 3,  // 純文本
                text
            })
        }
    }
}
複製代碼

上面代碼先對一些特殊狀況作判斷,好比文本是否直接寫在 template 中,是否是 placeholder 的文本、是否是 script 或者 style 裏面的文本等等。執行完判斷若是不是空字符串且包含表達式,執行 parseText 函數,定義在 src\compiler\parser\text-parser.jsbash

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

defaultTagRE 匹配兩個大括號中間的內容。而後再循環匹配文本,遇到普通文本就 push 到 rawTokenstokens 中,若是是表達式就轉換成 _s(${exp}) push 到 tokens 中,以及轉換成 {@binding:exp} push 到 rawTokens 中。對於咱們這個:chestnut:,咱們最後獲得的表達式:app

{
    expression: [""\n        "", "_s(_f("filtersOverFour")(_f("filtersOdd")(myArr)))", ""\n    ""],
    tokens: ["↵ ", {@binding: "_f("filtersOverFour")(_f("filtersOdd")(myArr))"}, "↵ "]
}
複製代碼

_f 是什麼呢?咱們一塊兒來分析下。上述代碼中,parseFilters 函數就是咱們這節的關鍵,它定義在 src\compiler\parser\filter-parser.js文件中:ide

/** * 處理text中的filters * @param {String} exp - 字符文本 * @return {String} expression - 處理完filters後的函數 */
export function parseFilters (exp: string): string {
  // ...
  // 循環文本表達式
  for (i = 0; i < exp.length; i++) {
    // ...
  }

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

  /** * 將全部filters處理函數推入到filters數組中 */
  function pushFilter () {
    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
    lastFilterIndex = i + 1
  }
  // 遍歷filters全部處理函數,依次包裝。轉換成_f
  if (filters) {
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i])
    }
  }

  // 有兩個filters處理函數生成的表達式 "_f("filtersOverFour")(_f("filtersOdd")(myArr))"
  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}`
  }
}

複製代碼

按照:chestnut:,parseFilters 的做用就是把整個文本表達式轉化成 _f("filtersOverFour")(_f("filtersOdd")(myArr)) 。_f 定義在 src\core\instance\render-helpers\index.js:函數

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

resolveFilter 定義在 src\core\instance\render-helpers\resolve-filter.js源碼分析

/** * 獲取filter對象中對應id的函數 * @param {String} id - 函數名 * @returns {Function} - 函數名是id的函數 */
export function resolveFilter (id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}
複製代碼

filter函數的獲取

截圖是 this.$options 對象,能夠看到:全局 filter 是掛在實例 filters 屬性原型中的。

生成執行代碼階段就不詳細分析了,最後生成的 render 函數代碼:

with(this){return _c('div',{attrs:{"id":"app"}},[_v("\n "+_s(_f("filtersOverFour")(_f("filtersOdd")(myArr)))+"\n ")])}
複製代碼

最後在調用 vm._render 函數時會執行_f 函數。至此,filter 的流程就走完了。下面經過一個簡單的:chestnut:來還原一下上面的場景:

// 至關於 render 函數
var withFn = (function() {
  with (this) {
    console.log(this);

    b();
  }
})

// 至關於_f函數
function b () {
  console.log(this);
}

// 至關於 vm._renderProxy
var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
  }
});

withFn.call(obj);
// 輸出結果:
// Proxy {}
// Window {}

複製代碼

這就回答了 filter 函數 this 爲何指向的是 Window 了!

歡迎糾正錯誤!更多內容請 前往博客!!!

相關文章
相關標籤/搜索