Vue源码学习(二):<templete>渲染第一步,模板解析
好家伙,
1.<template>去哪了
在正式内容之前,我们来思考一个问题,
当我们使用vue开发页面时,<tamplete>中的内容是如何变成我们网页中的内容的?
它会经历四步:
- 解析模板:Vue会解析 - <template>中的内容,识别出其中的指令、插值表达式(- {{}}),以及其他元素和属性。
- 生成AST:解析模板后,Vue会生成一个对应的AST(Abstract Syntax Tree,抽象语法树),用于表示模板的结构、指令、属性等信息。 
- 生成渲染函数:根据生成的AST,Vue会生成渲染函数。渲染函数是一个函数,接收一些数据作为参数,并返回一个虚拟DOM(Virtual DOM)。 
- 渲染到真实DOM:Vue执行渲染函数,将虚拟DOM转换为真实的DOM,并将其插入到页面中的指定位置。在这个过程中,Vue会根据数据的变化重新执行渲染函数,更新页面上的内容。 
所以,步骤如下:模板解析 =》AST =》生成渲染函数 =》渲染到真实DOM
2.ast语法树是什么?
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,抽象表示把js代码进行了结构化的转化,转化为一种数据结构。
这种数据结构其实就是一个大的json对象,json我们都熟悉,他就像一颗枝繁叶茂的树。有树根,有树干,有树枝,有树叶,无论多小多大,都是一棵完整的树。
简单理解,就是把我们写的代码按照一定的规则转换成一种树形结构。
举个简单的例子:
假设代码如下:
<div id="app">Hello</div>
随后我们将其转换为ast语法树(简单版本):
 {
     tag:'div'    //节点类型
     attrs:[{id:"app"}]    //属性
     children:[{tag:null,text:Hello},{xxx}]   //子节点
 }
当然,实际情况复杂得多,但总体结构不变
{
  "type": "Program",
  "start": 0,
  "end": 32,
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "JSXElement",
        "openingElement": {
          "type": "JSXOpeningElement",
          "name": {
            "type": "JSXIdentifier",
            "name": "div"
          },
          "attributes": [
            {
              "type": "JSXAttribute",
              "name": {
                "type": "JSXIdentifier",
                "name": "id"
              },
              "value": {
                "type": "Literal",
                "value": "app"
              }
            }
          ],
          "selfClosing": false
        },
        "closingElement": {
          "type": "JSXClosingElement",
          "name": {
            "type": "JSXIdentifier",
            "name": "div"
          }
        },
        "children": [
          {
            "type": "JSXText",
            "value": "Hello"
          },
          {
            "type": "JSXExpressionContainer",
            "expression": {
              "type": "Identifier",
              "name": "msg"
            }
          }
        ],
        "selfClosing": false
      }
    }
  ],
  "sourceType": "module"
}
2.模板解析
来看这个例子
<div id="app">Hello{{msg}}</div>
这无非就是一个简单的<div>标签,它由三个部分组成
开始标签:
<div id="app">
文本:
Hello{{msg}}
结束标签:
</div>
似乎只要用正则表达式来匹配就可以了,(事实上也确实是这么实现的)
//从源码处偷过来的正则表达式
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
//属性 例如: {id=app}
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //标签名称
const qnameCapture = `((?:${ncname}\\:)?${ncname})` //<span:xx>
const startTagOpen = new RegExp(`^<${qnameCapture}`) //标签开头
const startTagClose = /^\s*(\/?)>/ //匹配结束标签 的 >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) //结束标签 例如</div>
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
2.1.试验实例
我们来举一个实例看看:
代码已开源https://github.com/Fattiger4399/analytic-vue.git
(关键的部分已使用绿色荧光标出,没有耐心看完整代码的话,只看有绿色荧光标记的部分就好)
项目目录如下:

