vue2-过滤器

过滤器的原理

{{message | capitalize}}
这个过滤器会被模板编译成下面的样子
_s(_f("capitalize")(message))

_f函数是resolveFilter的别名。作用是从this.$options.filters中找到注册的过滤器并返回

this.$optioins.filters['capitalize']就是我们这次的capitalize过滤器函数

filters:{
  capitalize: function(value){
    if(!value)return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

因此 _f("capitalize")(message)其实就是执行过滤器并传递了参数。

_s函数其实是toString函数的别名。

export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

简单来说就是执行 过滤器函数 并传递 参数, 接着 capitalize 过滤器 处理后的结果 当做参数 传递给toString函数, toString函数执行后结果会保存到Vnode的text属性中。

串联过滤器

{{message | capitalize | suffix}}

本地 过滤器

filters:{
  capitalize: function(value){
    if(!value)return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  },
  suffix: funtion(value, symbol = '~'){
    return value + symbol
  }
}

模板编译阶段会编译成下面的样子
_s(_f("suffix")(_f("capitalize")(message)))

过滤器接受参数
{{message | capitalize| suffix('!')}}
编译后
_s(_f("suffix")(_f("capitalize")(message), '!'))

resolveFilter 的原理

resolveFilter 其实就是 调用改函数查找过滤器,找到了返回,如果没找到,方法identity(该函数方法同参数的值)

// export const identity = (_: any) => _
export function resolveFilter(id: string): Function {
    return resolveAsset(this.$options, 'filters', id, true) || identity
}

resolveAsset 函数可以查找 组件、指令、过滤器

// 可以 查找 组件、指令、过滤器
export function resolveAsset(
    options: Object,
    type: string,
    id: string,
    warnMissing ? : boolean
): any {
    /* istanbul ignore if */
    // id类型 必须是字符串类型
    if (typeof id !== 'string') {
        return
    }
    // 声明 assets 并将 options[typs]保存
    const assets = options[type]
        // 检查assets 自身是否有 id属性
    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]
        // 都没找到 再 循环一遍 ,检查原型链
        // 查找原型链 只需要 访问属性即可
    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
}

解析过滤器

我们在知道过滤器内部如何执行之后,再来了解一下,过滤器语法是如何编译的。
vue.js内部 src\compiler\parser\filter-parser.js 就是专门用来 将模板解析成过滤器函数 调用表达式的。

/* @flow */

const validDivisionCharRE = /[\w).+\-_$\]]/

//  "message | filterA | filterB(age)"
export function parseFilters(exp: string): string {
    let inSingle = false // 单引号 '
    let inDouble = false // 双引号 "
    let inTemplateString = false // `符号
    let inRegex = false // 正则符号 /
    let curly = 0 // {}
    let square = 0 // []
    let paren = 0 // ()
    let lastFilterIndex = 0
    let c, prev, i, expression, filters

    // 按字符串 遍历 exp  类似于 "message | filterA | filterB(age)"
    for (i = 0; i < exp.length; i++) {
        // 保存上一次 的 字符
        prev = c
            // 获取当前 字符 比如第一次  m 
        c = exp.charCodeAt(i)

        // 如果 在 inSingle 为true 的情况下 
        if (inSingle) {
            //  碰到 当前 c 为双引号,上一次字符 不是 \ 转义符   把 inSingle 置为 false
            //  上一次碰到过 单引号 ,这次又碰到 单引号 。 把 标识符inSingle 改为false。 之后的字符不属于 单引号中了
            if (c === 0x27 && prev !== 0x5C) inSingle = false
        } else if (inDouble) {
            //  碰到 当前 c 为双引号,上一次字符 不是 \ 转义符
            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 && // 当前字符是  | 
            exp.charCodeAt(i + 1) !== 0x7C && // 下一个字符不是 |
            exp.charCodeAt(i - 1) !== 0x7C && // 上一个字符也不是 |
            !curly && !square && !paren // 不在 {} () [] 中
        ) {
            // 第一次 expression 参数 不存在的情况下, 那么 | 前面的是 参数
            if (expression === undefined) {
                // first filter, end of expression
                // lastFilterIndex 设置为下一个字符
                lastFilterIndex = i + 1
                    // expression 为 0 到 当前位置 如 message 
                expression = exp.slice(0, i).trim()
            } else {
                // 不是第一次的情况,| 前面的是 filter方法。  保存当前的 filter方法 比如filterA
                pushFilter()
            }
        } else {
            // 上面情况都不符合的情况下 比如刚开始 c 为 "
            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
                }
                // 如果这个字符不存在,前面没有其他字符 刚开始 正则
                // 或者 是第二个正则符号  /, 且 挨着的第一个符号 不符合正则的规范
                // 那么 设置 inRegex,是正则 开始
                if (!p || !validDivisionCharRE.test(p)) {
                    inRegex = true
                }
            }
        }
    }

    // 如果把 exp 循环完之后 没有 expression 比如没有 message。 (没有遇到|)
    if (expression === undefined) {
        // expression参数 就是 自己本身
        expression = exp.slice(0, i).trim()
    } else if (lastFilterIndex !== 0) {
        // lastFilterIndex 不为0 。说明 前面遇到过 | 。 那么把最后的 
        // 循环 完之后 | 之后 的filter 保存 到filters 数组中 
        pushFilter()
    }


    // 获取 多个 filter 方法 ,并存入 filtes数组 
    function pushFilter() {
        (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
        lastFilterIndex = i + 1
    }

    // 如果有filter方法 。遍历
    if (filters) {
        for (i = 0; i < filters.length; i++) {
            // 分别调用 wrapFilter 传入上次处理后的expession作为参数,并传入filter方法
            expression = wrapFilter(expression, filters[i])
        }
    }

    // 返回最后的结果
    return expression
}

function wrapFilter(exp: string, filter: string): string {
    // filter中可能也有参数 比如 filterB(age)
    const i = filter.indexOf('(')
    if (i < 0) {
        // 如果没有 (, 那么 没有参数 直接返回 过滤器表达式
        // _f: resolveFilter
        return `_f("${filter}")(${exp})`
    } else {
        // 如果有参数 获取 name 为filterB
        // args 为 参数 age 
        const name = filter.slice(0, i)
        const args = filter.slice(i + 1)
            // 返回过滤器表达式  
            // 如果args 为 )  说明 有() 当时没有参数
        return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
    }
}

总结: 过滤器除了基本使用方式外,还能让串联并接受参数。

过滤器的原理: 在编译阶段将过滤器编译成函数调用, 串联的过滤器编译后是一个嵌套的函数调用, 前一个过滤器函数的执行结果是后一个过滤器函数的参数。

编译后的_f函数是resolveFilter函数的别名,作用是找到对应的过滤器并返回。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容