vue 中的模板编译的步骤:将 template 模板转化成 ast 语法树(拼接字符串),而后通过 new Function + with 语法,将 ast 语法树包装成 Render 函数,而后生成虚构节点,而后将虚构节点挂载到 dom 树上,生成实在 DOM.
(1) 将 template 模板转换成 ast 语法树 -parserHTML(正则实现)
(2) 对动态语法做动态标记 -markUp
(3) 从新生成代码 生成 render 函数返回的是虚构节点
留神:在开发时尽量不要应用 template, 因为将 template 转化成 render 办法,须要在运行时进行编译操作,会有性能损耗,同时援用电邮 compiler 包的 vue 体积也会变大。默认.vue 文件中的 template 解决是通过 vue-loader(依赖的是 vue-template-compiler)来进行解决的而不是通过运行时的编译
应用到的正则
// 匹配属性
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 startTagOpen = new RegExp(`^<${qnameCapture}`)
// 匹配开始标签闭合
const startTagClose = /^\s*(\/?)>/
// 匹配标签完结
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
解析开始节点,先匹配到 tagName, 而后再去循环匹配取的属性的值,而后组装成一个节点
function parseStartTag(){let start = html.match(startTagOpen)
if(start && start.length){
const match = {tagName:start[1],
attrs: []}
advance(start[0].length)
let end,attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {advance(attr[0].length)
match.attrs.push({name: attr[1],
value: attr[3]|| attr[4]|| attr[5]
})
}
if(end){advance(end[0].length)
}
return match
}
}
(1 代表 dom 节点,3 代表文本节点)
匹配完开始节点后,把曾经匹配过的字符串截取掉,残余字符串接着匹配,上面可能会呈现以下几种状况
(1)接下来匹配就是完结标签
那就祝贺你啦,这个节点曾经齐全匹配进去了,只须要把这个节点从咱们记录的栈里弹出,造成一个上面的构造就 OK 啦,而后截取点曾经匹配过的字符串,拿剩下的字符串持续进行匹配,直到传入的字符串全副被匹配一遍
{
tag: tagName,
type: 1,
children: [],
attrs:attrs,
parent: null
}
(2)接下来匹配的还是开始标签
此时咱们匹配到的这个开始标签和咱们上一个标签匹配到的必定是父子关系,从栈里取出最下面的一个节点,如果有值的话,就记录为以后节点的父节点,拿进去的节点的子节点为以后匹配的节点,而后截取点曾经匹配过的字符串,拿剩下的字符串持续进行匹配,直到传入的字符串全副被匹配一遍
(3)接下来匹配的是一段文本,文字就很简略了,用正则匹配到第一个‘<’也就是下一个 dom 节点的地位),把匹配后获得的字符串去空格解决后,生成一个上面的文本节点就 OK 啦,而后截取点曾经匹配过的字符串,拿剩下的字符串持续进行匹配,直到传入的字符串全副被匹配一遍
{
text: text,
type:3
}
等到传入的字符串全副匹配完结当前,会造成一个 js 形容的节点之间互相关系的 dom 树
{
tag: 'div',
parent: null,
attrs:[
{
name: 'id',
value: 'app'
}
],
children:[
tag: 'span',
parent:{
tag: 'div',
parent: null,
attrs:[
{
name: 'id',
value: 'app'
}
],
children:[...]
}
]
}
如图所示
而后就到了激动人心的时刻啦,咱们须要依据这颗形容节点的树提供的内容把他拼接成能够渲染成 render 函数的虚构 dom, 依照 tagName,attrs,children 的程序进行解决,最初造成相似上面这样的字符串
_c("div",{id:"app",style:{"width":"20px"}},_c("p",undefined,_v("hello"+_s(name))),_v("hello"))
特地留神的是对于行内款式的解决,要把行内款式的值拼成 key-value 的模式
if(attr.name === 'style'){let obj = {}
attr.value.split(';').forEach(attrItem => {let [key, value] = attrItem.split(':')
obj[key] = value
})
attr.value = obj
}
而后借助 new Function + with 就能够把拼接好的字符串转换成 render 函数了
graph TD
A[传入 el 属性] -->B{判断是否有 render 办法}
B -->| 是 | C[渲染 render 办法]
B -->| 否 | D{判断是否有 template}
D -->| 是 | E[渲染 template] --> F
D -->| 否 | F[渲染 el 外面的内容]
F -->| 否 |G{判断字符串里是否以箭头结尾}
G -->| 是 | H[应用正则进行匹配]
H --> I{用正则匹配是否是开始标签}
I --> | 是 | J[用正则别离取出来标签名和属性]-->T[将开始标签压入栈内]-->M
I --> | 否 | K{用正则匹配是否是完结标签}--> | 否 | L
K --> | 是 |R[从栈里取出来最下面的标签名, 造成闭合的节点] -->U[增加一个 dom 节点] --> M
G --> | 否 | L[阐明是以文本结尾而后]
L--> S[增加一个文字节点] --> M[把曾经匹配过的字段截掉从新进行匹配]--> F
F -->| 是 |N[生成用 js 形容 dom 节点的 ast 语法树]
N --> O[拼接包装成字符串]
O --> P[通过 new Function + with 语法生成 render 函数]--> C
(以上是本人学习过程中整顿,有谬误或者不谨严的中央欢送斧正)