我的编程空间,编程开发者的网络收藏夹
学习永远不晚

一文带你吃透Vue3编译原理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

一文带你吃透Vue3编译原理

一直对编译原理的东西都有一种恐惧感,感觉太难了,看不懂,打开vue3源码看到编译相关的代码,直接吓退。直到我学习了大崔哥的mini-vue,so ga ~

主要流程

现在我们就来一起分析一个简易的vue3的编译原理。一句话概括一下我们想要实现的功能,那就是将template模板生成我们想要的render函数即可。简单的一句话却蕴含着大量的知识。

<div>hi, {{message}}</div> 

最后生成

import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, "hi, " + _toDisplayString(_ctx.message), 1 ))
}

首先template会通过词法分析、语法分析解析成AST(抽象语法树),然后利用transformAST进行优化,最后通过generate模块生成我们想要的render函数。

vue3的源码中主要分成了3个部分(以下是简化后的源码)

export function baseCompile(template){
  const ast = baseParse(template)
  transform(ast)
  return generate(ast)
}
  • 通过parsetemplate生成ast
  • 通过transform优化ast
  • 通过generate生成render函数

由于这3个部分牵扯的东西比较多,我们这篇文章主要来讲解一下parse的实现(友情提示:为了让大家刚好的理解,本文的代码全部都是精简过得哦)

parse的实现

我们就拿一个简单的例子入手

<div><p>hi</p>{{message}}</div>

看似一个简单的例子,其实3种类型:elementtext、插值。我们将这三种类型用枚举定义一下。

const enum NodeTypes {
  ROOT,
  INTERPOLATION,
  SIMPLE_EXPRESSION,
  ELEMENT,
  TEXT
}

ROOT类型表示根节点,SIMPLE_EXPRESSION类型表示插值的内容。最后我们想要通过parse生成一个ast

{
    type: NodeTypes.ROOT
    children: [
        {
          type: NodeTypes.ELEMENT,
          tag: "div",
          children: [
            {
              type: NodeTypes.ELEMENT,
              tag: "p",
              children: [
                {
                  type: NodeTypes.TEXT,
                  content: "hi"
                }
              ]
            },
            {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "message"
              }
            }
          ]
        }
    ]
}

基于源码我们可以知道ast是由函数baseParse生成。那我们就从这个函数入手。

baseParse

export function baseParse(content: string) {
  const context = createParseContext(content)
  return createRoot(parserChildren(context, []))
}

function createParseContext(content: string) {
  return {
    source: content
  }
}

function createRoot(children) {
  return {
    children,
    type: NodeTypes.ROOT
  }
}

首先创建一个全局的上下文对象context,并且存储了sourcesource就是我们传入的模板内容。接着创建根节点,包含了typechildren。而children是由parseChildren创建。

parseChildren

function parseChildren(context, ancestors) {
  const nodes: any = []

  while (!isEnd(context, ancestors)) {
    const s = context.source
    let node
    if (s.startsWith("{{")) {
      node = parseInterpolation(context)
    } else if (s[0] === "<") {
      if (/[a-z]/i.test(s[1])) {
        node = parseElement(context, ancestors)
      }
    } else {
      node = parseText(context)
    }
    nodes.push(node)
  }
  return nodes
}

parseChildren是负责解析子节点并创建ast节点数组。parseChildren是自顶向下分析各个子节点的,对于模板内容要从左到右依次解析。每当碰到一个element节点都要递归的调用parseChildren去解析它的子节点。当碰到{{则认为需要处理的是插值节点,当碰到<则认为需要处理的是element节点,其余的则统一认为处理的是text节点。每处理完一个节点都会生成nodepushnodes中,最后返回nodes当做是父ast节点的children属性。

当然从左到右依次循环解析就一定要有一个退出循环的条件isEnd

function isEnd(context, ancestors) {
  const s = context.source

  if (s.startsWith("</")) {
    for (let i = 0; i < ancestors.length; i++) {
      const tag = ancestors[i]
      if (startsWithEndTagOpen(s, tag)) {
        return true
      }
    }
  }

  return !s
}
function startsWithEndTagOpen(source, tag) {
  return (
    source.startsWith("</") &&
    source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
  )
}

ancestors表示element标签的集合,大致的意思就是当碰到了结束标识符</,并且结束标签(source.slice(2, 2 + tag.length))和element标签的集合中的标签匹配则说明当前的element节点处理完毕,则退出循环

下面我们就来看一下插值节点parseInterpolationelement节点parseElement和文本节点parseText分别是怎么处理的

parseInterpolation

function parseInterpolation(context) {
  const openDelimiter = "{{"
  const closeDelimiter = "}}"

  const closeIndex = context.source.indexOf(
    closeDelimiter,
    openDelimiter.length
  )

  advanceBy(context, openDelimiter.length)

  const rawContentLength = closeIndex - openDelimiter.length

  const rawContent = parseTextData(context, rawContentLength)

  const content = rawContent.trim()
  advanceBy(context, closeDelimiter.length)

  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      content
    }
  }
}

