Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

为什么 filter 里的 this 绑定的不是 Vue ? #42

Open
Jouryjc opened this issue Apr 20, 2019 · 0 comments
Open

为什么 filter 里的 this 绑定的不是 Vue ? #42

Jouryjc opened this issue Apr 20, 2019 · 0 comments

Comments

@Jouryjc
Copy link
Owner

Jouryjc commented Apr 20, 2019

最近工作中遇到一个问题,就是 filter 中的函数无法使用绑定在 Vue.prototype 的函数。都知道,在 createdmountedmethods 中, this 绑定的都是当前 Vue 实例。偏偏 filter 函数 this 指向的是 Window

直接上 🌰

<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,打印出来的结果如下:
this 绑定

可以看到,都是全局 window 对象。下面就进入 filter 的源码分析一下为什么没有绑定当前 Vue 实例。我们从模板编译开始看。编译入口这里省略,想要了解的童鞋可以点击链接查看。直接来到 src\compiler\parser\html-parser.jsparseHTML 函数,这里会遍历整个模板,filter 属于文本部分:

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。到这里我们就拿到了 🌰 中的文本内容 {{ 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.js

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:,我们最后得到的表达式:

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

_f 是什么呢?我们一起来分析下。上述代码中,parseFilters 函数就是我们这节的关键,它定义在 src\compiler\parser\filter-parser.js文件中:

/**
 * 处理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 了!

欢迎纠正错误!更多内容请乘坐 🚀 前往!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant