【Vue2.0源码学习】模板编译篇-模板解析(代码生成阶段)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【Vue2.0源码学习】模板编译篇-模板解析(代码生成阶段)

1. 前言

经过前几篇文章,我们把用户所写的模板字符串先经过解析阶段解析生成对应的抽象语法树AST,接着再经过优化阶段将AST中的静态节点及静态根节点都打上标记,现在终于到了模板编译三大阶段的最后一个阶段了——代码生成阶段。所谓代码生成阶段,到底是要生成什么代码?答:要生成render函数字符串。

我们知道,Vue实例在挂载的时候会调用其自身的render函数来生成实例上的template选项所对应的VNode,简单的来说就是Vue只要调用了render函数,就可以把模板转换成对应的虚拟DOM。那么Vue要想调用render函数,那必须要先有这个render函数,那这个render函数又是从哪来的呢?是用户手写的还是Vue自己生成的?答案是都有可能。我们知道,我们在日常开发中是可以在Vue组件选项中手写一个render选项,其值对应一个函数,那这个函数就是render函数,当用户手写了render函数时,那么Vue在挂载该组件的时候就会调用用户手写的这个render函数。那如果用户没有写呢?那这个时候Vue就要自己根据模板内容生成一个render函数供组件挂载的时候调用。而Vue自己根据模板内容生成render函数的过程就是本篇文章所要介绍的代码生成阶段。

现在我们知道了,所谓代码生成其实就是根据模板对应的抽象语法树AST生成一个函数,通过调用这个函数就可以得到模板对应的虚拟DOM

2. 如何根据AST生成render函数

通过上文我们知道了,代码生成阶段主要的工作就是根据已有的AST生成对应的render函数供组件挂载时调用,组件只要调用的这个render函数就可以得到AST对应的虚拟DOMVNode。那么如何根据AST生成render函数呢?这其中是怎样一个过程呢?接下来我们就来细细剖析一下。

假设现有如下模板:

<div id="NLRX"><p>Hello {{name}}</p></div>

该模板经过解析并优化后对应的AST如下:

ast = {
    'type': 1,
    'tag': 'div',
    'attrsList': [
        {
            'name':'id',
            'value':'NLRX',
        }
    ],
    'attrsMap': {
      'id': 'NLRX',
    },
    'static':false,
    'parent': undefined,
    'plain': false,
    'children': [{
      'type': 1,
      'tag': 'p',
      'plain': false,
      'static':false,
      'children': [
        {
            'type': 2,
            'expression': '"Hello "+_s(name)',
            'text': 'Hello {{name}}',
            'static':false,
        }
      ]
    }]
  }

下面我们就来根据已有的这个AST来生成对应的render函数。生成render函数的过程其实就是一个递归的过程,从顶向下依次递归AST中的每一个节点,根据不同的AST节点类型创建不同的VNode类型。接下来我们就来对照已有的模板和AST实际演示一下生成render函数的过程。

  1. 首先,根节点div是一个元素型AST节点,那么我们就要创建一个元素型VNode,我们把创建元素型VNode的方法叫做_c(tagName,data,children)。我们暂且不管_c()是什么,只需知道调用_c()就可以创建一个元素型VNode。那么就可以生成如下代码:
_c('div',{attrs:{"id":"NLRX"}},[/*子节点列表*/])
  1. 根节点div有子节点,那么我们进入子节点列表children里遍历子节点,发现子节点p也是元素型的,那就继续创建元素型VNode并将其放入上述代码中根节点的子节点列表中,如下:
_c('div',{attrs:{"id":"NLRX"}},[_c('p',{attrs:{}},[/*子节点列表*/])])
  1. 同理,继续遍历p节点的子节点,发现是一个文本型节点,那就创建一个文本型VNode并将其插入到p节点的子节点列表中,同理,创建文本型VNode我们调用_v()方法,如下:
_c('div',{attrs:{"id":"NLRX"}},[_c('p',{attrs:{}},[_v("Hello "+_s(name))])])
  1. 到此,整个AST就遍历完毕了,我们将得到的代码再包装一下,如下:
`
 with(this){
   reurn _c(
     'div',
     {
       attrs:{"id":"NLRX"},
     },
     [
       _c(
         'p',
         {
           attrs:{}
         },
         [
           _v("Hello "+_s(name))
         ]
       )
     ]
   )
 }
 `
  1. 最后,我们将上面得到的这个函数字符串传递给createFunction函数(关于这个函数在后面会介绍到),createFunction函数会帮我们把得到的函数字符串转换成真正的函数,赋给组件中的render选项,从而就是render函数了。如下:
res.render = createFunction(compiled.render, fnGenErrors)
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

以上就是根据一个简单的模板所对应的AST生成render函数的过程,理论过程我们已经了解了,那么在源码中实际是如何实现的呢?下面我们就回归源码分析其具体实现过程。

3. 回归源码

代码生成阶段的源码位于src/compiler/codegen/index.js 中,源码虽然很长,但是逻辑不复杂,核心逻辑如下:

export function generate (ast,option) {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
const code = generate(ast, options)

调用generate函数并传入优化后得到的ast,在generate函数内部先判断ast是否为空,不为空则调用genElement(ast, state)函数创建VNode,为空则创建一个空的元素型divVNode。然后将得到的结果用with(this){return ${code}}包裹返回。可以看出,真正起作用的是genElement函数,下面我们继续来看一下genElement函数内部是怎样的。

genElement函数定义如下:

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      const data = el.plain ? undefined : genData(el, state)
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

genElement函数逻辑很清晰,就是根据当前 AST 元素节点属性的不同从而执行不同的代码生成函数。虽然元素节点属性的情况有很多种,但是最后真正创建出来的VNode无非就三种,分别是元素节点,文本节点,注释节点。接下来我们就着重分析一下如何生成这三种节点类型的render函数的。

3.1 元素节点

生成元素型节点的render函数代码如下:

const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`

生成元素节点的render函数就是生成一个_c()函数调用的字符串,上文提到了_c()函数接收三个参数,分别是节点的标签名tagName,节点属性data,节点的子节点列表children。那么我们只需将这三部分都填进去即可。

  1. 获取节点属性data
    首先判断plain属性是否为true,若为true则表示节点没有属性,将data赋值为undefined;如果不为true则调用genData函数获取节点属性data数据。genData函数定义如下:
export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','
    // key
    if (el.key) {
        data += `key:${el.key},`
    }
    // ref
    if (el.ref) {
        data += `ref:${el.ref},`
    }
    if (el.refInFor) {
        data += `refInFor:true,`
    }
    // pre
    if (el.pre) {
        data += `pre:true,`
    }
    // 篇幅所限,省略其他情况的判断
    data = data.replace(/,$/, '') + '}'
    return data
}
  1. 我们看到,源码中genData虽然很长,但是其逻辑非常简单,就是在拼接字符串,先给data赋值为一个{,然后判断存在哪些属性数据,就将这些数据拼接到data中,最后再加一个},最终得到节点全部属性data
  2. 获取子节点列表children
    获取子节点列表children其实就是遍历ASTchildren属性中的元素,然后根据元素属性的不同生成不同的VNode创建函数调用字符串,如下:
export function genChildren (el):  {
    if (children.length) {
        return `[${children.map(c => genNode(c, state)).join(',')}]`
    }
}
function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}
  1. 上面两步完成之后,生成_c()函数调用字符串,如下:
code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`

3.2 文本节点

文本型的VNode可以调用_v(text)函数来创建,所以生成文本节点的render函数就是生成一个_v(text)函数调用的字符串。_v()函数接收文本内容作为参数,如果文本是动态文本,则使用动态文本AST节点的expression属性,如果是纯静态文本,则使用text属性。其生成代码如下:

export function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

3.3 注释节点

注释型的VNode可以调用_e(text)函数来创建,所以生成注释节点的render函数就是生成一个_e(text)函数调用的字符串。_e()函数接收注释内容作为参数,其生成代码如下:

export function genComment (comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}

4. 总结

本篇文章介绍了模板编译三大阶段的最后一个阶段——代码生成阶段。

首先,介绍了为什么要有代码生成阶段以及代码生成阶段主要干什么。我们知道了,代码生成其实就是根据模板对应的抽象语法树AST生成一个函数供组件挂载时调用,通过调用这个函数就可以得到模板对应的虚拟DOM

接着,我们通过一个简单的模板演示了把模板经过递归遍历最后生成render函数的过程。

最后,我们回归源码,通过分析源码了解了生成render函数的具体实现过程。

目录
相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
12天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
2月前
|
数据采集 自然语言处理 搜索推荐
基于qwen2.5的长文本解析、数据预测与趋势分析、代码生成能力赋能esg报告分析
Qwen2.5是一款强大的生成式预训练语言模型,擅长自然语言理解和生成,支持长文本解析、数据预测、代码生成等复杂任务。Qwen-Long作为其变体,专为长上下文场景优化,适用于大型文档处理、知识图谱构建等。Qwen2.5在ESG报告解析、多Agent协作、数学模型生成等方面表现出色,提供灵活且高效的解决方案。
199 49
|
2月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
57 12
|
1月前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
13天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
2月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
2月前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
63 3