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

  • Vue源码学习(二):<templete>渲染第一步,模板解析已关闭评论
  • 102 次浏览
  • A+
所属分类:Web前端
摘要

好家伙,  在正式内容之前,我们来思考一个问题, 当我们使用vue开发页面时,<tamplete>中的内容是如何变成我们网页中的内容的 ?

好家伙,

 

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

(关键的部分已使用绿色荧光标出,没有耐心看完整代码的话,只看有绿色荧光标记的部分就好)

项目目录如下:

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

首先来到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源码学习(二):<templete>渲染第一步,模板解析

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