function advanceBy(context: any, length: number) {
  context.source = context.source.slice(length)
}

function parseTextData(context: any, length) {
  const content = context.source.slice(0, length)

  advanceBy(context, content.length)
  return content
}

我们主要是为了获取插值的内容然后返回一个插值对象即可。closeIndex表示“}}”所在的位置。advanceBy函数的功能是推进。比如"{{"是不需要处理的,那么就直接把它截取掉。rawContentLength代表“{{”和“}}”中间内容的长度,通过parseTextData获取“{{”和“}}”中间的内容,并返回。然后把中间内容的部分做推进。由于我们写代码习惯可能会给内容的前后做留白,所以需要用trim做处理。然后把最后的“}}”推进,返回一个插值类型的对象即可。

parseElement

function parseElement(context, ancestors) {
  const element: any = parseTag(context, TagType.Start)
  ancestors.push(element)
  element.children = parseChildren(context, ancestors)
  ancestors.pop()

  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End)
  } else {
    throw new Error(`缺少结束标签: ${element.tag}`)
  }

  return element
}

function parseTag(context: any, type: TagType) {
  const match: any = /^<\/?([a-z]*)/i.exec(context.source)
  const tag = match[1]
  advanceBy(context, match[0].length)
  advanceBy(context, 1)

  if (type === TagType.End) return

  return {
    type: NodeTypes.ELEMENT,
    tag
  }
}

function startsWithEndTagOpen(source, tag) {
  return (
    source.startsWith("</") &&
    source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
  )
}

parseElement第二个参数ancestors是一个数组来收集标签的(作用在上面的isEnd已经提到了)。通过parseTag获取标签名,parseTag通过正则拿到标签名然后返回一个标签对象,处理过的内容继续做推进。如果是结束标签则什么都不做。然后通过parseChildren递归的处理element的子节点。然后对结束标签进行处理,startsWithEndTagOpen判断是够存在结束标签,如果不存在则报错。

parseText

function parseText(context: any): any {
  let endIndex = context.source.length
  let endToken = ["<", "{{"]

  for (let i = 0; i < endToken.length; i++) {
    const index = context.source.indexOf(endToken[i])
    if (index !== -1 && endIndex > index) {
      endIndex = index
    }
  }

  const content = parseTextData(context, endIndex)

  return {
    type: NodeTypes.TEXT,
    content
  }
}

endIndex表示内容长度(此时内容的长度是已经推进过的字符到最后一个字符的长度)。比如

<div>hi,{{message}}</div> 

能够进入到parseText函数中说明开始标签已经处理过了,所以context.source应该是

hi,{{message}}</div>

