首页 > 编程语言 >Vue源码学习(二):<templete>渲染第一步,模板解析

Vue源码学习(二):<templete>渲染第一步,模板解析

时间:2023-09-06 20:45:58浏览次数:40  
标签:el Vue console log html 源码 options 模板

好家伙,

 

1.<template>去哪了

在正式内容之前,我们来思考一个问题,

当我们使用vue开发页面时,<tamplete>中的内容是如何变成我们网页中的内容的?

 

它会经历四步:

  1. 解析模板:Vue会解析<template>中的内容,识别出其中的指令、插值表达式({{}}),以及其他元素和属性。

  2. 生成AST:解析模板后,Vue会生成一个对应的AST(Abstract Syntax Tree,抽象语法树),用于表示模板的结构、指令、属性等信息。

  3. 生成渲染函数:根据生成的AST,Vue会生成渲染函数。渲染函数是一个函数,接收一些数据作为参数,并返回一个虚拟DOM(Virtual DOM)。

  4. 渲染到真实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)匹配后得到的数据长这样:

 

 

来看看看输出结果

成功地将我们需要的三样东西分出来了 

 

标签:el,Vue,console,log,html,源码,options,模板
From: https://www.cnblogs.com/FatTiger4399/p/17681865.html

相关文章

  • vue 生成海报并下载
    用到插件   vue-canvas-poster 具体的使用方法:html:<vue-canvas-poster:widthPixels="0":painting="painting"@success="success"@fail="fail"></vue-canvas-poster><......
  • 网红淘客卷轴模式系统开发介绍和部分源码分享
    网红淘客也是一种卷轴模式系统。什么是卷轴模式呢?新用户注册,先送你一部分积分,该积分用于兑换一个初始任务,俗称卷轴!卷轴模式的赚钱的原理是,你用积分兑换初级任务包,完成卷轴任务之后,你可以获得更多的积分,然后复投,达到一定数量后可以兑换更高级的任务包,任务包越高级每次获得的积分也就......
  • springboot vue人事管理系统源码
    开发环境及工具:大等于jdk1.8,大于mysql5.5,idea(eclipse),nodejs,vscode(webstorm)技术说明:springbootmybatisvueelementui代码注释齐全,没有多余代码,适合学习(毕设),二次开发,包含论文技术相关文档。功能介绍:员工端:考勤打卡:定位当前位置,打卡需要在打卡范围内才可打卡(后台设置打卡范围)我的......
  • 在vue项目中使用webWorker
    使用webworker可以进行多线程的数据处理, 我们可以把包含大量数据的逻辑交给webworker, 能避免在数据处理过程中造成的页面卡顿.1.首先,你可以在项目根目录的任意目录下,新建一个webworker的文件直接调用预置方法postMessage来传递数据, 使用onmessage来监听和获取处理完......
  • zone.js由入门到放弃之三——zone.js 源码分析【setTimeout篇】
    Delegate是个好东西,看看孙啸达同学对ZoneDelegate的介绍吧,这是他关于zone.js系列文章的第三篇~zone.js系列往期文章zone.js由入门到放弃之一——通过一场游戏认识zone.jszone.js由入门到放弃之二——zone.jsAPI大练兵zone.js源码分析接下来的全是干货,从头到尾,一干到底一点前置:Zon......
  • vue传递给后端时间格式问题
    前端处理首先前端使用moment.js进行处理data.userEnrolDate=moment(data.userEnrolDate).format('YYYY-MM-DDHH:mm:ss');后端处理@JsonFormat(timezone="GMT+8",pattern="yyyy-MM-ddHH:mm:ss")@DateTimeFormat(pattern="yyyy-MM-ddHH:mm:ss&quo......
  • 【VUE】倒计时计算
    <script>newVue({el:'#app',data:{openTime:'2023-09-0621:15:00',timer:"",定时器对象hours:"00",minut......
  • Vue + GitLab 实现自动化部署
      二、Linux安装nginx在Linux上安装NGINX的步骤如下:打开终端(命令行界面)。使用以下命令安装NGINX:对于Ubuntu/Debian系统:sudoapt-getinstallnginx对于CentOS/RHEL系统:sudoyuminstallnginx等待安装完成。使用以下命令启动NGI......
  • [SpringSecurity5.6.2源码分析三]:SpringWebMvcImportSelector
    1、SpringWebMvcImportSelector• SpringSecurity支持在SpringMVC进行参数解析的时候填充参数,支持以下的对象• 通过@AuthenticationPrincipal,获取UserDetails• 通过@CurrentSecurityContext,获取SecurityContext• 通过参数类型为CsrfToken获取CsrfToken• 究其原因是因为Spr......
  • Vue/React对比学习
    组件传值//父组件exportdefaultfunctionTab(props:any){const[serverUrl,setServerUrl]=useState<string|undefined>('https://');console.log(props);//父组件接受子组件的值并修改constchangeMsg=(msg?:string)=>{setServerUrl......