IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue源码阅读(33):过滤器的源码解析 -> 正文阅读

[JavaScript知识库]Vue源码阅读(33):过滤器的源码解析

过滤器的官方文档可以点击这里

今天和大家说说过滤器的实现原理,主要看过滤器模板字符串的编译过滤器模板字符串编译出来的代码字符串。

我们以下面的代码为例:

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

Vue.filter('prefix', function (value) {
  return `~${value}`
})

Vue.filter('suffix', function (value, word) {
  return `${value}${word}`
})

let app = new Vue({
  el: '#app',
  data() {
    return {
      message: 'hello vue'
    }
  },
  methods: {},
  template: `
    <div id="app">
      <h3>{{message | capitalize}}</h3>
      <h3>{{message | suffix('!')}}</h3>
      <h3>{{message | capitalize | prefix}}</h3>
    </div>
  `
})

在例子中, 注册了三个全局过滤器,分别是:capitalize、prefix、suffix。

  • capitalize 过滤器的作用是:将字符串的首字母大写;
  • prefix 过滤器的作用是:给字符串的前面拼接上 "~"?字符;
  • suffix 过滤器的作用是:将传递的字符串参数拼接到目标字符串的后面;

页面的运行效果如下所示:

1,过滤器运行原理

这一小节主要看过滤器模板字符串所编译出来的代码字符串,看看在运行时,过滤器是如何实现功能的。

1-1,没有传递参数的单个过滤器

以第一个 h3 节点为例,模板字符串是

<h3>{{message | capitalize}}</h3>

最终被编译成的 render 代码字符串是

_c('h3',[_v(_s(_f("capitalize")(message)))])

这里可以发现 {{message | capitalize}} 被翻译成了 _f("capitalize")(message),这里的 _f 是 resolveFilter 方法的别名,他的作用是根据过滤器的名称获取对应的过滤器函数。所以这里的?_f("capitalize")(message) 可以看成?capitalize(message),也就是以 message 作为参数执行?capitalize 函数,并将过滤器函数执行的返回值作为?_s 函数的参数,最终的效果就是创建出来的 h3 vnode 节点的子节点是一个文本 vnode 节点,并且该文本 vnode 节点的 text 属性是 "Hello vue",可以发现 "Hello vue" 正是 "hello vue" 经过?capitalize 过滤器函数处理后的结果,这也就实现了过滤器想要的效果。

1-2,传递参数的过滤器

以第二个 h3 节点为例,模板字符串是

<h3>{{message | suffix('!')}}</h3>

最终被编译成的 render 代码字符串是

_c('h3',[_v(_s(_f("suffix")(message,'!')))])

可以发现,这里所编译出的代码字符串和 1-1 小节差不多,唯一的不同是过滤器使用了参数,并且在编译出的代码字符串中,这个使用的参数作为 suffix 过滤器函数的第二个参数,message 作为 suffix 过滤器函数的第一个参数,关于这一点,官方文档也有专门的说明,如下图所示。

1-3,多个过滤器串联

以第三个 h3 节点为例,模板字符串是

<h3>{{message | capitalize | prefix}}</h3>

最终被编译成的 render 代码字符串是

_c('h3',[_v(_s(_f("prefix")(_f("capitalize")(message))))])

通过观察可以发现,多个过滤器串联的实现原理是编译出嵌套的过滤器函数。在模板字符串中,message 变量首先经过?capitalize 过滤器处理,然后再经过?prefix 过滤器处理,因此在编译出的代码字符串中,首先以 message 作为参数执行?capitalize 过滤器函数,然后以?capitalize 过滤器函数的执行结果作为参数执行?prefix 过滤器函数,最后将?prefix 过滤器函数执行的结果作为 _s 方法的参数。

1-4,_f、resolveFilter 方法的运行原理

在上面三小节中,_f 函数非常的重要,它的作用是根据过滤器的名称获取对应的过滤器函数,_f 是?resolveFilter 方法的别名,定义如下:

// _f 函数的定义
import { resolveFilter } from './resolve-filter'

export function installRenderHelpers (target: any) {
  target._f = resolveFilter
}

所以我们主要看?resolveFilter 方法的实现,代码如下:

export function resolveFilter (id: string): Function {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}

resolveFilter 方法的内部借助?resolveAsset 方法实现功能,并且传递的参数有 this.$options、'filters'、过滤器名称,这个 this.$options 变量的作用是存储当前 Vue 实例能够使用的资源,包括指令、过滤器和组件,关于这部分知识可以看我的这篇文章。接下来看?resolveAsset 方法的实现。

