Vue源码学习(二):<templete>渲染第一步,模板解析
  6dLT7EsVU3rP 2023年11月01日 67 0
Vue

好家伙,

 

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

 

 

来看看看输出结果

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

 

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  gYl4rku9YpWY   2024年05月17日   59   0   0 Vue
  JZjRRktyDDvK   2024年05月02日   85   0   0 Vue
  VlNAKfyhjjp9   2024年04月30日   81   0   0 Vue
  JNTrZmaOQEcq   2024年04月30日   58   0   0 Vue
  onf2Mh1AWJAW   2024年05月17日   63   0   0 Vue
  3A8RnFxQCDqM   2024年05月17日   59   0   0 Vue
6dLT7EsVU3rP