Vue源码系列讲解——过滤器篇【三】(解析过滤器)

目录

1. 前言

2. 在何处解析过滤器

3. parseFilters函数分析

4. 小结


1. 前言

在上篇文章中我们说了,无论用户是以什么方式使用过滤器,终归是将解析器写在模板中,既然是在模板中,那它肯定就会被解析编译,通过解析用户所写的模板,进而解析出用户所写的过滤器message | filterA | filterB中哪部分是被处理的表达式,哪部分是过滤器id及其参数。

还记得我们在介绍模板编译篇的解析阶段中说过,用户所写的模板会被三个解析器所解析,分别是HTML解析器parseHTML、文本解析器parseText和过滤器解析器parseFilters。其中HTML解析器是主线,在使用HTML解析器parseHTML函数解析模板中HTML标签的过程中,如果遇到文本信息,就会调用文本解析器parseText函数进行文本解析;如果遇到文本中包含过滤器,就会调用过滤器解析器parseFilters函数进行解析。在之前的文章中,我们只对HTML解析器parseHTML和文本解析器parseText其内部原理进行了分析,没有分析过滤器解析器parseFilters,那么本篇文章就来分析过滤器解析器的内部原理。

2. 在何处解析过滤器

我们一再强调,过滤器有两种使用方式,分别是在双花括号插值中和在 v-bind 表达式中,如下:

<!-- 在双花括号中 -->
{{ message | capitalize }}<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

两种不同的使用方式唯一的区别是将过滤器写在不同的地方,既然有两种不同的地方可以书写过滤器,那解析的时候必然要在这两种不同地方都进行解析。

  • 写在 v-bind 表达式中

    v-bind 表达式中的过滤器它属于存在于标签属性中,那么写在该处的过滤器就需要在处理标签属性时进行解析。我们知道,在HTML解析器parseHTML函数中负责处理标签属性的函数是processAttrs,所以会在processAttrs函数中调用过滤器解析器parseFilters函数对写在该处的过滤器进行解析,如下:

    function processAttrs (el) {// 省略无关代码...if (bindRE.test(name)) { // v-bind// 省略无关代码...value = parseFilters(value)// 省略无关代码...}// 省略无关代码...
    }
    

  • 写在双花括号中

    在双花括号中的过滤器它属于存在于标签文本中,那么写在该处的过滤器就需要在处理标签文本时进行解析。我们知道,在HTML解析器parseHTML函数中,当遇到文本信息时会调用parseHTML函数的chars钩子函数,在chars钩子函数内部又会调用文本解析器parseText函数对文本进行解析,而写在该处的过滤器它就是存在于文本中,所以会在文本解析器parseText函数中调用过滤器解析器parseFilters函数对写在该处的过滤器进行解析,如下:

    export function parseText (text,delimiters){// 省略无关代码...const exp = parseFilters(match[1].trim())// 省略无关代码...
    }
    

现在我们已经知道了过滤器会在何处进行解析,那么接下来我们就来分析过滤器解析器parseFilters函数,来看看其内部是如何对过滤器进行解析的。

3. parseFilters函数分析

parseFilters函数的定义位于源码的src/complier/parser/filter-parser.js文件中,其代码如下:

export function parseFilters (exp) {let inSingle = false                     // exp是否在 '' 中let inDouble = false                     // exp是否在 "" 中let inTemplateString = false             // exp是否在 `` 中let inRegex = false                      // exp是否在 \\ 中let curly = 0                            // 在exp中发现一个 { 则curly加1,发现一个 } 则curly减1,直到culy为0 说明 { ... }闭合let square = 0                           // 在exp中发现一个 [ 则curly加1,发现一个 ] 则curly减1,直到culy为0 说明 [ ... ]闭合let paren = 0                            // 在exp中发现一个 ( 则curly加1,发现一个 ) 则curly减1,直到culy为0 说明 ( ... )闭合let lastFilterIndex = 0let c, prev, i, expression, filtersfor (i = 0; i < exp.length; i++) {prev = cc = 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 && // pipeexp.charCodeAt(i + 1) !== 0x7C &&exp.charCodeAt(i - 1) !== 0x7C &&!curly && !square && !paren) {if (expression === undefined) {// first filter, end of expressionlastFilterIndex = i + 1expression = 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 - 1let p// find first non-whitespace prev charfor (; 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()}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
}function wrapFilter (exp: string, filter: string): string {const i = filter.indexOf('(')if (i < 0) {// _f: resolveFilterreturn `_f("${filter}")(${exp})`} else {const name = filter.slice(0, i)const args = filter.slice(i + 1)return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`}
}

该函数的作用的是将传入的形如'message | capitalize'这样的过滤器字符串转化成_f("capitalize")(message),接下来我们就来分析一下其内部逻辑。

在该函数内部,首先定义了一些变量,如下:

let inSingle = false
let inDouble = false
let inTemplateString = false
let inRegex = false
let curly = 0
let square = 0
let paren = 0
let lastFilterIndex = 0

  • inSingle:标志exp是否在 ' ... ' 中;
  • inDouble:标志exp是否在 " ... " 中;
  • inTemplateString:标志exp是否在 ` ... ` 中;
  • inRegex:标志exp是否在 \ ... \ 中;
  • curly = 0 : 在exp中发现一个 { 则curly加1,发现一个 } 则curly减1,直到culy为0 说明 { ... }闭合;
  • square = 0:在exp中发现一个 [ 则curly加1,发现一个 ] 则curly减1,直到culy为0 说明 [ ... ]闭合;
  • paren = 0:在exp中发现一个 ( 则curly加1,发现一个 ) 则curly减1,直到culy为0 说明 ( ... )闭合;
  • lastFilterIndex = 0:解析游标,每循环过一个字符串游标加1;

接着,从头开始遍历传入的exp每一个字符,通过判断每一个字符是否是特殊字符(如',",{,},[,],(,),\,|)进而判断出exp字符串中哪些部分是表达式,哪些部分是过滤器id,如下:

for (i = 0; i < exp.length; i++) {prev = cc = 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 && // pipeexp.charCodeAt(i + 1) !== 0x7C &&exp.charCodeAt(i - 1) !== 0x7C &&!curly && !square && !paren) {if (expression === undefined) {// first filter, end of expressionlastFilterIndex = i + 1expression = 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 - 1let p// find first non-whitespace prev charfor (; 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()
}function pushFilter () {(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())lastFilterIndex = i + 1
}

可以看到,虽然代码稍微有些长,但是其逻辑非常简单。为了便于阅读,我们提供一个上述代码中所涉及到的ASCII码与字符的对应关系,如下:

0x22 ----- "
0x27 ----- '
0x28 ----- (
0x29 ----- )
0x2f ----- /
0x5C ----- \
0x5B ----- [
0x5D ----- ]
0x60 ----- `
0x7C ----- |
0x7B ----- {
0x7D ----- }

上述代码的逻辑就是将字符串exp的每一个字符都从前往后开始一个一个匹配,匹配出那些特殊字符,如',",`,{,},[,],(,),\,|

如果匹配到',",`字符,说明当前字符在字符串中,那么直到匹配到下一个同样的字符才结束,同时, 匹配 (){},[] 这些需要两边相等闭合, 那么匹配到的 | 才被认为是过滤器中的|

当匹配到过滤器中的|符时,那么|符前面的字符串就认为是待处理的表达式,将其存储在 expression 中,后面继续匹配,如果再次匹配到过滤器中的 |符 ,并且此时expression有值, 那么说明后面还有第二个过滤器,那么此时两个|符之间的字符串就是第一个过滤器的id,此时调用 pushFilter函数将第一个过滤器添加进filters数组中。举个例子:

假如有如下过滤器字符串:

message | filter1 | filter2(arg)

那么它的匹配过程如下图所示:

将上例中的过滤器字符串都匹配完毕后,会得到如下结果:

expression = message
filters = ['filter1','filter2(arg)']

接下来遍历得到的filters数组,并将数组的每一个元素及expression传给wrapFilter函数,用来生成最终的_f函数调用字符串,如下:

if (filters) {for (i = 0; i < filters.length; i++) {expression = wrapFilter(expression, filters[i])}
}function wrapFilter (exp, filter) {const i = filter.indexOf('(')if (i < 0) {return `_f("${filter}")(${exp})`} else {const name = filter.slice(0, i)const args = filter.slice(i + 1)return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`}
}

可以看到, 在wrapFilter函数中,首先在解析得到的每个过滤器中查找是否有(,以此来判断过滤器中是否接收了参数,如果没有(,表示该过滤器没有接收参数,则直接构造_f函数调用字符串即_f("filter1")(message)并返回赋给expression,如下:

const i = filter.indexOf('(')
if (i < 0) {return `_f("${filter}")(${exp})`
}

接着,将新的experssionfilters数组中下一个过滤器再调用wrapFilter函数,如果下一个过滤器有参数,那么先取出过滤器id,再取出其带有的参数,生成第二个过滤器的_f函数调用字符串,即_f("filter2")(_f("filter1")(message),arg),如下:

const name = filter.slice(0, i)
const args = filter.slice(i + 1)
return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`

这样就最终生成了用户所写的过滤器的_f函数调用字符串。

4. 小结

本篇文章介绍了Vue是如何解析用户所写的过滤器的。

首先,我们介绍了两种不同写法的过滤器会在不同的地方进行解析,但是解析原理都是相同的,都是调用过滤器解析器parseFilters函数进行解析。

接着,我们分析了parseFilters函数的内部逻辑。该函数接收一个形如'message | capitalize'这样的过滤器字符串作为,最终将其转化成_f("capitalize")(message)输出。在parseFilters函数的内部是通过遍历传入的过滤器字符串每一个字符,根据每一个字符是否是一些特殊的字符从而作出不同的处理,最终,从传入的过滤器字符串中解析出待处理的表达式expression和所有的过滤器filters数组。

最后,将解析得到的expressionfilters数组通过调用wrapFilter函数将其构造成_f函数调用字符串。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/732783.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【自动化】PyoutuGUI操作键鼠

自动化之PyoutuGUI操作键鼠 文章目录 自动化之PyoutuGUI操作键鼠  &#x1f449;引言&#x1f48e;一、初始化环境二、键盘鼠标事件三、消息框功能四、案例实战自动登录WPS 五、问题解决 &#x1f449;引言&#x1f48e; 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一…

EF类和E/F类功率放大器(能量转换器)的波形推导和理想仿真--基于Matlab和ADS

EF类和E/F类功率放大器&#xff08;能量转换器&#xff09;的波形推导和理想仿真–基于Matlab和ADS 参考论文&#xff1a;Modeling and Analysis of Class EF and Class E/F Inverters With Series-Tuned Resonant Networks(2016) 这篇文章的思路和MTT的文章A Generalized Hi…

存储引擎的简介

简介&#xff1a; 1.在mysql存储引擎可以说就是指表的类型&#xff0c;可以称为表处理器&#xff0c;以表的形式存储。 2.他的功能就是接收上层传下来的指令&#xff0c;然后对表中的数据进行提取写入操作。 目的&#xff1a; 为了管理方便&#xff0c;我们把连接管理&#xf…

如何在一个pycharm项目中创建jupyter notebook文件,并切换到conda环境中

1、第一步可以直接在pycharm项目中创建jupyter notebook文件 2、假若想要切换成pytorch环境做实验例子&#xff0c;会发现报这个错误 Jupyter server process exited with code 1 C:\Users\12430\.conda\envs\pytorch3.11\python.exe: No module named jupyter在这里&#xff…

Canvas笔记05:绘制文本,可视化图表中最常用

hello&#xff0c;我是贝格前端工场&#xff0c;最近在学习canvas&#xff0c;分享一些canvas的一些知识点笔记&#xff0c;本期分享canvas绘制文本的知识&#xff0c;欢迎老铁们一同学习&#xff0c;欢迎关注&#xff0c;如有前端项目可以私信贝格。 Canvas绘制文本是指使用H…

【粉丝福利第四期】:《低代码平台开发实践:基于React》(文末送书)

文章目录 前言一、React与低代码平台的结合优势二、基于React的低代码平台开发挑战三、基于React的低代码平台开发实践四、未来展望《低代码平台开发实践&#xff1a;基于React》五、粉丝福利 前言 随着数字化转型的深入&#xff0c;企业对应用开发的效率和灵活性要求越来越高…

PyTorch之完整的神经网络模型训练

简单的示例&#xff1a; 在PyTorch中&#xff0c;可以使用nn.Module类来定义神经网络模型。以下是一个示例的神经网络模型定义的代码&#xff1a; import torch import torch.nn as nnclass MyModel(nn.Module):def __init__(self):super(MyModel, self).__init__()# 定义神经…

运维打工人,兼职跑外卖的第二个周末

北京&#xff0c;晴&#xff0c;西南风1级。 前序 今天天气还行&#xff0c;赶紧起来&#xff0c;把衣服都洗洗&#xff0c;准备准备&#xff0c;去田老师吃饭早饭了。 一个甜饼、一个茶叶蛋、3元自助粥花费7.5。5个5挺吉利的。 跑外卖的意义 两个字减肥&#xff0c;记录刚入…

基于最小二乘递推算法的系统参数辨识matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于最小二乘递推算法的系统参数辨识。对系统的参数a1&#xff0c;b1&#xff0c;a2&#xff0c;b2分别进行估计&#xff0c;计算估计误差以及估计收敛曲线&#…

如何在Windows中对硬盘进行分区?这里有详细步骤

本文介绍如何在Windows11、10、8、7、Vista和XP中对硬盘进行分区 如果这个过程听起来比你想象的要复杂一点,不要担心,因为事实并非如此。在Windows中对硬盘进行分区一点也不难,通常只需要几分钟。以下是操作方法。 注意:这些说明适用于Windows 11、Windows 10、Windows 8…

腾讯云轻量应用服务器流量用完了怎么办?

腾讯云轻量服务器流量用完了怎么办&#xff1f;超额流量另外支付流量费&#xff0c;流量价格为0.8元/GB&#xff0c;会自动扣你的腾讯云余额&#xff0c;如果你的腾讯云账号余额不足&#xff0c;那么你的轻量应用服务器会面临停机&#xff0c;停机后外网无法访问&#xff0c;继…

js【详解】Promise

为什么需要使用 Promise &#xff1f; 传统回调函数的代码层层嵌套&#xff0c;形成回调地狱&#xff0c;难以阅读和维护&#xff0c;为了解决回调地狱的问题&#xff0c;诞生了 Promise 什么是 Promise &#xff1f; Promise 是一种异步编程的解决方案&#xff0c;本身是一个构…

自然语言处理之语言模型(LM)介绍

自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;是人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;的一个重要分支&#xff0c;它旨在使计算机能够理解、解释和生成人类语言。在自然语言处理中&#xff0c;语言模型&…

阿珊详解Vue Router的守卫机制

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【漏洞复现】Salia PLCC cPH2 远程命令执行漏洞(CVE-2023-46359)

0x01 漏洞概述 Salia PLCC cPH2 v1.87.0 及更早版本中存在一个操作系统命令注入漏洞&#xff0c;该漏洞可能允许未经身份验证的远程攻击者通过传递给连接检查功能的特制参数在系统上执行任意命令。 0x02 测绘语句 fofa&#xff1a;"Salia PLCC" 0x03 漏洞复现 ​…

video视频播放

1.列表页面 <template><div><ul><li class"item" v-for"(item,index) in list" :key"index" click"turnPlay(item.videoUrl)"><img :src"item.img" alt""><div class"btn…

套接字编程 --- 一

目录 1. 预备知识 1.1. 端口号 1.2. 认识TCP协议 1.3. 认识UDP协议 1.4. 网络字节序 2. socket 2.1. socket 常见系统调用 2.1.1. socket 系统调用 2.1.2. bind 系统调用 2.1.3. recvfrom 系统调用 2.1.4. sendto系统调用 2.3. 其他相关接口 2.3.1. bzero 2.3.2…

力扣:17. 电话号码的字母组合

力扣&#xff1a;17. 电话号码的字母组合 描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输…

Linux——文件重定向

目录 前言 一、重定向 二、重定向的运用 三、dup2 四、命令行中的重定向 五、为什么要有标准错误 前言 在之前我们学习了文件标识符&#xff0c;直到close可以使用文件标识符进行关闭&#xff0c;但是当我们关闭1号&#xff08;stdout&#xff09;时&#xff0c;无法往显…

00在linux环境下搭建stm32开发环境

文章目录 前言一、环境搭建1.arm-none-eabi-gcc2.openocd 三、创建stm32标准库工程1.创建工程目录2.修改stm32_flash.ld文件3.写makefile文件4.修改core_cm3.c5.写main函数并下载到板子上 最后 前言 我在那天终于说服自己将系统换成了linux系统了&#xff0c;当换成了linux系统…