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 = 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
    ) {
      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()
  }

  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: 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}`
  }
}

该函数的作用的是将传入的形如'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 = 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
    ) {
        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()
}

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.mfbz.cn/a/435692.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ESP32 IDF快速入门】点亮第一个LED灯与流水灯

文章目录 前言一、有哪些工作模式&#xff1f;1.1 GPIO的详细介绍1.2 GPIO的内部框图输入模式输出部分 二、GPIO操作函数2.1 GPIO 汇总2.2 GPIO操作函数gpio_config配置引脚reset 引脚函数设置引脚电平选中对应引脚设置引脚的方向 2.3 点亮第一个灯 三、流水灯总结 前言 ESP32…

【Godot4.2】GDScript数组分类及类型化数组和紧缩数组概述

概述 GDScript的数组是一种很常用的数据类型。本文主要阐述一下GDScript数组分类&#xff0c;以及官方文档和大多数视频或教程较少提及的类型化数组和紧缩数组。 GDScript数组分类 通过反复查阅GDScript内置文档并进行细节比较&#xff0c;发现GDScript的数组&#xff0c;可…

Qt for WebAssembly : Application exit (SharedArrayBuffer is not defined)

用Qt开发 WebAssembly&#xff0c;放到nginx里面&#xff0c;用127.0.0.1访问没问题&#xff0c;用局域网IP访问就提示如下&#xff1a; 总结了以下两种解决办法&#xff1a; ①&#xff1a;配置 nginx http 头 [ 支持&#xff1a;WebAssembly Qt (single-threaded) ] ②&#…

关于 selinux 规则

1. 查看selinux状态 SELinux的状态&#xff1a; enforcing&#xff1a;强制&#xff0c;每个受限的进程都必然受限 permissive&#xff1a;允许&#xff0c;每个受限的进程违规操作不会被禁止&#xff0c;但会被记录于审计日志 disabled&#xff1a;禁用 相关命令&#xf…

ElevenLabs用AI为Sora文生视频模型配音 ,景联文科技提供高质量真人音频数据集助力生成逼真音效

随着Open AI公司推出的Sora文生视频模型惊艳亮相互联网&#xff0c;AI语音克隆创企ElevenLabs又为Sora的演示视频生成了配音&#xff0c;所有的音效均由AI创造&#xff0c;与视频内容完美融合。 ElevenLabs的语音克隆技术能够从一分钟的音频样本中创建逼真的声音。为了实现这一…

LVS集群 ----------------(直接路由 )DR模式部署

一、LVS集群的三种工作模式 lvs-nat&#xff1a;修改请求报文的目标IP,多目标IP的DNAT lvs-dr&#xff1a;操纵封装新的MAC地址&#xff08;直接路由&#xff09; lvs-tun&#xff1a;隧道模式 lvs-dr 是 LVS集群的 默认工作模式 NAT通过网络地址转换实现的虚拟服务器&…

Ubuntu 22.04修改静态ip

1. 备份原网络配置文件 # 配置文件名称因机器设置有异 cd /etc/netplan cp 01-network-config.yaml 01-network-config.yaml.bak# 文件内容如下 network:version: 2renderer: NetworkManager2. 修改配置文件 使用 ipconfig 命令查看网络信息&#xff0c;ip addr 命令也可 我这…

【S32DS报错】-8-调用初始化函数Port_Init后,S32DS断开与调试器PEmicro/J-Link的连接,无法调试Debug(基于MCAL)

问题背景&#xff1a; 在S32DS IDE中&#xff0c;调用初始化函数Port_Init后&#xff0c;S32DS断开与调试器PEmicro / J-Link的连接&#xff0c;无法调试Debug&#xff1a; 问题原因&#xff1a; 调用初始化函数Port_Init时&#xff0c;MCU的JTAG接口被初始化&#xff0c;导致…

Echarts 配置项 series 中的 data 是多维度

文章目录 需求分析 需求 如下图数据格式所示&#xff0c;现要求按照该格式进行绘制折线图 分析 在绘制折线图时&#xff0c;通常我们的 series 中的 data 数据是这样的格式 option {title: {text: Stacked Area Chart},tooltip: {trigger: axis,axisPointer: {type: cross…

紧握时代契机链接亿万家庭 创维汽车2024全球经销商大会圆满召开

3月6日&#xff0c;以“极致 见新境”创维汽车2024全球经销商大会在徐州隆重举行。徐州经开区管委会副主任季洪志&#xff0c;缅甸驻华大使馆商务参赞 Win Myat Aung&#xff0c;法国中小企业联盟主席 Xavier Michon-Lehnebach&#xff0c;创维集团、创维汽车创始人黄宏生&…

【项目】图书管理系统

目录 前言&#xff1a; 项目要求&#xff1a; 知识储备&#xff1a; 代码实现&#xff1a; Main&#xff1a; Books包&#xff1a; Book&#xff1a; BookList&#xff1a; Operate包&#xff1a; Operate: addOperate: deleteOperate: exitOperate: findOperate:…

Python与FPGA——膨胀腐蚀

文章目录 前言一、膨胀腐蚀二、Python实现腐蚀算法三、Python实现膨胀算法四、Python实现阈值算法五、FPGA实现腐蚀算法总结 前言 腐蚀是指周围的介质作用下产生损耗与破坏的过程&#xff0c;如生锈、腐烂等。而腐蚀算法也类似一种能够产生损坏&#xff0c;抹去部分像素的算法。…

代码随想录算法训练营第13天

239. 滑动窗口最大值 &#xff08;一刷至少需要理解思路&#xff09; 方法&#xff1a;暴力法 &#xff08;时间超出限制&#xff09; 注意&#xff1a; 代码&#xff1a; class Solution { public:vector<int> maxSlidingWindow(vector<int>& nums, int k…

掌握 Vue3、Vite 和 SCSS 实现一键换肤的魔法步骤

前言 一个网站的换肤效果算是一个比较常见的功能&#xff0c;尤其是在后台管理系统中&#xff0c;我们几乎都能看到他的身影&#xff0c;这里给大家提供一个实现思路。 搭建项目 vitevue3搭建项目这里就不演示了&#xff0c;vite官网里面讲得很清楚。 注&#xff1a;这里使…

【YOLO v5 v7 v8 v9小目标改进】辅助超推理SAHI:分而治之,解决高分辨率图像中小物体检测的问题

辅助超推理SAHI&#xff1a;分而治之&#xff0c;解决高分辨率图像中小物体检测的问题 设计思路结构小目标涨点YOLO v5 魔改YOLO v7 魔改YOLO v8 魔改YOLO v9 魔改 论文&#xff1a;https://arxiv.org/pdf/2202.06934.pdf 代码&#xff1a;https://github.com/obss/sahi 设计思…

Java+SpringBoot+Vue+MySQL:农业管理新篇章

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

Git基础知多少

什么是Git Git 是分布式版本控制系统&#xff08;DVCS&#xff09;。它可以跟踪文件的更改&#xff0c;并允许你恢复到任何特定版本的更改。与 SVN 等其他版本控制系统&#xff08;VCS&#xff09;相比&#xff0c;其分布式架构具有许多优势&#xff0c;一个主要优点是它不依赖…

本地知识库搭建成功后,企业效率真的翻倍了

在如今这个快节奏的信息时代&#xff0c;对企业来说&#xff0c;拥有一套高效的知识管理系统早已不再是选项&#xff0c;而是必要。而本地知识库&#xff0c;它这个集信息存储、管理和查询于一体的平台&#xff0c;不仅改变了公司信息资源共享的方式&#xff0c;还帮助进一步提…

DataLoader

import torchvision from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter# 准备的测试数据集 数据放在了CIFAR10文件夹下test_data torchvision.datasets.CIFAR10("./CIFAR10",trainFalse, transformtorchvision.transfor…

Flutter性能优化

性能分析工具 &#xff08;1&#xff09;performance overlay 开启performance overlay后&#xff0c;Flutter APP上将显示一个展示一个浮层&#xff0c;浮层中会实时展示当前的UI线程及Raster线程的运行情况。如果都是蓝色竖条&#xff0c;说明界面运行流畅&#xff0c;否则则…