前言
看vue2官网的生命周期图可以得到结论:
vue初次渲染 => 先初始化数据 => 将模板进行编译 => 变成render() => 生成虚拟节点 => 变成真实DOM=>放到页面
vue中模板编译的方式:
- render
- tmplate
- el (这个必须要有)
执行顺序是 render => tmplate => el
所新增代码以及文件的目录结构:
- index.html
- src
- compile // 编译相关(主要作用:就是把模板转换成render函数,在render函数中创建虚拟DOM)
-index.js
- init.js
获取html及创建ast树
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">{{msg}}123<h1>6666</h1></div>
<script src="dist/vue.js"></script>
<script>
let vm= new Vue({
el:"#app",
data(){
console.log(this,"@@");
return {
msg:"hello world",
arr:[{name:"小明",age:18}]
}
},
})
console.log(vm,"这是vm");
</script>
</body>
</html>
src/init.js:
import { initState } from "./initState";
import { compileToFunction } from "./compile/index";
/**
* @description 初始化vue
* @param {Object} Vue
* @returns {void}
*/
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
let vm = this
vm.$options = options
// 初始化状态
initState(vm)
// 渲染模板
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
// 创建$mount 进行模板编译
Vue.prototype.$mount = function (el) {
let vm = this
// 获取dom元素
el = document.querySelector(el)
let options = vm.$options
// 没有render函数
if (!options.render) {
let template = options.template
if (!template && el) {
// 获取html
el = el.outerHTML
// 转换成ast语法树 vnode(虚拟dom) [ast语法树是能操作js和css 虚拟dom只能操作一个节点]
let ast = compileToFunction(el)
}
}
}
}
src/compile/index.js:
let root // 表示根元素
let createParent // 当前元素的父元素
let stack = [] // 数据结构 栈
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 获取属性
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*` // 获取标签名称
// const qnameCapture = `((?:${ncname}\\:)?${ncname})` //
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 标签开头的正则 捕获的内容是标签名
const startTagClose = /^\s*(\/?)>/ // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
/***
* @description 生成ast语法树
* @param {string} template
*
*/
export function compileToFunction(template) {
// 解析HTML
let ast = parseHTML(template)
}
function parseHTML(html) {
// 遍历 html 为空就结束
while (html) {
// 判断标签
let textEnd = html.indexOf('<');
let text
if (textEnd === 0) { // 标签
// 有两种可能开始标签 和 结束标签
let startTagMatch = parseStartTag();
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
let endTagMatch = html.match(endTag)
if (endTagMatch) {
advance(endTagMatch[0].length)
end(endTagMatch[1])
continue
}
}
// 文本
if (textEnd > 0) {
// 获取到文本内容
text = html.substring(0, textEnd)
}
if (text) {
// 删除文本
advance(text.length)
charts(text)
}
}
function parseStartTag() {
// 匹配开始标签
let start = html.match(startTagOpen);
if (!start) {
return
}
let match = {
tagName: start[1],
attrs: [],
}
// 删除开始标签
advance(start[0].length)
// 遍历属性
let attr
let end
while (!(end= html.match(startTagClose)) && (attr = html.match(attribute))){
match.attrs.push({ name:attr[1],value:attr[3]||attr[4]||attr[5] })
advance(attr[0].length)
}
if (end) {
advance(end[0].length)
return match
}
}
function advance(n) {
html = html.substring(n)
}
console.log("最后的root",root);
return root
}
// 遍历开始的标签
function start(tagName,attrs) {
let element = createASTElement(tagName, attrs)
if (!root) {
root=element
}
createParent = element
stack.push(element)
}
// 遍历文本标签
function charts(text) {
// 空格
text = text.replace(/\s/g, ' ')
if (text) {
createParent.children.push({type:3,text:text})
}
}
// 遍历结束标签
function end(tagName) {
let element = stack.pop() // 获取最后的元素进行出栈
createParent=stack[stack.length-1]
if (createParent) { // 元素闭合
element.parent = createParent.tag
createParent.children.push(element)
}
}
function createASTElement(tag,attrs) {
return {
tag, // 表示元素
attrs, // 表示属性
children: [], // 是否有子节点
type: 1,
parent: null,
}
}
最后解析出来的ast树: