- A+
好家伙,
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)匹配后得到的数据长这样:
来看看看输出结果
成功地将我们需要的三样东西分出来了