首先来到index.html我们人为的制造一些假数据
注意:此处的vue是我们自己写的实验品,并非大尤的Vue
index.html
<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head> <body>
<div id="app">Hello{{msg}}</div>
<script src="dist/vue.js"></script>
<script>
//umd Vue
// console.log(Vue)
//响应式 Vue
let vm = new Vue({
el: '#app', //编译模板
}) </script>
</body> </html>
入口文件index.js
import {initMixin} from "./init"
function Vue(options) {
    // console.log(options)
    //初始化
    this._init(options)
}
initMixin(Vue)
export default Vue
初始化脚本init.js
import { compileToFunction } from "./compile/index.js";
export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        // console.log(options)
        let vm = this
        //options为
        vm.$options = options
        //初始化状态
        initState(vm)
        // 渲染模板 el
        if (vm.$options.el) {
            vm.$mount(vm.$options.el)
        }
    }
    //创建 $mount
    Vue.prototype.$mount = function (el) {
        // console.log(el)
        //el template render
        let vm = this
        el = document.querySelector(el) //获取元素
        let options = vm.$options
        if (!options.render) { //没有
            let template = options.template
            if (!template && el) {
                //获取html
                el = el.outerHTML
                console.log(el,'this is init.js attrs:el')
                //<div id="app">Hello</div>
                //变成ast语法树
                let ast = compileToFunction(el)
                console.log(ast,'this is ast')
                //render()
            }
        }
    }
}
来到我们的核心部分/compile/index.js中的parseHTML()方法和parseStartTag()方法
function start(tag, attrs) { //开始标签
    console.log(tag, attrs, '开始的标签')
}
function charts(text) { //获取文本
    console.log(text, '文本')
}
function end(tag) { //结束的标签
    console.log(tag, '结束标签')
}
function parseHTML(html) {
    while (html) { //html 为空时,结束
        //判断标签 <>
        let textEnd = html.indexOf('<') //0
        console.log(html,textEnd,'this is textEnd')
        if (textEnd === 0) { //标签
            // (1) 开始标签
            const startTagMatch = parseStartTag() //开始标签的内容{}
            if (startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue;
            }
            // console.log(endTagMatch, '结束标签')
            //结束标签
            let endTagMatch = html.match(endTag)
            if (endTagMatch) {
                advance(endTagMatch[0].length)
                end(endTagMatch[1])
                continue;
            }
        }
        let text
        //文本
        if (textEnd > 0) {
            // console.log(textEnd)
            //获取文本内容
            text = html.substring(0, textEnd)
            // console.log(text)
        }
        if (text) {
            advance(text.length)
            charts(text)
            // console.log(html)
        }
    }
    function parseStartTag() {
        //
        const start = html.match(startTagOpen) // 1结果 2false
        console.log(start,'this is start')
        // match() 方法检索字符串与正则表达式进行匹配的结果
        // console.log(start)
        //创建ast 语法树
        if (start) {
            let match = {
                tagName: start[1],
                attrs: []
            }
            console.log(match,'match match')
            //删除 开始标签
            advance(start[0].length)
            //属性
            //注意 多个 遍历
            //注意>
            let attr //属性
            let end //结束标签
            //attr=html.match(attribute)用于匹配
            //非结束位'>',且有属性存在
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                // console.log(attr,'attr attr'); //{}
                // console.log(end,'end end')
                match.attrs.push({
                    name: attr[1],
                    value: attr[3] || attr[4] || attr[5]
                })
                advance(attr[0].length)
                //匹配完后,就进行删除操作
            }
            //end里面有东西了(只能是有">"),那么将其删除
            if (end) {
                // console.log(end)
                advance(end[0].length)
                return match
            }
        }
    }
    function advance(n) {
        // console.log(html)
        // console.log(n)
        html = html.substring(n)
        // substring() 方法返回一个字符串在开始索引到结束索引之间的一个子集,
        // 或从开始索引直到字符串的末尾的一个子集。
        // console.log(html)
    }
    console.log(root)
    return root
}
export function compileToFunction(el) {
    // console.log(el)
    let ast = parseHTML(el)
    console.log(ast,'ast ast')
}
注释已经非常详细了,实在看不懂的话,上机调试一遍吧
代码已开源https://github.com/Fattiger4399/analytic-vue.git
tips:
(1)parseHTML中拿到的参数html为 " el = el.outerHTML " 获取的元素
即' <div id="app">Hello{{msg}}</div> '
(2)attr = html.match(attribute)匹配后得到的数据长这样:

