Vue原始碼學習(三):<templete>渲染第二步,建立ast語法樹

2023-09-09 06:01:05

好傢伙,書接上回

 

在上一篇Vue原始碼學習(二):<templete>渲染第一步,模板解析中,我們完成了模板解析

現在我們繼續,將模板解析的轉換為ast語法樹

 

1.前情提要

程式碼已開源https://github.com/Fattiger4399/analytic-vue.git手動偵錯一遍,

勝過我解釋給你聽一萬遍

function start(tag, attrs) { //開始標籤
    console.log(tag, attrs, '開始的標籤')
}

function charts(text) { //獲取文字
    console.log(text, '文字')
}

function end(tag) { //結束的標籤
    console.log(tag, '結束標籤')
}

在這裡,我們知道start,charts,end分別可以拿到

我們的`開始標籤`,`文字`,`結束標籤`

效果如下:(仔細看,這也是我們實驗要用到的例子)

 

隨後我們開始改造這幾個方法

 

2.程式碼詳解

2.1.ast樹節點的結構

確定我們ast樹節點的結構:

let root; //根元素
let createParent //當前元素的父親
let stack = [] 
function createASTElement(tag, attrs) {
    return {
        tag,
        attrs,
        children: [],
        type: 1,
        parent: null
    }
}

節點元素分別為

  • tag:標籤名
  • attrs:標籤屬性
  • children:子元素(陣列)
  • type:型別(後面會用到,目前"1"代表標籤"3"代表文字)
  • parent:父元素

 

2.2.start()方法

function start(tag, attrs) { //開始標籤
    let element = createASTElement(tag, attrs) //生成一個開始標籤元素
    //檢視root根元素是否為空
    //若是,將該元素作為根
    //非原則
    if (!root) {
        root = element
    }
    createParent = element
    stack.push(element)
    console.log(tag, attrs, '開始的標籤')
}

此處,生成一個開始標籤元素,判斷root是否為空,若為空,則將該元素作為根元素

隨後將該元素作為父元素.

 

2.3.charts()方法

function charts(text) { //獲取文字
    console.log(text, '文字')
    // text = text.replace(/a/g,'')
    if(text){
        createParent.children.push({
            type:3,
            text
        })
    }
    // console.log(stack,'stack')
}

這個好理解,將"文字內容"作為父元素的孩子

 

2.4.end()方法

function end(tag) { //結束的標籤
    let element = stack.pop()
    createParent = stack[stack.length - 1]
    if (createParent) { //元素閉合
        element.parent = createParent.tag
        createParent.children.push(element)
    }
    console.log(tag, '結束標籤')
}

此處,我們先將棧stack最新的元素彈出棧(作為當前元素,我們要對他進行操作),

隨後獲取棧的前一個元素作為父元素,

當前元素的父元素屬性指向父元素的標籤屬性

隨後將該元素推入父元素的children中,

emmmm,我還是說人話吧

 

假設現在stack=['div','h1']

然後pop了,createParent = 'h1'

'h1'.parent =>'div'

'div'.children =>'h1'

(多看幾遍就理解了,其實非常簡單)

 

來看看最終實現的ast語法樹長什麼樣子

 (父子關係和諧)

 

搞定啦!

 

3.完整程式碼

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

let root; //根元素
let createParent //當前元素的父親
let stack = [] 
function createASTElement(tag, attrs) {
    return {
        tag,
        attrs,
        children: [],
        type: 1,
        parent: null
    }
}

function start(tag, attrs) { //開始標籤
    let element = createASTElement(tag, attrs) //生成一個開始標籤元素
    //檢視root根元素是否為空
    //若是,將該元素作為根
    //非原則
    if (!root) {
        root = element
    }
    createParent = element
    stack.push(element)
    console.log(tag, attrs, '開始的標籤')
}

function charts(text) { //獲取文字
    console.log(text, '文字')
    // text = text.replace(/a/g,'')
    if(text){
        createParent.children.push({
            type:3,
            text
        })
    }
    // console.log(stack,'stack')
}

function end(tag) { //結束的標籤
    let element = stack.pop()
    createParent = stack[stack.length - 1]
    if (createParent) { //元素閉合
        element.parent = createParent.tag
        createParent.children.push(element)
    }
    console.log(tag, '結束標籤')
}

export 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 
}