resolveAsset 方法很简单,只要从 this.$options 变量中获取指定的资源,并返回出去即可,源码如下所示,看注释即可。

export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  // 如果 id 不是字符串类型的话,直接 return
  if (typeof id !== 'string') {
    return
  }
  // options 的数据结构如下所示:
  // options:{
  //   components:{},
  //   directives:{},
  //   filters:{}
  // }
  const assets = options[type]
  // 判断 assets 对象本身有没有指定的 原始id、小驼峰id、大驼峰id
  // 如果有的话,直接返回
  // 判断 assets 对象本身是否存在原始id属性,如果有的话,直接返回
  if (hasOwn(assets, id)) return assets[id]
  // 判断 assets 对象本身是否存在小驼峰id属性,如果有的话,直接返回
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  // // 判断 assets 对象本身是否存在大驼峰id属性,如果有的话,直接返回
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // 如果 assets 对象本身 原始id、小驼峰id、大驼峰id 属性都不存在的话
  // 则判断 assets 的原型链上有没有指定的 原始id、小驼峰id、大驼峰id 属性
  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
}

2,过滤器模板字符串的编译

这一小节研究过滤器模板字符串是如何编译成对应的代码字符串的。

对应的源码在?src/compiler/parser/filter-parser.js 文件中,因为在真实的源码中有很多代码是处理边界情况的,不便于理解,所以我进行了主要代码的精简,方法大家理解主体逻辑,源码如下所示:

主线逻辑就是遍历?filters 字符串数组,调用?wrapFilter 方法完成代码字符串的拼接。

function parseFilters(exp) {
  // 以 exp == "message | suffix('!')" 为例进行分析
  // 以 '|' 为基准切分 exp 字符串
  // filters == ["message ", " suffix('!')"]
  let filters = exp.split('|')
  // 弹出 filters 字符串数组的第一个元素赋值给 expression 变量,并且删除前后的空格
  // 这弹出的第一个元素就是过滤器想要处理的数据,数组中剩下的元素都是过滤器字符串和参数
  let expression = filters.shift().trim()
  // 第一个元素弹出后,filters == [" suffix('!')"]
  let i
  // 接下来进行过滤器代码字符串的拼接,调用 wrapFilter 实现功能
  if (filters) {
    // 遍历 filters 数组
    for (i = 0; i < filters.length; i++) {
      // 调用 wrapFilter 方法进行代码字符串的拼接
      // 第一个参数是当前处理过滤器的参数
      // 第二个参数是当前处理过滤器的函数名称
      //
      // wrapFilter 方法的返回值直接赋值给 expression 变量,此时的
      // expression 字符串变成了下一轮过滤器的参数
      expression = wrapFilter(expression, filters[i].trim())
    }
  }
  // 返回最终拼接好的代码字符串
  return expression
}

function wrapFilter(exp, filter) {
  // 判断当前处理的过滤器字符串有没有 '(' 字符
  // 如果有的话,说明当前的过滤器有参数,例如:"suffix('!')"
  // 如果没有的话,则当前的过滤器没有参数
  const i = filter.indexOf('(')
  if (i < 0) {
    // 如果 i < 0 的话,说明过滤器字符串中没有 '(' 字符,
    // 也就是说当前的过滤器没有使用参数
    // 此时直接 return `_f("${filter}")(${exp})`
    return `_f("${filter}")(${exp})`
  } else {
    // 如果 i 不是小于 0,则说明当前的过滤器使用了参数,此时就需要对参数进行处理。
    // 原因是因为在过滤器函数的调用中,过滤器函数要处理的目标数据(例如:message)需要作为第一个参数。
    //
    // 以 i 下标为分割点,分割 filter 字符串,例如:filter == "suffix('!')",则
    // name == "suffix",args == "'!')"
    const name = filter.slice(0, i) // 过滤器函数名称
    const args = filter.slice(i + 1) // 使用过滤器时,传入的参数
    // 接下来进行代码字符串的拼接。注意,这里 exp 作为过滤器函数调用的第一个参数
    return `_f("${name}")(${exp},${args}`
  }
}

// 测试
let filterRender1 = parseFilters(`message | suffix('!')`)
let filterRender2 = parseFilters(`message | capitalize | suffix('!')`)

console.log(filterRender1) // _f("suffix")(message,'!')
console.log(filterRender2) // _f("suffix")(_f("capitalize")(message),'!')

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-20 15:42:12  更:2021-09-20 15:44:43 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 18:53:24-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码