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:[...]
        }
    ]
}

如图所示
image.png
然后就到了激动人心的时刻啦,我们需要根据这颗描述节点的树提供的内容把他拼接成可以渲染成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

(以上是自己学习过程中整理,有错误或者不严谨的地方欢迎指正)


山谣
10 声望0 粉丝