上文中,我们已经实现了,数组的响应式和对象的响应式。接下来会学习到模板编译,ast语法树,虚拟DOM
1. 模板编译
在使用vue时,我们是使用el属性或者vm.$mount()的这两种方式去挂载实例。我们可以在实例内去写一些模板,也可以通过template属性去写一些模板
<div id="app">
<div> {{name}} </div>
<span>{{age}}</span>
</div>
const vue = new Vue({
data() {
return {
name: 'aaa',
age: 18,
address: {
ad :1
},
arr:[1,2,3,4,{a:1}]
}
},
el: "#app",
template: `<div>name</div>`
})
在了解了这一些之后就可以开始写源码
在init.js内
如果有options里面有el属性那么就帮他去调Vue的$mount方法
首先,根据el去获取挂载的元素,然后看没有用random属性,没有就去帮他生成,使用compileToFunction(template)方法,以template属性内的内容为主
// 进行模板编译,挂载实例
if(options.el) vm.$mount(options.el)
Vue.prototype.$mount = function (el) {
const vm = this
let ops = vm.$options
el = document.querySelector(el)
if(!ops.random) {
let template;
if(!ops.template && el) template = el.outerHTML
else {
if(el) template = ops.template
}
if(template) ops.random = compileToFunction(template)
}
}
compileToFunction方法,就是将el绑定的根元素的html文本进行解析或者template属性的html文本进行解析,把他变为ast语法树的一种结构。
首先对html文本进行解析,也就是使用正则表达式匹配对html的标签,文本,属性一一的匹配出来,然后匹配完毕之后就进行删除。不断的匹配删除,直到html被匹配完就截至。
可以通过该debugger来观察整个匹配的过程
compile.js内
// 标签名 a-aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 命名空间标签 aa:aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 开始标签-捕获标签名 <div
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 结束标签-匹配标签结尾的 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配标签结束的 > <br/>
const startTagClose = /^\s*(\/?)>/;
// 匹配 {{ }} 表达式
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function parseHTML(html) {
function advice(n) {
html = html.substring(n);
}
function parseStarTag() {
let start = html.match(startTagOpen)
if(start) {
let match = {
tagName: start[1],
attrs:[]
}
advice(start[0].length)
// 解析属性
let attr,end;
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advice(attr[0].length);
match.attrs.push({name:attr[1],value:attr[3] || attr[4] || attr[5]})
}
if(end) advice(end.length)
return match
}
return false
}
while(html) {
debugger
// textEnd = 0说明是标签的开始或者结束位置,大于0说明是文本的结束位置
let textEnd = html.indexOf('<')
if(textEnd == 0) {
let startTagMatch = parseStarTag()
if(startTagMatch) {
continue
}
let endTagMatch = html.match(endTag)
if(endTagMatch) {
advice(endTagMatch[0].length)
continue
}
}
if(textEnd > 0) {
let text = html.substring(0,textEnd)
if(text) {
advice(text.length)
}
}
}
}
export function compileToFunction(template) {
// 1. 将template转化为ast语法树
let ast = parseHTML(template);
}
2. 生成ast语法树
对匹配到的文本,标签,属性声明三个对应得方法进行处理,对于匹配后得元素父子关系通过栈的形式进行划分。
通过currentParent属性,标识栈内最后一个元素,如果有新元素进来,那么新元素就是之前的最后一个元素的儿子,currentParent属性将会更新,指向新进来的元素,直到将找到结束标签其弹出,currentParent属性将会更新
逻辑图示如下:
代码如下:
// 标签名 a-aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 命名空间标签 aa:aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 开始标签-捕获标签名 <div
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 结束标签-匹配标签结尾的 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配标签结束的 > <br/>
const startTagClose = /^\s*(\/?)>/;
// 匹配 {{ }} 表达式
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function parseHTML(html) {
const ELEMENT_TYPE = 1
const TEXT_TYPE = 3
const stack = []
// 指向栈中最后一个元素
let currentParent
// 根节点
let root
function createASTElement(tag,attrs) {
return {
tag,
type:ELEMENT_TYPE,
attrs,
children:[],
parent:null
}
}
function start(tagName,attrs) {
let node = createASTElement(tagName,attrs)
if(!root) root = node
stack.push(node)
if(currentParent) {
node.parent = currentParent
currentParent.children.push(node)
}
currentParent = node
}
function chars(text) {
text = text.replace(/\s/g,"")
text && currentParent.children.push({
type: TEXT_TYPE,
text
})
}
function end(tagName) {
stack.pop()
currentParent = stack[stack.length - 1]
}
function advice(n) {
html = html.substring(n);
}
function parseStarTag() {
let start = html.match(startTagOpen)
if(start) {
let match = {
tagName: start[1],
attrs:[]
}
advice(start[0].length)
// 解析属性
let attr,end;
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advice(attr[0].length);
match.attrs.push({name:attr[1],value:attr[3] || attr[4] || attr[5]})
}
if(end) advice(1)
return match
}
return false
}
while(html) {
// textEnd = 0说明是标签的开始或者结束位置,大于0说明是文本的结束位置
let textEnd = html.indexOf('<')
if(textEnd == 0) {
let startTagMatch = parseStarTag()
if(startTagMatch) {
start(startTagMatch.tagName,startTagMatch.attrs)
continue
}
let endTagMatch = html.match(endTag)
if(endTagMatch) {
end(endTagMatch[1])
advice(endTagMatch[0].length)
continue
}
}
if(textEnd > 0) {
let text = html.substring(0,textEnd)
if(text) {
chars(text)
advice(text.length)
}
}
}
console.log(root);
}
export function compileToFunction(template) {
// 1. 将template转化为ast语法树
let ast = parseHTML(template);
}