来看看看输出结果

成功地将我们需要的三样东西分出来了
Vue源码学习(二):<templete>渲染第一步,模板解析的更多相关文章
- Vue源码学习二 ———— Vue原型对象包装
		Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ... 
- vue 源码学习二 实例初始化和挂载过程
		vue 入口 从vue的构建过程可以知道,web环境下,入口文件在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compil ... 
- Vue源码学习(零):内部原理解析
		本篇文章是在阅读<剖析 Vue.js 内部运行机制>小册子后总结所得,想要了解详细内容,请参考原文:https://juejin.im/book/5a36661851882538e2259 ... 
- Vue源码学习(二)$mount() 后的做的事(1)
		Vue实例初始化完成后,启动加载($mount)模块数据. (一)Vue$3.protype.$mount 标红的函数 compileToFunctions 过于复杂,主要是生 ... 
- Vue源码学习三 ———— Vue构造函数包装
		Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ... 
- 手牵手,从零学习Vue源码 系列二(变化侦测篇)
		系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ... 
- Vue源码学习1——Vue构造函数
		Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ... 
- Vue源码分析(二) : Vue实例挂载
		Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ... 
- 【Vue源码学习】依赖收集
		前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ... 
- Dubbo源码学习(二)
		@Adaptive注解 在上一篇ExtensionLoader的博客中记录了,有两种扩展点,一种是普通的扩展实现,另一种就是自适应的扩展点,即@Adaptive注解的实现类. @Documented ... 
随机推荐
- mac部署flutter时执行brew update无反应
			找来找去还是镜像的问题 1.替换brew 镜像 git remote set-url origin https://mirrors.ustc.edu.cn/ew.git 2.替换homebrew-co ... 
- springboot接入influxdb
			转载请注明出处: 1.添加maven依赖 <dependency> <groupId>org.springframework.boot</groupId> < ... 
- 一次 HPC 病毒感染与解决经历
			周一的时候,有同事反馈说,HPC 的项目报告路径正在不断产生 *.exe 和 *.pif 文件,怀疑是不是被病毒感染! 收到信息,第一时间进去目录,的确发现该目录每个几秒钟就自动生成一个 *.exe ... 
- Qt6使用SeriaPortl包
			简介: 最近使用Qt6.0开发一个自己串口小工具的时候,遇到了没有QtSerialPort包的情况,一番折腾终于找到了解决方案... 一. 在系统自带的卸载更改程序中,找到Qt,点击卸载 二. 点击添 ... 
- 【Leetcode】 #9 回文数
			判断一个整数是否是回文数.回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数.示例 1:输入: 121输出: true示例 2:输入: -121输出: false解释: 从左向右读, 为 - ... 
- JavaScript 显示数据
			JavaScript 显示数据 JavaScript 可以通过不同的方式来输出数据: 使用 window.alert() 弹出警告框. 使用 document.write() 方法将内容写到 HTML ... 
- 行行AI人才直播第4期: 跟随占冰强老师走近《如何定制企业专属AI大模型?》
			行行AI人才是博客园和顺顺智慧共同运营的AI行业人才全生命周期服务平台. 每个企业定制专属AI大模型的目的都不同,比如某企业希望通过AI技术提升其客户服务和销售效果.该企业面临着庞大的商品数据.用户评 ... 
- List 接口及其常用方法
			List 接口基本介绍 List接口是Collection接口的子接口,其主要特点如下: List中元素有序,是按照元素的插入顺序进行排序的.每个元素都有一个与之关联的整数型索引(索引从 0 开始), ... 
- Visual Studio2019如何添加引用
			 同一解决方案中添加引用 比如我们想在Test项目中添加Queue项目的引用 1.鼠标右击引用-->添加引用 2."引用管理器"-->项目-->解决方案--&g ... 
- Unity中的InitializeOnLoad特性:深入解析与实践
			Unity中的InitializeOnLoad特性:深入解析与实践 在Unity开发过程中,我们经常需要在编辑器启动时或脚本重新编译后执行一些操作,例如初始化数据.注册事件等.这时,我们可以使用Ini ... 
