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

vue,源码,学习,templete,渲染,第一步,模板,解析 · 浏览次数 : 298

小编点评

## Summary of the code: This code is an example of using the `compileToFunction` method to compile an HTML string into a reusable function. **Key functionalities:** * Parses an HTML string into an AST (Abstract Syntax Tree). * Extracts tag names and attributes from the HTML. * Identifies start and end tags of different element types. * Reconstructs the AST and returns it as a function. **Key parts of the code:** * `parseHTML` method: * Uses a regular expression to match start and end tags in the HTML string. * For each tag type, it extracts its name and attributes into an object. * Builds the AST by combining extracted objects. * `compileToFunction` method: * Parses the HTML string and extracts relevant information. * Uses `parseHTML` to generate the AST. * Returns the AST as a function. **Overall, the code demonstrates how to:** * Parse HTML strings into a structured data format. * Extract and handle different element types. * Build a hierarchical representation of the HTML structure. * Convert the AST into a reusable function. **Benefits:** * This approach allows for easier maintenance and extension of the parsed AST. * It simplifies the process of processing and rendering HTML templates. **Note:** The provided code is a comment-rich example, providing a clear understanding of the code's functionality. The actual HTML string and AST are not provided in the context.

正文

好家伙,

 

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)匹配后得到的数据长这样:

 

 

来看看看输出结果

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

 

与Vue源码学习(二):渲染第一步,模板解析相似的内容:

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

好家伙, 1.去哪了 在正式内容之前,我们来思考一个问题, 当我们使用vue开发页面时,中的内容是如何变成我们网页中的内容的? 它会经历四步: 解析模板:Vue会解析中的内容,识别出其中的指令、插值表达式({{}}),以及其他元素和属性。

Vue源码学习(三):渲染第二步,创建ast语法树

好家伙,书接上回 在上一篇Vue源码学习(二):渲染第一步,模板解析中,我们完成了模板解析 现在我们继续,将模板解析的转换为ast语法树 1.前情提要 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我

Vue源码学习(四):渲染第三步,将ast语法树转换为渲染函数

好家伙, Vue源码学习(三):渲染第二步,创建ast语法树, 在上一篇,我们已经成功将 我们的模板 转换为ast语法树 接下来我们继续进行操作 1.方法封装 由于代码太多,为了增加代码的可阅读性 我们先将代码进行封装 index.js import { generate } f

Vue源码学习(五):渲染第四步,生成虚拟dom并将其转换为真实dom

好家伙, 前情提要: 在上一篇我们已经成功将ast语法树转换为渲染函数 现在我们继续 1.项目目录 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我解释给你听一万遍 新增文件:vnode/index.js vnode/p

Vue源码学习(七):合并生命周期(混入Vue.Mixin)

好家伙, 1.使用场景 现在来,来想一下,作为一个使用Vue的开发者,假设现在我们要使用created(),我们会如何使用 1.1. .vue文件中使用 {{ message }}

Vue源码学习(十一):计算属性computed初步学习

好家伙, 1.Computed实现原理 if (opts.computed) { initComputed(vm,opts.computed); } function initComputed(vm, computed) { // 存放计算属性的watcher const watchers = vm

Vue源码学习(一):数据劫持(对象类型)

好家伙,了解一下Vue如何实现数据劫持 1.Vue中data的使用 首先,我得搞清楚这玩意的概念,我们先从vue的使用开始吧 想想看,我们平时是如何使用vue的data部分的? 无非是这两种情况 (你可千万不要带着惊讶的表情说"啊!原来有两种写法的吗") //函数写法 data() { return

Vue源码学习(六):(支线)渲染函数中with(),call()的使用以及一些思考

好家伙, 昨天,在学习vue源码的过程中,看到了这个玩意 嘶,看不太懂,研究一下 1.上下文 这段出现vue模板编译的虚拟node部分 export function renderMixin(Vue) { Vue.prototype._c = function () { //创建标签 return

Vue源码学习(十):关于dep和watcher使用的一些思考

好家伙, 前面想了好久,都没想明白为什么要dep和watcher打配合才能实现数据-视图同步 为什么要多一个依赖管理这样的东西 给每个数据绑个watcher(xxfunction),然后,数据变了,调set,然后调xxfunction,不就行了, 然后今天突然想明白了,不是为什么要这么干,而是必须这

Vue源码学习(九):响应式前置:实现对象的依赖收集(dep和watcher)

好家伙,这是目前为止最绕的一章,也是十分抽象的一章 由于实在太过抽象,我只能用一个不那么抽象的实例去说服我自己 完整代码已开源https://github.com/Fattiger4399/analytic-vue.git 1.我们要做什么? 来看这个例子, index.html setTimeou