所以endIndex的长度应该是上面代码的长度。当碰到”<“或者”{{“的时候,则我们需要改变endIndex的值,比如上面的代码,我们想要拿到的文本内容应该是hi,,所以当碰到”{{“时,改变endIndex然后通过parseTextData拿到文本内容,返回一个文本对象。

总结

parse的作用就是将template生成ast对象。则需要对template从左到右依次处理,处理过了则进行推进,碰到element标签还需要递归处理,并把添加到element.children上,最终返回一个ast抽象语法树。

以上就是一文带你吃透Vue3编译原理的详细内容,更多关于Vue3编译原理的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

一文带你吃透Vue3编译原理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

一文带你吃透Vue3编译原理

一直对编译原理的东西都有一种恐惧感,感觉太难了,看不懂,打开vue3源码看到编译相关的代码,直接吓退。不要担心,小编今天带你一文吃透Vue3编译原理
2023-02-09

一文带你吃透Java中的String类

在Java中,字符串是一种常见的数据类型,经常用于存储一些文本信息,而String类则是Java提供的专门用于字符串操作的类,本文就来和大家聊聊String类的常用方法与实现原理吧
2023-05-19

【Spring】一文带你吃透AOP面向切面编程技术(上篇)

个人主页: 几分醉意的CSDN博客_传送门 文章目录 💖AOP概念✨AOP作用✨AOP术语✨什么时候需要用AOP 💖Aspectj框架介绍✨Aspectj的5个通知注解✨Aspectj切入
2023-08-30

一文带你吃透Python中的os和sys模块

os 模块是 Python中的一个内置模块,也是 Python中整理文件和目录最为常用的模块。sys 模块主要负责与 Python 解释器进行交互,该模块提供了一系列用于控制 Python 运行时环境的不同部分(函数和变量等)。本文主要来聊聊这两个模块的使用,希望对大家有所帮助
2023-02-23

一文吃透Go的内置RPC原理

这篇文章主要为大家详细介绍了Go语言中内置RPC的原理。说起 RPC 大家想到的一般是框架,Go 作为编程语言竟然还内置了 RPC,着实让我有些吃鲸,本文就来一起聊聊吧
2023-03-03

一文带你深入理解Vue3响应式原理

响应式就是当对象本身(对象的增删值)或者对象属性(重新赋值)发生变化时,将会运行一些函数,最常见的就是render函数,下面这篇文章主要给大家介绍了关于Vue3响应式原理的相关资料,需要的朋友可以参考下
2022-11-13

一文带你吃透什么是PHP中的序列化

在 PHP 中,序列化是将数据结构或对象转换为可以存储或传输的字符串表示的过程。本文将通过一些简单的示例为大家介绍一下PHP序列化的相关知识,需要的可以参考一下
2023-05-18

一文带你吃透Python中的日期时间模块

Python 提供了 日期和时间模块用来处理日期和时间,还可以用于格式化日期和时间等常见功能。这篇文章就来带大家了解一下它的使用,需要的可以参考一下
2023-02-23

一文带你吃透C#中面向对象的相关知识

这篇文章主要为大家详细介绍了C#中面向对象的相关知识,文中的示例代码讲解详细,对我们学习C#有一定的帮助,需要的小伙伴可以参考一下
2023-02-26

一文带你吃透数据库的约束,不做CRUD程序员

在SQL标准中,一共规定了6种不同的约束,包括非空约束,唯一约束和检查约束等,而在MySQL中是不支持检查约束的,所以这篇文章先对其余5种约束做一个详解和练习。 文章目录 1. 约束的概念 2. 约束的分类 3. 非空约束
2023-08-22

一文带你吃透JSP增删改查实战案例详细解读

这篇文章主要为大家详细介绍了JSP中增删改查实战案例的相关知识,文中的示例代码讲解现象,具有一定的借鉴价值,感兴趣的小伙伴可以了解一下
2023-03-21

一文带你吃透JSP,增删改查实战案例详细解读

文章目录 前言JSP 概述JSP快速入门搭建环境导入JSP依赖创建 JSP 页面编写代码测试 JSP原理JSP 脚本实战案例JSP缺点发展阶段EL 表达式概述实战案例 域对象JSTL 标签用法1用法2 前言 不得不说
2023-08-17

如何使用JDBC操作数据库?一文带你吃透JDBC规范

文章目录 1. 前言2. JDBC 概述2.1 概念2.2 优点 3. JDBC 快速入门4. JDBC API详解4.1 DriverManager4.1.1 注册驱动4.1.2 获取连接 4.2 Connection4
2023-08-17

一文搞懂vue编译器(DSL)原理

本文主要介绍了一文搞懂vue编译器(DSL)原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
2023-05-18

一文带你了解MySQL之连接原理

目录一、连接简介1.1 连接的本质1.2 连接过程简介1.3 内连接和外连接1.4 左外连接1.5 右外连接1.6 内连接小结二、连接的原理2.1 嵌套循环连接(Nested-Loop Join)2.2 使用索引加快连接速度2.3 基于块的
2023-05-22

一文带你彻底剖析Java中Synchronized原理

Synchronized是Java中的隐式锁,它的获取锁和释放锁都是隐式的,完全交由JVM帮助我们操作,在了解Synchronized关键字之前,首先要学习的知识点就是Java的对象结构,本文介绍的非常详细,需要的朋友可以参考下
2023-05-18

编程热搜

目录