Vue源码学习(三):渲染第二步,创建ast语法树

vue,源码,学习,templete,渲染,第二步,创建,ast,语法 · 浏览次数 : 140

小编点评

好的!你对代码的解析非常详细,以下是一些补充和建议: **1. 代码的结构** * 代码首先使用 `while` 循环读取 HTML 代码,并将文本内容提取到 `text` 中。 * 然后,使用 `parseHTML` 函数解析 HTML 代码,并将结果返回。 * `parseHTML` 函数使用递归调用自身 `parseStartTag` 和 `parseEnd` 函数来解析标签和文本内容。 * `parseStartTag` 函数使用正则表达式匹配标签的名称和属性,并将其添加到 `match` 对象中。 * `parseEnd` 函数使用正则表达式匹配标签的结束位置,并将结束标签的文本提取到 `end` 中。 * 每个标签和文本内容都用 `advance` 函数进行处理,将文本内容添加到相应的标签中。 **2. 代码的改进** * 可以使用正则表达式进行标签名和属性的匹配,提高代码的简化程度。 * 可以使用缓存机制来优化代码性能,避免重复执行相同的操作。 * 可以使用模板字符串进行代码书写,提高代码的可读性。 **3. 代码的注意事项** * 代码中没有考虑空白字符,可能导致解析结果中出现空白节点。 * 代码中没有处理错误的场景,例如无法匹配的标签或文本格式。 **4. 代码的总结** 代码非常清晰易懂,但可以进行一些优化和改进,以提高代码的性能和可维护性。

正文

好家伙,书接上回

 

在上一篇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 
}

 

与Vue源码学习(三):渲染第二步,创建ast语法树相似的内容:

Vue源码学习(三):渲染第二步,创建ast语法树

好家伙,书接上回 在上一篇Vue源码学习(二):渲染第一步,模板解析中,我们完成了模板解析 现在我们继续,将模板解析的转换为ast语法树 1.前情提要 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我

Vue源码学习(四):渲染第三步,将ast语法树转换为渲染函数

好家伙, Vue源码学习(三):渲染第二步,创建ast语法树, 在上一篇,我们已经成功将 我们的模板 转换为ast语法树 接下来我们继续进行操作 1.方法封装 由于代码太多,为了增加代码的可阅读性 我们先将代码进行封装 index.js import { generate } f

Vue源码学习(二):渲染第一步,模板解析

好家伙, 1.去哪了 在正式内容之前,我们来思考一个问题, 当我们使用vue开发页面时,中的内容是如何变成我们网页中的内容的? 它会经历四步: 解析模板:Vue会解析中的内容,识别出其中的指令、插值表达式({{}}),以及其他元素和属性。

Vue源码学习(五):渲染第四步,生成虚拟dom并将其转换为真实dom

好家伙, 前情提要: 在上一篇我们已经成功将ast语法树转换为渲染函数 现在我们继续 1.项目目录 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我解释给你听一万遍 新增文件:vnode/index.js vnode/p

Vue源码学习(七):合并生命周期(混入Vue.Mixin)

好家伙, 1.使用场景 现在来,来想一下,作为一个使用Vue的开发者,假设现在我们要使用created(),我们会如何使用 1.1. .vue文件中使用 {{ message }}

Vue源码学习(十一):计算属性computed初步学习

好家伙, 1.Computed实现原理 if (opts.computed) { initComputed(vm,opts.computed); } function initComputed(vm, computed) { // 存放计算属性的watcher const watchers = vm

Vue源码学习(一):数据劫持(对象类型)

好家伙,了解一下Vue如何实现数据劫持 1.Vue中data的使用 首先,我得搞清楚这玩意的概念,我们先从vue的使用开始吧 想想看,我们平时是如何使用vue的data部分的? 无非是这两种情况 (你可千万不要带着惊讶的表情说"啊!原来有两种写法的吗") //函数写法 data() { return

Vue源码学习(六):(支线)渲染函数中with(),call()的使用以及一些思考

好家伙, 昨天,在学习vue源码的过程中,看到了这个玩意 嘶,看不太懂,研究一下 1.上下文 这段出现vue模板编译的虚拟node部分 export function renderMixin(Vue) { Vue.prototype._c = function () { //创建标签 return

Vue源码学习(十):关于dep和watcher使用的一些思考

好家伙, 前面想了好久,都没想明白为什么要dep和watcher打配合才能实现数据-视图同步 为什么要多一个依赖管理这样的东西 给每个数据绑个watcher(xxfunction),然后,数据变了,调set,然后调xxfunction,不就行了, 然后今天突然想明白了,不是为什么要这么干,而是必须这

Vue源码学习(九):响应式前置:实现对象的依赖收集(dep和watcher)

好家伙,这是目前为止最绕的一章,也是十分抽象的一章 由于实在太过抽象,我只能用一个不那么抽象的实例去说服我自己 完整代码已开源https://github.com/Fattiger4399/analytic-vue.git 1.我们要做什么? 来看这个例子, index.html setTimeou