精华内容
下载资源
问答
  • Vue.js 模板解析原理
    千次阅读
    2019-04-03 05:18:13

    本文来自《深入浅出Vue.js》模板编译原理篇的第九章,主要讲述了如何将模板解析成AST,这一章的内容是全书最复杂且烧脑的章节。本文排版较为紧凑和图片是未经加工的原稿,真实纸质书的排版和图片会更加精致。

    通过第8章的学习,我们知道解析器在整个模板编译中的位置。我们只有将模板解析成AST后,才能基于AST做优化或者生成代码字符串,那么解析器是如何将模板解析成AST的呢?

    本章中,我们将详细介绍解析器内部的运行原理。

    9.1 解析器的作用

    解析器要实现的功能是将模板解析成AST。

    例如:

    <div>
      <p>{{name}}</p>
    </div>
    复制代码

    上面的代码是一个比较简单的模板,它转换成AST后的样子如下:

    {
      tag: "div"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: undefined,
      attrsList: [],
      attrsMap: {},
      children: [
        {
          tag: "p"
          type: 1,
          staticRoot: false,
          static: false,
          plain: true,
          parent: {tag: "div", ...},
          attrsList: [],
          attrsMap: {},
          children: [{
            type: 2,
            text: "{{name}}",
            static: false,
            expression: "_s(name)"
          }]
        }
      ]
    }
    复制代码

    其实AST并不是什么很神奇的东西,不要被它的名字吓倒。它只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。比如,parent属性保存了父节点的描述对象,children属性是一个数组,里面保存了一些子节点的描述对象。再比如,type属性代表一个节点的类型等。当很多个独立的节点通过parent属性和children属性连在一起时,就变成了一个树,而这样一个用对象描述的节点树其实就是AST。

    9.2 解析器内部运行原理

    事实上,解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器,其中最主要的是HTML解析器。顾名思义,HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。

    伪代码如下:

    parseHTML(template, {
        start (tag, attrs, unary) {
            // 每当解析到标签的开始位置时,触发该函数
        },
        end () {
            // 每当解析到标签的结束位置时,触发该函数
        },
        chars (text) {
            // 每当解析到文本时,触发该函数
        },
        comment (text) {
            // 每当解析到注释时,触发该函数
        }
    })
    复制代码

    你可能不能很清晰地理解,下面我们举个简单的例子:

    <div><p>我是Berwin</p></div>
    复制代码

    当上面这个模板被HTML解析器解析时,所触发的钩子函数依次是:startstartcharsendend

    也就是说,解析器其实是从前向后解析的。解析到<div>时,会触发一个标签开始的钩子函数start;然后解析到<p>时,又触发一次钩子函数start;接着解析到我是Berwin这行文本,此时触发了文本钩子函数chars;然后解析到</p>,触发了标签结束的钩子函数end;接着继续解析到</div>,此时又触发一次标签结束的钩子函数end,解析结束。

    因此,我们可以在钩子函数中构建AST节点。在start钩子函数中构建元素类型的节点,在chars钩子函数中构建文本类型的节点,在comment钩子函数中构建注释类型的节点。

    当HTML解析器不再触发钩子函数时,就代表所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST构建完成。

    我们发现,钩子函数start有三个参数,分别是tagattrsunary,它们分别代表标签名、标签的属性以及是否是自闭合标签。

    而文本节点的钩子函数chars和注释节点的钩子函数comment都只有一个参数,只有text。这是因为构建元素节点时需要知道标签名、属性和自闭合标识,而构建注释节点和文本节点时只需要知道文本即可。

    什么是自闭合标签?举个简单的例子,input标签就属于自闭合标签:<input type="text" />,而div标签就不属于自闭合标签:<div></div>

    start钩子函数中,我们可以使用这三个参数来构建一个元素类型的AST节点,例如:

    function createASTElement (tag, attrs, parent) {
        return {
            type: 1,
            tag,
            attrsList: attrs,
            parent,
            children: []
        }
    }
    
    parseHTML(template, {
        start (tag, attrs, unary) {
            let element = createASTElement(tag, attrs, currentParent)
        }
    })
    复制代码

    在上面的代码中,我们在钩子函数start中构建了一个元素类型的AST节点。

    如果是触发了文本的钩子函数,就使用参数中的文本构建一个文本类型的AST节点,例如:

    parseHTML(template, {
        chars (text) {
            let element = {type: 3, text}
        }
    })
    复制代码

    如果是注释,就构建一个注释类型的AST节点,例如:

    parseHTML(template, {
        comment (text) {
            let element = {type: 3, text, isComment: true}
        }
    })
    复制代码

    你会发现,9.1节中看到的AST是有层级关系的,一个AST节点具有父节点和子节点,但是9.2节中介绍的创建节点的方式,节点是被拉平的,没有层级关系。因此,我们需要一套逻辑来实现层级关系,让每一个AST节点都能找到它的父级。下面我们介绍一下如何构建AST层级关系。

    构建AST层级关系其实非常简单,我们只需要维护一个栈(stack)即可,用栈来记录层级关系,这个层级关系也可以理解为DOM的深度。

    HTML解析器在解析HTML时,是从前向后解析。每当遇到开始标签,就触发钩子函数start。每当遇到结束标签,就会触发钩子函数end

    基于HTML解析器的逻辑,我们可以在每次触发钩子函数start时,把当前构建的节点推入栈中;每当触发钩子函数end时,就从栈中弹出一个节点。

    这样就可以保证每当触发钩子函数start时,栈的最后一个节点就是当前正在构建的节点的父节点,如图9-1所示。

    图9-1 使用栈记录DOM层级关系( 英文为代码体

    下面我们用一个具体的例子来描述如何从0到1构建一个带层级关系的AST。

    假设有这样一个模板:

    <div>
        <h1>我是Berwin</h1>
        <p>我今年23岁</p>
    </div>
    复制代码

    上面这个模板被解析成AST的过程如图9-2所示。

    图9-2构建AST的过程(下面的(1)~(12)需要改成图中那样黑底白字的

    图9-2给出了构建AST的过程,图中的黑底白数字代表解析的步骤,具体如下。

    (1) 模板的开始位置是div的开始标签,于是会触发钩子函数startstart触发后,会先构建一个div节点。此时发现栈是空的,这说明div节点是根节点,因为它没有父节点。最后,将div节点推入栈中,并将模板字符串中的div开始标签从模板中截取掉。

    (2) 这时模板的开始位置是一些空格,这些空格会触发文本节点的钩子函数,在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。

    (3) 这时模板的开始位置是h1的开始标签,于是会触发钩子函数start。与前面流程一样,start触发后,会先构建一个h1节点。此时发现栈的最后一个节点是div节点,这说明h1节点的父节点是div,于是将h1添加到div的子节点中,并且将h1节点推入栈中,同时从模板中将h1的开始标签截取掉。

    (4) 这时模板的开始位置是一段文本,于是会触发钩子函数charschars触发后,会先构建一个文本节点,此时发现栈中的最后一个节点是h1,这说明文本节点的父节点是h1,于是将文本节点添加到h1节点的子节点中。由于文本节点没有子节点,所以文本节点不会被推入栈中。最后,将文本从模板中截取掉。

    (5) 这时模板的开始位置是h1结束标签,于是会触发钩子函数endend触发后,会把栈中最后一个节点弹出来。

    (6) 与第(2)步一样,这时模板的开始位置是一些空格,这些空格会触发文本节点的钩子函数,在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。

    (7) 这时模板的开始位置是p开始标签,于是会触发钩子函数startstart触发后,会先构建一个p节点。由于第(5)步已经从栈中弹出了一个节点,所以此时栈中的最后一个节点是div,这说明p节点的父节点是div。于是将p推入div的子节点中,最后将p推入到栈中,并将p的开始标签从模板中截取掉。

    (8) 这时模板的开始位置又是一段文本,于是会触发钩子函数chars。当chars触发后,会先构建一个文本节点,此时发现栈中的最后一个节点是p节点,这说明文本节点的父节点是p节点。于是将文本节点推入p节点的子节点中,并将文本从模板中截取掉。

    (9) 这时模板的开始位置是p的结束标签,于是会触发钩子函数end。当end触发后,会从栈中弹出一个节点出来,也就是把p标签从栈中弹出来,并将p的结束标签从模板中截取掉。

    (10) 与第(2)步和第(6)步一样,这时模板的开始位置是一些空格,这些空格会触发文本节点的钩子函数并且在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。

    (11) 这时模板的开始位置是div的结束标签,于是会触发钩子函数end。其逻辑与之前一样,把栈中的最后一个节点弹出来,也就是把div弹了出来,并将div的结束标签从模板中截取掉。

    (12)这时模板已经被截取空了,也就代表着HTML解析器已经运行完毕。这时我们会发现栈已经空了,但是我们得到了一个完整的带层级关系的AST语法树。这个AST中清晰写明了每个节点的父节点、子节点及其节点类型。

    9.3 HTML解析器

    通过前面的介绍,我们发现构建AST非常依赖HTML解析器所执行的钩子函数以及钩子函数中所提供的参数,你一定会非常好奇HTML解析器是如何解析模板的,接下来我们会详细介绍HTML解析器的运行原理。

    9.3.1 运行原理

    事实上,解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,直到HTML模板被截成一个空字符串时结束循环,解析完毕,如图9-2所示。

    在截取一小段字符串时,有可能截取到开始标签,也有可能截取到结束标签,又或者是文本或者注释,我们可以根据截取的字符串的类型来触发不同的钩子函数。

    循环HTML模板的伪代码如下:

    function parseHTML(html, options) {
      while (html) {
        // 截取模板字符串并触发钩子函数
      }
    }
    复制代码

    为了方便理解,我们手动模拟HTML解析器的解析过程。例如,下面这样一个简单的HTML模板:

    <div>
      <p>{{name}}</p>
    </div>
    复制代码

    它在被HTML解析器解析的过程如下。

    最初的HTML模板:

    `<div>
      <p>{{name}}</p>
    </div>`
    复制代码

    第一轮循环时,截取出一段字符串<div>,并且触发钩子函数start,截取后的结果为:

    `
      <p>{{name}}</p>
    </div>`
    复制代码

    第二轮循环时,截取出一段字符串:

    `
      `
    复制代码

    并且触发钩子函数chars,截取后的结果为:

    `<p>{{name}}</p>
    </div>`
    复制代码

    第三轮循环时,截取出一段字符串<p>,并且触发钩子函数start,截取后的结果为:

    `{{name}}</p>
    </div>`
    复制代码

    第四轮循环时,截取出一段字符串{{name}},并且触发钩子函数chars,截取后的结果为:

    `</p>
    </div>`
    复制代码

    第五轮循环时,截取出一段字符串</p>,并且触发钩子函数end,截取后的结果为:

    `
    </div>`
    复制代码

    第六轮循环时,截取出一段字符串:

    `
    `
    复制代码

    并且触发钩子函数chars,截取后的结果为:

    `</div>`
    复制代码

    第七轮循环时,截取出一段字符串</div>,并且触发钩子函数end,截取后的结果为:

    ``
    复制代码

    解析完毕。

    HTML解析器的全部逻辑都是在循环中执行,循环结束就代表解析结束。接下来,我们要讨论的重点是HTML解析器在循环中都干了些什么事。

    你会发现HTML解析器可以很聪明地知道它在每一轮循环中应该截取哪些字符串,那么它是如何做到这一点的呢?

    通过前面的例子,我们发现一个很有趣的事,那就是每一轮截取字符串时,都是在整个模板的开始位置截取。我们根据模板开始位置的片段类型,进行不同的截取操作。

    例如,上面例子中的第一轮循环:如果是以开始标签开头的模板,就把开始标签截取掉。

    再例如,上面例子中的第四轮循环:如果是以文本开始的模板,就把文本截取掉。

    这些被截取的片段分很多种类型,示例如下。

    • 开始标签,例如<div>
    • 结束标签,例如</div>
    • HTML注释,例如<!-- 我是注释 -->
    • DOCTYPE,例如<!DOCTYPE html>
    • 条件注释,例如<!--[if !IE]>-->我是注释<!--<![endif]-->
    • 文本,例如我是Berwin

    通常,最常见的是开始标签、结束标签、文本以及注释。

    9.3.2 截取开始标签

    上一节中我们说过,每一轮循环都是从模板的最前面截取,所以只有模板以开始标签开头,才需要进行开始标签的截取操作。

    那么,如何确定模板是不是以开始标签开头?

    在HTML解析器中,想分辨出模板是否以开始标签开头并不难,我们需要先判断HTML模板是不是以<开头。

    如果HTML模板的第一个字符不是<,那么它一定不是以开始标签开头的模板,所以不需要进行开始标签的截取操作。

    如果HTML模板以<开头,那么说明它至少是一个以标签开头的模板,但这个标签到底是什么类型的标签,还需要进一步确认。

    如果模板以<开头,那么它有可能是以开始标签开头的模板,同时它也有可能是以结束标签开头的模板,还有可能是注释等其他标签,因为这些类型的片段都以<开头。那么,要进一步确定模板是不是以开始标签开头,还需要借助正则表达式来分辨模板的开始位置是否符合开始标签的特征。

    那么,如何使用正则表达式来匹配模板以开始标签开头?我们看下面的代码:

    const ncname = '[a-zA-Z_][\\w\\-\\.]*'
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    const startTagOpen = new RegExp(`^<${qnameCapture}`)
    
    // 以开始标签开始的模板
    '<div></div>'.match(startTagOpen) // ["<div", "div", index: 0, input: "<div></div>"]
    
    // 以结束标签开始的模板
    '</div><div>我是Berwin</div>'.match(startTagOpen) // null
    
    // 以文本开始的模板
    '我是Berwin</p>'.match(startTagOpen) // null
    复制代码

    通过上面的例子可以看到,只有'<div></div>'可以成功匹配,而以</div>开头的或者以文本开头的模板都无法成功匹配。

    在9.2节中,我们介绍了当HTML解析器解析到标签开始时,会触发钩子函数start,同时会给出三个参数,分别是标签名(tagName)、属性(attrs)以及自闭合标识(unary)。

    因此,在分辨出模板以开始标签开始之后,需要将标签名、属性以及自闭合标识解析出来。

    在分辨模板是否以开始标签开始时,就可以得到标签名,而属性和自闭合标识则需要进一步解析。

    当完成上面的解析后,我们可以得到这样一个数据结构:

    const start = '<div></div>'.match(startTagOpen)
    if (start) {
        const match = {
            tagName: start[1],
            attrs: []
        }
    }
    复制代码

    这里有一个细节很重要:在前面的例子中,我们匹配到的开始标签并不全。例如:

    const ncname = '[a-zA-Z_][\\w\\-\\.]*'
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    const startTagOpen = new RegExp(`^<${qnameCapture}`)
    
    '<div></div>'.match(startTagOpen)
    // ["<div", "div", index: 0, input: "<div></div>"]
    
    '<p></p>'.match(startTagOpen)
    // ["<p", "p", index: 0, input: "<p></p>"]
    
    '<div class="box"></div>'.match(startTagOpen)
    // ["<div", "div", index: 0, input: "<div class="box"></div>"]
    复制代码

    可以看出,上面这个正则表达式虽然可以分辨出模板是否以开始标签开头,但是它的匹配规则并不是匹配整个开始标签,而是开始标签的一小部分。

    事实上,开始标签被拆分成三个小部分,分别是标签名、属性和结尾,如图9-3所示。

    图9-3 开始标签被拆分成三个小部分( 代码用代码体

    通过“标签名”这一段字符,就可以分辨出模板是否以开始标签开头,此后要想得到属性和自闭合标识,则需要进一步解析。

    1. 解析标签属性

    在分辨模板是否以开始标签开头时,会将开始标签中的标签名这一小部分截取掉,因此在解析标签属性时,我们得到的模板是下面伪代码中的样子:

    ' class="box"></div>'
    复制代码

    通常,标签属性是可选的,一个标签的属性有可能存在,也有可能不存在,所以需要判断标签是否存在属性,如果存在,对它进行截取。

    下面的伪代码展示了如何解析开始标签中的属性,但是它只能解析一个属性:

    const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    let html = ' class="box"></div>'
    let attr = html.match(attribute)
    html = html.substring(attr[0].length)
    console.log(attr)
    // [' class="box"', 'class', '=', 'box', undefined, undefined, index: 0, input: ' class="box"></div>']
    复制代码

    如果标签上有很多属性,那么上面的处理方式就不足以支撑解析任务的正常运行。例如下面的代码:

    const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    let html = ' class="box" id="el"></div>'
    let attr = html.match(attribute)
    html = html.substring(attr[0].length)
    console.log(attr)
    // [' class="box"', 'class', '=', 'box', undefined, undefined, index: 0, input: ' class="box" id="el"></div>']
    复制代码

    可以看到,这里只解析出了class属性,而id属性没有解析出来。

    此时剩余的HTML模板是这样的:

    ' id="el"></div>'
    复制代码

    所以属性也可以分成多个小部分,一小部分一小部分去解析与截取。

    解决这个问题时,我们只需要每解析一个属性就截取一个属性。如果截取完后,剩下的HTML模板依然符合标签属性的正则表达式,那么说明还有剩余的属性需要处理,此时就重复执行前面的流程,直到剩余的模板不存在属性,也就是剩余的模板不存在符合正则表达式所预设的规则。

    例如:

    const startTagClose = /^\s*(\/?)>/
    const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    let html = ' class="box" id="el"></div>'
    let end, attr
    const match = {tagName: 'div', attrs: []}
    
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        html = html.substring(attr[0].length)
        match.attrs.push(attr)
    }
    复制代码

    上面这段代码的意思是,如果剩余HTML模板不符合开始标签结尾部分的特征,并且符合标签属性的特征,那么进入到循环中进行解析与截取操作。

    通过match方法解析出的结果为:

    {
        tagName: 'div',
        attrs: [
            [' class="box"', 'class', '=', 'box', null, null],
            [' id="el"', 'id','=', 'el', null, null]
        ]
    }
    复制代码

    可以看到,标签中的两个属性都已经解析好并且保存在了attrs中。

    此时剩余模板是下面的样子:

    "></div>"
    复制代码

    我们将属性解析后的模板与解析之前的模板进行对比:

    // 解析前的模板
    ' class="box" id="el"></div>'
    
    // 解析后的模板
    '></div>'
    
    // 解析前的数据
    {
        tagName: 'div',
        attrs: []
    }
    
    // 解析后的数据
    {
        tagName: 'div',
        attrs: [
            [' class="box"', 'class', '=', 'box', null, null],
            [' id="el"', 'id','=', 'el', null, null]
        ]
    }
    复制代码

    可以看到,标签上的所有属性都已经被成功解析出来,并保存在attrs属性中。

    2. 解析自闭合标识

    如果我们接着上面的例子继续解析的话,目前剩余的模板是下面这样的:

    '></div>'
    复制代码

    开始标签中结尾部分解析的主要目的是解析出当前这个标签是否是自闭合标签。

    举个例子:

    <div></div>
    复制代码

    这样的div标签就不是自闭合标签,而下面这样的input标签就属于自闭合标签:

    <input type="text" />
    复制代码

    自闭合标签是没有子节点的,所以前文中我们提到构建AST层级时,需要维护一个栈,而一个节点是否需要推入到栈中,可以使用这个自闭合标识来判断。

    那么,如何解析开始标签中的结尾部分呢?看下面这段代码:

    function parseStartTagEnd (html) {
      const startTagClose = /^\s*(\/?)>/
      const end = html.match(startTagClose)
      const match = {}
    
      if (end) {
          match.unarySlash = end[1]
          html = html.substring(end[0].length)
          return match
      }
    }
    
    console.log(parseStartTagEnd('></div>')) // {unarySlash: ""}
    console.log(parseStartTagEnd('/><div></div>')) // {unarySlash: "/"}
    复制代码

    这段代码可以正确解析出开始标签是否是自闭合标签。

    从代码中打印出来的结果可以看到,自闭合标签解析后的unarySlash属性为/,而非自闭合标签为空字符串。

    3. 实现源码

    前面解析开始标签时,我们将其拆解成了三个部分,分别是标签名、属性和结尾。我相信你已经对开始标签的解析有了一个清晰的认识,接下来看一下Vue.js中真实的代码是什么样的:

    const ncname = '[a-zA-Z_][\\w\\-\\.]*'
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    const startTagOpen = new RegExp(`^<${qnameCapture}`)
    const startTagClose = /^\s*(\/?)>/
    
    function advance (n) {
        html = html.substring(n)
    }
    
    function parseStartTag () {
        // 解析标签名,判断模板是否符合开始标签的特征
        const start = html.match(startTagOpen)
        if (start) {
            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(attr)
            }
            
            // 判断是否是自闭合标签
            if (end) {
                match.unarySlash = end[1]
                advance(end[0].length)
                return match
            }
        }
    }
    复制代码

    上面的代码是Vue.js中解析开始标签的源码,这段代码中的html变量是HTML模板。

    调用parseStartTag就可以将剩余模板开始部分的开始标签解析出来。如果剩余HTML模板的开始部分不符合开始标签的正则表达式规则,那么调用parseStartTag就会返回undefined。因此,判断剩余模板是否符合开始标签的规则,只需要调用parseStartTag即可。如果调用它后得到了解析结果,那么说明剩余模板的开始部分符合开始标签的规则,此时将解析出来的结果取出来并调用钩子函数start即可:

    // 开始标签
    const startTagMatch = parseStartTag()
    if (startTagMatch) {
        handleStartTag(startTagMatch)
        continue
    }
    复制代码

    前面我们说过,所有解析操作都运行在循环中,所以continue的意思是这一轮的解析工作已经完成,可以进行下一轮解析工作。

    从代码中可以看出,如果调用parseStartTag之后有返回值,那么会进行开始标签的处理,其处理逻辑主要在handleStartTag中。这个函数的主要目的就是将tagNameattrsunary等数据取出来,然后调用钩子函数将这些数据放到参数中。

    9.3.3 截取结束标签

    结束标签的截取要比开始标签简单得多,因为它不需要解析什么,只需要分辨出当前是否已经截取到结束标签,如果是,那么触发钩子函数就可以了。

    那么,如何分辨模板已经截取到结束标签了呢?其道理其实和开始标签的截取相同。

    如果HTML模板的第一个字符不是<,那么一定不是结束标签。只有HTML模板的第一个字符是<时,我们才需要进一步确认它到底是不是结束标签。

    进一步确认时,我们只需要判断剩余HTML模板的开始位置是否符合正则表达式中定义的规则即可:

    const ncname = '[a-zA-Z_][\\w\\-\\.]*'
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
    
    const endTagMatch = '</div>'.match(endTag)
    const endTagMatch2 = '<div>'.match(endTag)
    
    console.log(endTagMatch) // ["</div>", "div", index: 0, input: "</div>"]
    console.log(endTagMatch2) // null
    复制代码

    上面代码可以分辨出剩余模板是否是结束标签。当分辨出结束标签后,需要做两件事,一件事是截取模板,另一件事是触发钩子函数。而Vue.js中相关源码被精简后如下:

    const endTagMatch = html.match(endTag)
    if (endTagMatch) {
        html = html.substring(endTagMatch[0].length)
        options.end(endTagMatch[1])
        continue
    }
    复制代码

    可以看出,先对模板进行截取,然后触发钩子函数。

    9.3.4 截取注释

    分辨模板是否已经截取到注释的原理与开始标签和结束标签相同,先判断剩余HTML模板的第一个字符是不是<,如果是,再用正则表达式来进一步匹配:

    const comment = /^<!--/
    
    if (comment.test(html)) {
        const commentEnd = html.indexOf('-->')
    
        if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
                options.comment(html.substring(4, commentEnd))
            }
            html = html.substring(commentEnd + 3)
            continue
        }
    }
    复制代码

    在上面的代码中,我们使用正则表达式来判断剩余的模板是否符合注释的规则,如果符合,就将这段注释文本截取出来。

    这里有一个有意思的地方,那就是注释的钩子函数可以通过选项来配置,只有options.shouldKeepComment为真时,才会触发钩子函数,否则只截取模板,不触发钩子函数。

    9.3.5 截取条件注释

    条件注释不需要触发钩子函数,我们只需要把它截取掉就行了。

    截取条件注释的原理与截取注释非常相似,如果模板的第一个字符是<,并且符合我们事先用正则表达式定义好的规则,就说明需要进行条件注释的截取操作。

    在下面的代码中,我们通过indexOf找到条件注释结束位置的下标,然后将结束位置前的字符都截取掉:

    const conditionalComment = /^<!\[/
    if (conditionalComment.test(html)) {
        const conditionalEnd = html.indexOf(']>')
    
        if (conditionalEnd >= 0) {
            html = html.substring(conditionalEnd + 2)
            continue
        }
    }
    复制代码

    我们来举个例子:

    const conditionalComment = /^<!\[/
    let html = '<![if !IE]><link href="non-ie.css" rel="stylesheet"><![endif]>'
    if (conditionalComment.test(html)) {
        const conditionalEnd = html.indexOf(']>')
        if (conditionalEnd >= 0) {
            html = html.substring(conditionalEnd + 2)
        }
    }
    
    console.log(html) // '<link href="non-ie.css" rel="stylesheet"><![endif]>'
    复制代码

    从打印结果中可以看到,HTML中的条件注释部分截取掉了。

    通过这个逻辑可以发现,在Vue.js中条件注释其实没有用,写了也会被截取掉,通俗一点说就是写了也白写。

    9.3.6 截取DOCTYPE

    DOCTYPE与条件注释相同,都是不需要触发钩子函数的,只需要将匹配到的这一段字符截取掉即可。下面的代码将DOCTYPE这段字符匹配出来后,根据它的length属性来决定要截取多长的字符串:

    const doctype = /^<!DOCTYPE [^>]+>/i
    const doctypeMatch = html.match(doctype)
    if (doctypeMatch) {
        html = html.substring(doctypeMatch[0].length)
        continue
    }
    复制代码

    示例如下:

    const doctype = /^<!DOCTYPE [^>]+>/i
    let html = '<!DOCTYPE html><html lang="en"><head></head><body></body></html>'
    const doctypeMatch = html.match(doctype)
    if (doctypeMatch) {
        html = html.substring(doctypeMatch[0].length)
    }
    
    console.log(html) // '<html lang="en"><head></head><body></body></html>'
    复制代码

    从打印结果可以看到,HTML中的DOCTYPE被成功截取掉了。

    9.3.7 截取文本

    若想分辨在本轮循环中HTML模板是否已经截取到文本,其实很简单,我们甚至不需要使用正则表达式。

    在前面的其他标签类型中,我们都会判断剩余HTML模板的第一个字符是否是<,如果是,再进一步确认到底是哪种类型。这是因为以<开头的标签类型太多了,如开始标签、结束标签和注释等。然而文本只有一种,如果HTML模板的第一个字符不是<,那么它一定是文本了。

    例如:

    我是文本</div>
    复制代码

    上面这段HTML模板并不是以<开头的,所以可以断定它是以文本开头的。

    那么,如何从模板中将文本解析出来呢?我们只需要找到下一个<在什么位置,这之前的所有字符都属于文本,如图9-4所示。

    图9-4 尖括号前面的字符都属于文本

    在代码中可以这样实现:

    while (html) {
        let text
        let textEnd = html.indexOf('<')
        
        // 截取文本
        if (textEnd >= 0) {
            text = html.substring(0, textEnd)
            html = html.substring(textEnd)
        }
    
        // 如果模板中找不到<,就说明整个模板都是文本
        if (textEnd < 0) {
            text = html
            html = ''
        }
    
        // 触发钩子函数
        if (options.chars && text) {
            options.chars(text)
        }
    }
    复制代码

    上面的代码共有三部分逻辑。

    第一部分是截取文本,这在前面介绍过了。<之前的所有字符都是文本,直接使用html.substring从模板的最开始位置截取到<之前的位置,就可以将文本截取出来。

    第二部分是一个条件:如果在整个模板中都找不到<,那么说明整个模板全是文本。

    第三部分是触发钩子函数并将截取出来的文本放到参数中。

    关于文本,还有一个特殊情况需要处理:如果<是文本的一部分,该如何处理?

    举个例子:

    1<2</div>
    复制代码

    在上面这样的模板中,如果只截取第一个<前面的字符,最后被截取出来的将只有1,而不能把所有文本都截取出来。

    那么,该如何解决这个问题呢?

    有一个思路是,如果将<前面的字符截取完之后,剩余的模板不符合任何需要被解析的片段的类型,就说明这个<是文本的一部分。

    什么是需要被解析的片段的类型?在9.3.1节中,我们说过HTML解析器是一段一段截取模板的,而被截取的每一段都符合某种类型,这些类型包括开始标签、结束标签和注释等。

    说的再具体一点,那就是上面这段代码中的1被截取完之后,剩余模板是下面的样子:

    <2</div>
    复制代码

    <2符合开始标签的特征么?不符合。

    <2符合结束标签的特征么?不符合。

    <2符合注释的特征么?不符合。

    当剩余的模板什么都不符合时,就说明<属于文本的一部分。

    当判断出<是属于文本的一部分后,我们需要做的事情是找到下一个<并将其前面的文本截取出来加到前面截取了一半的文本后面。

    这里还用上面的例子,第二个<之前的字符是<2,那么把<2截取出来后,追加到上一次截取出来的1的后面,此时的结果是:

    1<2
    复制代码

    截取后剩余的模板是:

    </div>
    复制代码

    如果剩余的模板依然不符合任何被解析的类型,那么重复此过程。直到所有文本都解析完。

    说完了思路,我们看一下具体的实现,伪代码如下:

    while (html) {
        let text, rest, next
        let textEnd = html.indexOf('<')
        
        // 截取文本
        if (textEnd >= 0) {
            rest = html.slice(textEnd)
            while (
                !endTag.test(rest) &&
                !startTagOpen.test(rest) &&
                !comment.test(rest) &&
                !conditionalComment.test(rest)
            ) {
                // 如果'<'在纯文本中,将它视为纯文本对待
                next = rest.indexOf('<', 1)
                if (next < 0) break
                textEnd += next
                rest = html.slice(textEnd)
            }
            text = html.substring(0, textEnd)
            html = html.substring(textEnd)
        }
        
        // 如果模板中找不到<,那么说明整个模板都是文本
        if (textEnd < 0) {
            text = html
            html = ''
        }
        
        // 触发钩子函数
        if (options.chars && text) {
            options.chars(text)
        }
    }
    复制代码

    在代码中,我们通过while来解决这个问题(注意是里面的while)。如果剩余的模板不符合任何被解析的类型,那么重复解析文本,直到剩余模板符合被解析的类型为止。

    在上面的代码中,endTagstartTagOpencommentconditionalComment都是正则表达式,分别匹配结束标签、开始标签、注释和条件注释。

    在Vue.js源码中,截取文本的逻辑和其他的实现思路一致。

    9.3.8 纯文本内容元素的处理

    什么是纯文本内容元素呢?scriptstyletextarea这三种元素叫作纯文本内容元素。解析它们的时候,会把这三种标签内包含的所有内容都当作文本处理。那么,具体该如何处理呢?

    前面介绍开始标签、结束标签、文本、注释的截取时,其实都是默认当前需要截取的元素的父级元素不是纯文本内容元素。事实上,如果要截取元素的父级元素是纯文本内容元素的话,处理逻辑将完全不一样。

    事实上,在while循环中,最外层的判断条件就是父级元素是不是纯文本内容元素。例如下面的伪代码:

    while (html) {
        if (!lastTag || !isPlainTextElement(lastTag)) {
            // 父元素为正常元素的处理逻辑
        } else {
            // 父元素为script、style、textarea的处理逻辑
        }
    }
    复制代码

    在上面的代码中,lastTag代表父元素。可以看到,在while中,首先进行判断,如果父元素不存在或者不是纯文本内容元素,那么进行正常的处理逻辑,也就是前面介绍的逻辑。

    而当父元素是script这种纯文本内容元素时,会进入到else这个语句里面。由于纯文本内容元素都被视作文本处理,所以我们的处理逻辑就变得很简单,只需要把这些文本截取出来并触发钩子函数chars,然后再将结束标签截取出来并触发钩子函数end

    也就是说,如果父标签是纯文本内容元素,那么本轮循环会一次性将这个父标签给处理完毕。

    伪代码如下:

    while (html) {
        if (!lastTag || !isPlainTextElement(lastTag)) {
            // 父元素为正常元素的处理逻辑
        } else {
            // 父元素为script、style、textarea的处理逻辑
            const stackedTag = lastTag.toLowerCase()
            const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
            const rest = html.replace(reStackedTag, function (all, text) {
                if (options.chars) {
                    options.chars(text)
                }
                return ''
            })
            html = rest
            options.end(stackedTag)
        }
    }
    复制代码

    上面代码中的正则表达式可以匹配结束标签前包括结束标签自身在内的所有文本。

    我们可以给replace方法的第二个参数传递一个函数。在这个函数中,我们得到了参数text(代表结束标签前的所有内容),触发了钩子函数chars并把text放到钩子函数的参数中传出去。最后,返回了一个空字符串,代表将匹配到的内容都截掉了。注意,这里的截掉会将内容和结束标签一起截取掉。

    最后,调用钩子函数end并将标签名放到参数中传出去,代表本轮循环中的所有逻辑都已处理完毕。

    假如我们现在有这样一个模板:

    <div id="el">
        <script>console.log(1)</script>
    </div>
    复制代码

    当解析到script中的内容时,模板是下面的样子:

    console.log(1)</script>
    </div>
    复制代码

    此时父元素为script,所以会进入到else中的逻辑进行处理。在其处理过程中,会触发钩子函数charsend

    钩子函数chars的参数为script中的所有内容,本例中大概是下面的样子:

    chars('console.log(1)')
    复制代码

    钩子函数end的参数为标签名,本例中是script

    处理后的剩余模板如下:

    
    </div>
    复制代码

    9.3.9 使用栈维护DOM层级

    通过前面几节的介绍,特别是9.3.8节中的介绍,你一定会感到很奇怪,如何知道父元素是谁?

    在前面几节中,我们并没有介绍HTML解析器内部其实也有一个栈来维护DOM层级关系,其逻辑与9.2.1节相同:就是每解析到开始标签,就向栈中推进去一个;每解析到标签结束,就弹出来一个。因此,想取到父元素并不难,只需要拿到栈中的最后一项即可。

    同时,HTML解析器中的栈还有另一个作用,它可以检测出HTML标签是否正确闭合。例如:

    <div><p></div>
    复制代码

    在上面的代码中,p标签忘记写结束标签,那么当HTML解析器解析到div的结束标签时,栈顶的元素却是p标签。这个时候从栈顶向栈底循环找到div标签,在找到div标签之前遇到的所有其他标签都是忘记了闭合的标签,而Vue.js会在非生产环境下在控制台打印警告提示。

    关于使用栈来维护DOM层级关系的具体实现思路,9.2.1节已经详细介绍过,这里不再重复介绍。

    9.3.10 整体逻辑

    前面我们把开始标签、结束标签、注释、文本、纯文本内容元素等的截取方式拆分开,单独进行了详细介绍。本节中,我们就来介绍如何将这些解析方式组装起来完成HTML解析器的功能。

    首先,HTML解析器是一个函数。就像9.2节介绍的那样,HTML解析器最终的目的是实现这样的功能:

    parseHTML(template, {
        start (tag, attrs, unary) {
            // 每当解析到标签的开始位置时,触发该函数
        },
        end () {
            // 每当解析到标签的结束位置时,触发该函数
        },
        chars (text) {
            // 每当解析到文本时,触发该函数
        },
        comment (text) {
            // 每当解析到注释时,触发该函数
        }
    })
    复制代码

    所以HTML解析器在实现上肯定是一个函数,它有两个参数——模板和选项:

    export function parseHTML (html, options) {
        // 做点什么
    }
    复制代码

    我们的模板是一小段一小段去截取与解析的,所以需要一个循环来不断截取,直到全部截取完毕:

    export function parseHTML (html, options) {
        while (html) {
            // 做点什么
        }
    }
    复制代码

    在循环中,首先要判断父元素是不是纯文本内容元素,因为不同类型父节点的解析方式将完全不同:

    export function parseHTML (html, options) {
        while (html) {
            if (!lastTag || !isPlainTextElement(lastTag)) {
                // 父元素为正常元素的处理逻辑
            } else {
                // 父元素为script、style、textarea的处理逻辑
            }
        }
    }
    复制代码

    在上面的代码中,我们发现这里已经把整体逻辑分成了两部分,一部分是父标签是正常标签的逻辑,另一部分是父标签是scriptstyletextarea这种纯文本内容元素的逻辑。

    如果父标签为正常的元素,那么有几种情况需要分别处理,比如需要分辨出当前要解析的一小段模板到底是什么类型。是开始标签?还是结束标签?又或者是文本?

    我们把所有需要处理的情况都列出来,有下面几种情况:

    • 文本
    • 注释
    • 条件注释
    • DOCTYPE
    • 结束标签
    • 开始标签

    我们会发现,在这些需要处理的类型中,除了文本之外,其他都是以标签形式存在的,而标签是以<开头的。

    所以逻辑就很清晰了,我们先根据<来判断需要解析的字符是文本还是其他的:

    export function parseHTML (html, options) {
        while (html) {
            if (!lastTag || !isPlainTextElement(lastTag)) {
                let textEnd = html.indexOf('<')
                if (textEnd === 0) {
                    // 做点什么
                }
                
                let text, rest, next
                if (textEnd >= 0) {
                    // 解析文本
                }
                
                if (textEnd < 0) {
                    text = html
                    html = ''
                }
                
                if (options.chars && text) {
                    options.chars(text)
                }
            } else {
                // 父元素为script、style、textarea的处理逻辑
            }
        }
    }
    复制代码

    在上面的代码中,我们可以通过<来分辨是否需要进行文本解析。关于文本解析的内容,详见9.3.7节。

    如果通过<分辨出即将解析的这一小部分字符不是文本而是标签类,那么标签类有那么多类型,我们需要进一步分辨具体是哪种类型:

    export function parseHTML (html, options) {
        while (html) {
            if (!lastTag || !isPlainTextElement(lastTag)) {
                let textEnd = html.indexOf('<')
                if (textEnd === 0) {
                    // 注释
                    if (comment.test(html)) {
                        // 注释的处理逻辑
                        continue
                    }
                    
                    // 条件注释
                    if (conditionalComment.test(html)) {
                        // 条件注释的处理逻辑
                        continue
                    }
                    
                    // DOCTYPE
                    const doctypeMatch = html.match(doctype)
                    if (doctypeMatch) {
                        // DOCTYPE的处理逻辑
                        continue
                    }
                    
                    // 结束标签
                    const endTagMatch = html.match(endTag)
                    if (endTagMatch) {
                        // 结束标签的处理逻辑
                        continue
                    }
                    
                    // 开始标签
                    const startTagMatch = parseStartTag()
                    if (startTagMatch) {
                        // 开始标签的处理逻辑
                        continue
                    }
                }
                
                let text, rest, next
                if (textEnd >= 0) {
                    // 解析文本
                }
                
                if (textEnd < 0) {
                    text = html
                    html = ''
                }
                
                if (options.chars && text) {
                    options.chars(text)
                }
            } else {
                // 父元素为script、style、textarea的处理逻辑
            }
        }
    }
    复制代码

    关于不同类型的具体处理方式,前面已经详细介绍过,这里不再重复。

    9.4 文本解析器

    文本解析器的作用是解析文本。你可能会觉得很奇怪,文本不是在HTML解析器中被解析出来了么?准确地说,文本解析器是对HTML解析器解析出来的文本进行二次加工。为什么要进行二次加工?

    文本其实分两种类型,一种是纯文本,另一种是带变量的文本。例如下面这样的文本是纯文本:

    Hello Berwin
    复制代码

    而下面这样的是带变量的文本:

    Hello {{name}}
    复制代码

    在Vue.js模板中,我们可以使用变量来填充模板。而HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。

    我们在9.2节中介绍过,每当HTML解析器解析到文本时,都会触发chars函数,并且从参数中得到解析出的文本。在chars函数中,我们需要构建文本类型的AST,并将它添加到父节点的children属性中。

    而在构建文本类型的AST时,纯文本和带变量的文本是不同的处理方式。如果是带变量的文本,我们需要借助文本解析器对它进行二次加工,其代码如下:

    parseHTML(template, {
        start (tag, attrs, unary) {
            // 每当解析到标签的开始位置时,触发该函数
        },
        end () {
            // 每当解析到标签的结束位置时,触发该函数
        },
        chars (text) {
            text = text.trim()
            if (text) {
                const children = currentParent.children
                let expression
                if (expression = parseText(text)) {
                    children.push({
                        type: 2,
                        expression,
                        text
                    })
                } else {
                    children.push({
                        type: 3,
                        text
                    })
                }
            }
        },
        comment (text) {
            // 每当解析到注释时,触发该函数
        }
    })
    复制代码

    chars函数中,如果执行parseText后有返回结果,则说明文本是带变量的文本,并且已经通过文本解析器(parseText)二次加工,此时构建一个带变量的文本类型的AST并将其添加到父节点的children属性中。否则,就直接构建一个普通的文本节点并将其添加到父节点的children属性中。而代码中的currentParent是当前节点的父节点,也就是前面介绍的栈中的最后一个节点。

    假设chars函数被触发后,我们得到的text是一个带变量的文本:

    "Hello {{name}}"
    复制代码

    这个带变量的文本被文本解析器解析之后,得到的expression变量是这样的:

    "Hello "+_s(name)
    复制代码

    上面代码中的_s其实是下面这个toString函数的别名:

    function toString (val) {
        return val == null
            ? ''
            : typeof val === 'object'
                ? JSON.stringify(val, null, 2)
                : String(val)
    }
    复制代码

    假设当前上下文中有一个变量name,其值为Berwin,那么expression中的内容被执行时,它的内容是不是就是Hello Berwin了?

    我们举个例子:

    var obj = {name: 'Berwin'}
    with(obj) {
        function toString (val) {
            return val == null
                ? ''
                : typeof val === 'object'
                    ? JSON.stringify(val, null, 2)
                    : String(val)
        }
        console.log("Hello "+toString(name)) // "Hello Berwin"
    }
    复制代码

    在上面的代码中,我们打印出来的结果是"Hello Berwin"

    事实上,最终AST会转换成代码字符串放在with中执行,这部分内容会在第11章中详细介绍。

    接着,我们详细介绍如何加工文本,也就是文本解析器的内部实现原理。

    在文本解析器中,第一步要做的事情就是使用正则表达式来判断文本是否是带变量的文本,也就是检查文本中是否包含{{xxx}}这样的语法。如果是纯文本,则直接返回undefined;如果是带变量的文本,再进行二次加工。所以我们的代码是这样的:

    function parseText (text) {
        const tagRE = /\{\{((?:.|\n)+?)\}\}/g
        if (!tagRE(text)) {
            return
        }
    }
    复制代码

    在上面的代码中,如果是纯文本,则直接返回。如果是带变量的文本,该如何处理呢?

    一个解决思路是使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)这样的形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。如果最后一个变量的后面有文本,就将它添加到数组中。

    这时我们其实已经有一个数组,数组元素的顺序和文本的顺序是一致的,此时将这些数组元素用+连起来变成字符串,就可以得到最终想要的效果,如图9-5所示。

    图9-5 文本解析过程

    在图9-5中,最上面的字符串代表即将解析的文本,中间两个方块代表数组中的两个元素。最后,使用数组方法join将这两个元素合并成一个字符串。

    具体实现代码如下:

    function parseText (text) {
        const tagRE = /\{\{((?:.|\n)+?)\}\}/g
        if (!tagRE.test(text)) {
            return
        }
    
        const tokens = []
        let lastIndex = tagRE.lastIndex = 0
        let match, index
        while ((match = tagRE.exec(text))) {
            index = match.index
            // 先把 {{ 前边的文本添加到tokens中
            if (index > lastIndex) {
                tokens.push(JSON.stringify(text.slice(lastIndex, index)))
            }
            // 把变量改成`_s(x)`这样的形式也添加到数组中
            tokens.push(`_s(${match[1].trim()})`)
            
            // 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
            lastIndex = index + match[0].length
        }
        
        // 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
        if (lastIndex < text.length) {
            tokens.push(JSON.stringify(text.slice(lastIndex)))
        }
        return tokens.join('+')
    }
    复制代码

    这是文本解析器的全部代码,代码并不多,逻辑也不是很复杂。

    这段代码有一个很关键的地方在lastIndex:每处理完一个变量后,会重新设置lastIndex的位置,这样可以保证如果后面还有其他变量,那么在下一轮循环时可以从lastIndex的位置开始向后匹配,而lastIndex之前的文本将不再被匹配。

    下面用文本解析器解析不同的文本看看:

    parseText('你好{{name}}')
    // '"你好 "+_s(name)'
    
    parseText('你好Berwin')
    // undefined
    
    parseText('你好{{name}}, 你今年已经{{age}}岁啦')
    // '"你好"+_s(name)+", 你今年已经"+_s(age)+"岁啦"'
    复制代码

    从上面代码的打印结果可以看到,文本已经被正确解析了。

    9.5 总结

    解析器的作用是通过模板得到AST(抽象语法树)。

    生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数时,我们可以构建出不同的节点。

    随后,我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。

    最终,当HTML解析器运行完毕后,我们就可以得到一个完整的带DOM层级关系的AST。

    HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。

    文本分两种类型,不带变量的纯文本和带变量的文本,后者需要使用文本解析器进行二次加工。

    更多精彩内容可以观看《深入浅出Vue.js》

    关于《深入浅出Vue.js》

    本书使用最最容易理解的文笔来描述Vue.js的内部原理,对于想学习Vue.js原理的小伙伴是非常值得入手的一本书。

    京东:item.jd.com/12573168.ht…

    亚马逊:www.amazon.cn/gp/product/…

    当当:product.dangdang.com/26922892.ht…

    扫码京东购买

    转载于:https://juejin.im/post/5ca44160518825440a4b9fab

    更多相关内容
  • vue模板解析原理

    2022-04-04 18:27:23
    head> <body> <div id="app"> <h1> {{ name }} h1> {{name}} div> <script> const app = new Vue({ el: '#app', data: { name: '温情key' } }) script> body> html> 解析前页面显示 vue是如何解析成我们定义data所...
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
    
        <div id="app">
            <h1> {{ name }} </h1>
            {{name}}
        </div>
    
        <script>
            const app = new Vue({
                el: '#app',
                data: {
                    name: '温情key'
                }
            })
        </script>
    </body>
    </html>
    

    解析前页面显示

    在这里插入图片描述

    vue是如何解析成我们定义data所对应的值

    class Vue {
        constructor(options) {
            this.$el = document.querySelector(options.el);
            this.$data = options.data;
            this.compile(this.$el);
        };
    
        compile(node) {
            // node.childNodes 节点的所有子节点
            node.childNodes.forEach((item, index) => {
                // nodeType 节点类型  1代表元素节点 3代表文本节点  如果文本节点有{{}}就替换成数据
                if(item.nodeType === 1) {  // <h1> {{ name }} </h1>
                    if(item.childNodes.length > 0) {
                        this.compile(item);
                    }
                }
                if(item.nodeType === 3) {  // {{name}}
                    // 正则匹配{{}}
                    let reg = /\{\{(.*?)\}\}/g;
                    let text = item.textContent;  // textContent  节点内容
                    // 给节点赋值
                    item.textContent = text.replace(reg, (match, vmKey) => {
                        vmKey = vmKey.trim();
                        return this.$data[vmKey];
                    })
                }
            })
        };
    }
    

    解析后页面显示

    在这里插入图片描述

    展开全文
  • Vue模板编译原理

    千次阅读 2020-12-22 00:46:09
    Vue中的模板编译是什么 ...这其实都是Vue模板编译的功劳。 对于Vue来说,我们所认为的“HTML”其实都是字符串。Vue会根据其规定的模板语法规则,将其解析成AST语法树(其实就是用一个树状的大对象来描述我们所谓的“HT

    Vue中的模板编译是什么

    刚接触Vue的同学可能会产生这样的疑问:为什么在“HTML”中可以通过{{ name }}、v-bind:value="value"等方式获取JavaScript中的变量?为什么单文件组件导出的只有JavaScript的代码,但是其它地方在使用该组件时却能渲染出组件的“HTML”样式?这其实都是Vue模板编译的功劳。

    对于Vue来说,我们所认为的“HTML”其实都是字符串。Vue会根据其规定的模板语法规则,将其解析成AST语法树(其实就是用一个树状的大对象来描述我们所谓的“HTML”);然后会对这个大对象进行一些初步处理,比如标记没有动态绑定值的节点;最后,会把这个大对象编译成render函数,并将它绑定在组件的实例上。这样,我们所认为的“HTML”就变成了JavaScript代码,可以基于JavaScript模块规则进行导入导出,在需要渲染组件的地方,就调用render函数,根据组件当前的状态生成虚拟dom,然后就可以根据这个虚拟dom去更新视图了。

    Vue的模板编译就是将“HTML”模板编译成render函数的过程。这个过程大致可以分成三个阶段:

    • 解析阶段:将“HTML”模板解析成AST语法树;
    • 优化阶段:从AST语法树中找出静态子树并进行标记(被标记的静态子树在虚拟dom比对时会被忽略,从而提高虚拟dom比对的性能);
    • 代码生成阶段:通过AST生成代码字符串,并最终生成render函数。

    下面我们来详细介绍这三个阶段。

    解析阶段

    解析阶段就是将模板字符串解析成AST语法树的过程,我们先来看一下经过这个过程转换的效果:

    原始的模板字符串:

    解析后得到的AST语法树:

    接下来我们来看Vue是怎样将模板字符解析成AST语法树的,其核心代码如下:

    // 通过一个栈来维护dom的层级
    const stack = []
    // 抽象语法树的根节点,在解析的过程中,会不断给它添加子孙节点
    let root
    // 当前解析内容的父节点,既当前解析出的内容都应该放在这个变量的children数组中;它是通过栈来维护的,在解析的过程中随着dom层级不断变化
    let currentParent
     
    // parseHTML是解析模板字符串的“主线程”,它的第一个参数是要解析的模板字符串,也就是单文件组件中最外层<template>所包裹的部分;第二个参数是一个选项对象,它会包含一些回调,以及一些配置项。
    // parseHTML会从模板字符串的开始顺序向后解析,然后在特定的时机调用相应的回调:比如,在匹配到开始标签后,会把开始标签的信息传给相应的回调执行一下,然后把这个匹配到的内容截取掉,然后对剩下的内容继续下一次匹配。
    // parseHTML的具体实现我们会在后面介绍,这里只需要了解它的功能即可。
    parseHTML(template, {
      start (tag, attrs, unary) {
        // 匹配到开始标签时的回调,tag为当前标签的标签名,attrs为该标签上的属性列表,unary为当前标签是否为自闭合标签
        // 匹配到开始标签,会创建一个元素类型的树节点,如果当前不存在根节点,则将其设置为根节点,如果存在,则将其设置为currentParent的子节点
        // 然后将当前节点压入stack栈中,并将它设置为currentParent
      },
      end () {
        // 匹配到结束标签时的回调
        // 匹配到结束标签时,会从栈中弹出一个节点,并将栈中的最后一个节点设置为currentParent
      },
      chars (text) {
        // 匹配到文本节点的回调
        // 匹配到文本节点后,会对text进一步分析,根据它是静态字符串还是用{{}}绑定的变量字符串,创建不同类型的文本节点,并将其设置为currentParent的子节点
      },
      comment (text) {
        // 匹配到注释节点的回调,其处理逻辑跟文本的处理逻辑类似
      }
    })

    上面就是将模板字符串解析成AST语法树的主体过程,下面我们来看一下paseHTML函数的工作原理。首先,Vue定义了很多匹配HTML的正则表达式:

    这些正则表达式都是匹配“开头”的,所以parseHTML是从头开始匹配字符串,如果被上面定义的某一个正则匹配成功,则会进入相应的处理逻辑,然后调用选项对象中的相应回调;处理完毕后会把刚刚匹配到的部分截取掉,得到一个新的字符串,然后对这个新的字符串进行下一次匹配。下面是一个简单的示例:

    `<div class="my-class"><span> {{ name }} </span></div>`
     
    // 首先会被匹配起始标签开始的正则匹配成功,获取当前的标签名为div,然后截掉匹配成功的'<div'部分,得到新的字符串` class="my-class"><span> {{ name }} </span></div>`
    // 截取掉开始标签后,会使用匹配属性的正则去匹配,如果匹配成功,则得到该标签的属性列表,如果匹配不成功,则该标签的属性列表为空数组,然后截掉' class="my-class"',得到新的字符串`><span> {{ name }} </span></div>`
    // 截掉属性后,会使用匹配开始标签结束的正则去匹配,得到它是否是自闭合标签的信息,然后截掉匹配到的字符串得到新的字符串`<span> {{ name }} </span></div>`
    // 现在第一个匹配开始标签的工作结束了,我们得到了该起始标签的标签名、属性列表信息、是否是自闭合标签,然后调用选项对象中的start回调
     
    // 经过上面的匹配,剩下的字符串部分为:
    `<span> {{ name }} </span></div>`
     
    // 然后进行下一次的匹配尝试,仍然被起始标签的正则匹配成功,然后执行跟上面相同的步骤
     
    // 经过这次匹配后,剩下的字符串为:
    ` {{ name }} </span></div>`
     
    // 上面的正则表达式中并没有能够匹配到上面的字符串,我们可以通过判断它是不是以'<'为开头来判定它是不是字符串,如果它是字符串,则通过下一个下一个'<'的位置来获取文本的结束
    // 这样就可以得到当前的文本节点了,然后将匹配到的字符串截取掉,并调用选项对象中的chars回调
     
    // 经过上面的匹配,剩下的字符串为:
    `</span></div>`
     
    // 然后进行下一次匹配尝试,这次被匹配结束标签的正则匹配成功,接下来就是将匹配到的内容截取掉,然后调用选项对象中的end回调
     
    // 经过上面的匹配,剩下的字符串为:
    `</div>`
     
    // 它也会被匹配结束标签的正则匹配成功,然后进行跟上面相同的操作。到这里,整个模板字符串就被解析完了

    到现在我们已经完成了解析过程,将模板字符串解析成了AST语法树,接下来就要进入优化阶段了。

    优化阶段

    上面简单介绍过,优化阶段的工作就是标记静态子树,标记静态子树后主要有以下两个优点:

    • 生成虚拟dom的过程中,如果发现一个节点是静态子树,除了首次渲染外不会生成新的子节点树,而是拷贝已存在的静态子树;
    • 比对虚拟dom的过程中,如果发现当前节点是静态子树,则直接跳过,不需要进行比对。

    标记静态子树的过程分为两个步骤:

    • 遍历AST语法树,找出所有的静态节点并打上标记(注:当前节点及其所有子节点都是静态节点,当前节点才会被打上静态节点的标记)
    • 遍历经过上面步骤后的树,找出静态根节点,并打上标记(注:静态根节点是指本身及所有子节点都是静态节点,但是父节点为动态节点的节点,找到了静态根节点也就找到了“静态子树”)

    标记静态节点的代码:

    function markStatic (node) {
      // 判断当前节点是否为静态节点,isStaic是判断节点是否为静态节点的方法
      node.static = isStatic(node)
      // 如果当前节点为元素类型,则递归遍历它的子节点,并进行标记
      if (node.type === 1) {
        for (child of node.children) {
          // 标记子节点是否为静态节点
          markStatic(child)
           
          if (!child.static) {
            // 如果子节点不是静态节点,则其父节点也不是静态节点,所以重新标记node为不是静态节点
            node.static = false
          }
        }
      }
    }

    通过上面的方法,我们可以递归遍历AST语法树,并给所有节点打上是否为静态节点的标签。接下来就该标记静态根节点了:

    function markStaticRoot (node) {
      // 如果当前节点不是元素类型,则不可能是静态根节点,直接return
      if (node.type !== 1) return
      if (node.static) {
        // 如果当前节点是静态根,则将其标记为静态根节点,然后直接返回,不再判断它的子节点
        node.staticRoot = true
        return
      }
      // 如果当前节点不是静态根节点,并且存在子节点,则继续向下查找
      if (node.children) {
        for (child of node.children) {
          markStaticRoot(child)
        }
      }
    }
     
    // 注:Vue源码中基于优化的成本和收益考虑,有些静态根节点并没有被标记,这里为了讲解方便省略了那部分逻辑

    现在,我们已经把所有的静态子树都打上标记了,优化阶段也就完成了,下面进入代码生成阶段。

    代码生成阶段

    代码生成阶段的核心是通过AST语法树生成代码字符串,我们先来看一下代码字符串是什么。

    下面是上面截图中的模板生成的代码字符串:

    我们将它格式化来看一下:

    with (this) {
      return _c(
        'div',
        { attrs: {"id": "app"} },
        [
          _c(
            'div',
            { staticClass: "class-name", attrs: { "title": `title`} },
            [
              _v(" "+_s(name)+" ")
            ]
          ),
          _v(" "),
          _c(
            'div',
            [_v("tetttt")]
          )
        ]
      )
    }

    看到上面的格式化后的字符串,我们很容易想到render函数。的确,上面的代码通过new Function(stringCode)就可以得到当前组件的render函数了,还函数会通过“with”语法将this上的属性和数据解析成变量,比如:代码字符串中的_c相当于this._c,name相当于this.name。_c、_v这些变量其实就是vue创建不同类型虚拟dom节点的方法,比如_c就是我们在写render函数时非常熟悉的创建元素类型节点的createElement方法,_v是创建文本类型节点的方法。

    下面我们来看下Vue是怎样将AST语法树编译成代码字符串的:

    // 生成一个节点的代码字符串的方法,他会根据节点类型的不同调用生成不同类型节点代码字符串的方法,并返回生成的代码字符串
    // state参数可以理解为一个挂载了选项参数及工具方法的对象,他会在生成元素类型节点时用到,这里不作深入了解
    function genNode (node, state) {
      if (node.type === 1) {
        return genElement(node, state)
      } else if (node.type === 3 && node.isComment) {
        return genComment(node)
      } else {
        return genText(node)
      }
    }
     
    // 生成元素类型节点的代码字符串
    function genElement (el, state) {
      // 生成_c方法接收的第二个参数(属性选项字符串),如果当前节点没有任何属性,则给一个undefined
      const data el.plain ? undefined : genData(el, state)
     
      // 生成当前节点子节点的代码字符串
      const children = genChildren(el, state)
     
      const code = `_c('${tag}'${data ? `,${data}` : ''})${children ? `,${children}` : ''}`
     
      return code
    }
     
    // 生成子节点的代码字符串
    function genChildren(el, state) {
      const children = el.children
      if (children.length) {
        return `[${children.map(c => genNode(c, state)).join(',')}]`
      }
    }
     
    // 生成注释类型、文本类型节点的字符串这里不再介绍了,有兴趣的同学可以看源码
     
    // 现在,我们只需要将上面生成的AST作为参数,调用以下genElement方法就可以得到代码字符出串了
    const code = genElement(ast, state)
     
    // 上面的代码中虽然没有显示的递归,但是genElement调用genChildren,genChildren调用genNode,genNode再调用genElement,实际上也实现了一个递归调用的过程,从而将整棵AST语法树编译成我们需要的字符
     
    // 最后,我们将上面得到的字符串用with包裹一下,然后就可以通过function构造器实例化出render函数了
     
    const render = new Function(`with(this){return ${code}}`)

    现在,我们已经得到了render函数,整个编译过程也就结束了。

     

    注:本文对vue模板编译原理的介绍是基于vue2的;为了优化性能,vue3对模板编译的过程进行了一些修改,感兴趣的同学可以去探索一下。

    展开全文
  • vue的template里可以填充变量...1、解析器:模板解析成AST(抽象语法树); 2、优化器:遍历AST标记静态节点,这样在虚拟DOM更新节点时避免重新渲染静态节点; 3、代码生成器:使用AST生成render函数。 一、解析
    vue的template里可以填充变量、表达式、指令,这些在HTML没有的功能就是靠模板编译实现的。模板编译的作用就是将模板转换为渲染函数,渲染函数执行的时候都会生成当前最新的vnode进行页面渲染。
    模板--[输入]-->模板编译--[输出]-->渲染函数
    这个过程分为三个步骤:
    1、解析器:模板解析成AST(抽象语法树);
    2、优化器:遍历AST标记静态节点,这样在虚拟DOM更新节点时避免重新渲染静态节点;
    3、代码生成器:使用AST生成render函数。

    一、解析器

    AST与vnode类似,都是通过js对象来表示节点,比如parent记录父节点,type表示节点类型,children记录子节点这样,所以AST就是一个用js对象描述的节点树。

    解析器又分成过滤器解析器、文本解析器(解析类似Hello {{ name }}这种带变量的文本)和HTML解析器(核心,解析模板),这些解析器通过主线被组装在一起。

    1、解析器内部运行原理:

    主要的就是HTML解析器,在解析的过程中触发各种钩子函数:

    • 开始标签钩子函数start(tag:标签名, attrs:属性, unary:是否是自闭合标签)

    • 结束标签钩子函数end()

    • 文本钩子函数chars(text)

    • 注释钩子函数comment(text)

    比如:

    <div><p>Hello</p></div>

     从前向后解析依次就会触发start, start, chars, end, end。

    但是AST是有层级关系的,所以需要维护一个栈来记录这层关系,即DOM的深度。因为是从前往后解析,遇到节点就压入栈,所以栈的最里面是根节点,最外面就是当前正在构建的节点的父节点,因此栈也可以用来检测HTML标签是否正确闭合,只要当读取结束标签时,发现栈顶却是另外的标签,那么就警告。在解析过程中不断判断模板的开始标签、文本标签、结束标签、注释从而进行出栈入栈、触发相应的钩子函数和截掉已解析模板,最后知道模板空并且栈为空,说明解析结束得到最终AST。

    所以AST的构建和HTML解析器里各个钩子函数密切相关,接下来看看具体的HTML解析器的运行原理:

    2、HTML解析器运行原理:

    说白了就是对HTML模板字符串进行循环,每次循环截取一段字符串,触发相应钩子函数,重复这个过程,直到剩下的模板为空。

    parseHTML(html, options)函数定义在src/compiler/parser/html-parse.js中。

    整个解析逻辑包在while循环里,while循环结束,表示解析结束。每次被截取到的片段有很多种,比如开始标签,结束标签,HTML注释(<!-- xxx -->),DOCTYPE,文本,条件注释。

    怎么识别是开始标签这些东西呢?当然上正则啦。

    对于开始标签:

    const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`

    const qnameCapture = `((?:${ncname}\\:)?${ncname})`

    const startTagOpen = new RegExp(`^<${qnameCapture}`)

     例如<div class="box"></div>这段来match(startTagOpen),得到的数组是["<div", "div", index: 0, input: "<div class="box"></div>"]。所以只能拿到tag的信息,对于标签的属性和是否自闭合,还需要进一步解析。

    (1)属性:拿到开始标签后,就会被截走,于是剩下'class="box"></div>',靠正则去解析属性每次只能识别出一个,如果是多个属性存在,也是需要用while循环来一点点解析的。然后将每次解析到的属性push到attrs数组中去。

    (2)自闭合标识:例如<input type="text" />这种,通过startTagClose正则得出,如果是自闭合标签,那么unarySlash就是“/”,否则就是空字串。来用于判断该标签是否需要放入栈中(毕竟自闭合标签没有子节点不需要入栈)。

    其他的截取操作其实都是大同小异。那么有个问题,在文本截取中,如果<本身就是文本的一部分呢?

    比如剩下的模板字符串是“1<2</div>”这样,如果只是简单判断<前面是文本,那么只能得到1。

    思路就是查看1被截取后,剩余的模板中“<2”是否符合前面那些标签的特征,如果不符合,就说明这个<是文本的一部分。之后就把这个<和下一个<之间的字符串截出来加到1上,之后重复判断后面的模板直到文本解析完。

     还有一点要注意的就是纯文本内容元素的处理,也就是script、style和textarea三种元素,这个在代码中是有别与之前正常元素的处理逻辑,纯文本内容元素都会被视作文本处理,也就是把这些文本截取出来然后触发chars函数。

    3、文本解析器chars():

    文本解析器是对HTML解析器解析出来的文本进行二次加工,因为文本分为两类:纯文本、带变量的文本( 例如:Hello {{name}})。所以这种带变量的文本在虚拟DOM渲染的时候,需要将变量替换为真实值。

    大致思路:

    带不带变量文本的判断通过parseText()函数得出,如果该函数有返回结果,则构建一个带变量的文本类AST并push到父节点的children里,否则就是push一个普通文本节点。

    最后Hello {{name}} 将会被解析为'"Hello" +_s(name)'。

    内部实现原理:

    • 首先使用正则判断是否带变量,也就是是否含有{{xxx}}这样的语法,/\{\{((?:.|\r?\n)+?)\}\}/g,如果是纯文本就直接return;

    • 如果有变量,则先把变量左边的文本添加到数组中,然后把当前变量变成_s(x)的形式加入数组;

    • 重复步骤2直到所有变量都添加了;

    • 如果最后一个变量后面还有文本,则添加到数组中。

     4、整体流程总结:

    最后自己总结着画了一张流程图

    下一篇:VUE2.0 模板编译原理(二):优化器

    ——以上内容学习自《深入浅出Vue.js》

    展开全文
  • Vue模板渲染原理

    千次阅读 2019-09-14 14:10:22
    Vue模板渲染原理 1、概念 模板:本质字符串。 与字符串的区别: 有逻辑(vue中有v-if、v-for) 嵌入js变量({{变量}}) 需要转化为html,页面才会识别并正确解析。 2、过程理解 目的:理解模板(字符串)----...
  • Vue模板编译原理详解

    2021-08-13 15:02:06
    编译器的主要作用是将 .vue模板编译为render函数,因为在开发的时候,写render函数不符合我们的开发习惯,所以我们平常开发用的都是runtime+complier的版本。而项目打包时,就将编译的工作交由webpack来执行打包...
  • Vue 模板编译原理

    2019-12-31 08:04:10
    关于vue的内部原理其实有很多个重要的部分,变化侦测,模板编译,virtualDOM,整体运行流程等。之前写过一篇《深入浅出 - vue变化侦测原理》讲了关于变化侦测的实现原理。那今天...
  • 转自微信公众号:前端时空; 来源于微信公众号:更了不起的前端; 作者: shenfq 写在开头 ...Vue 3 发布之后,本来想着直接看看 Vue 3 的模板编译,但是我打开 Vue 3 源码的时候,发现我好像连 Vue .
  • 文章目录流程总览数据代理模板解析数据绑定 流程总览 对Vue实例中的属性实现数据代理–>利用observer对象并监视其变化–风吹草动–>代理至me._data[key] = newVal;(注意此时只没有更新)–>触发observer中...
  • Vue源码之模板编译原理

    千次阅读 2019-12-24 21:33:44
    模板编译整理流程 解析器——AST HTML解析器 文本解析器 过滤器解析器 优化器 代码生成器 模板编译整体流程 在Vue中我们有三种方式来创建HTML 模板 手动写渲染函数 JSX 渲染函数是最原始的方法,而模板最终会...
  • 通过查看vue源码,可以知道Vue源码中使用了虚拟DOM(Virtual Dom)...本文通过对Vue源码中的AST转化部分进行简单提取,返回静态的AST结构(不考虑兼容性及属性的具体解析)。并最终根据一个实例的template转化为最终的A...
  • mustache 是 “胡子”的意思,因为它的嵌入标记 {{ }} 旋转过来很像胡子,Vue中的 {{ }} 语法也引用了mustache,这也是我深入学习的目的。 1、原始js方式使 数据 变为视图 <ul id="list"></ul> <...
  • 但是我们也知道,Vue 底层是通过虚拟 DOM 来进行渲染的,那么 .vue 文件的模板到底是怎么转换成虚拟 DOM 的呢?这一块对我来说一直是个黑盒,之前也没有深入研究过,今天打算一探究竟。Vue 3 发布在即,本来想着直接...
  • 接上一篇内容: VUE2.0 模板编译原理(一):解析器 二、优化器 优化器的作用是在AST中找到静态子树并打上标记,即那些永远不会发生变化的节点,例如纯文本节点。 打标记的作用: 在生成VNode的过程中,除了首次...
  • vue模板编译 模板编译的概念 在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数,渲染函数的执行就会产生最新状态下的vnode,然后使用这个vnode进行重新渲染视图 模板编译的作用:输入模板,输出渲染函数 Vue.js...
  • vue核心面试题:vue模板编译原理

    千次阅读 2020-07-15 16:41:28
    一、vue怎么将template 转化成 render 函数 1.源码文件路径:src/compiler/index.js // 创建了一个编辑器 export const createCompiler = createCompilerCreator(function baseCompile ( template: string, ...
  • vue核心原理解析

    千次阅读 2020-07-11 06:54:58
    1. MVVM原理 vue的精髓在于组件化和数据驱动视图。 组件化之前就有,比如php等语言,vue使用了组件化思想。 进行了数据驱动视图的创新。之前的前端操作是静态渲染,要操作dom修改视图。 数据劫持的优势 无需显示...
  • 第一种就是经过模板编译生成 render 函数 第二种是我们自己在组件里定义了 render 函数,这种会跳过模板编译的过程 <template></template> 这个是模板,不是真实的 HTML,浏览器是不认识模板的,所以...
  • Vue 模板

    千次阅读 2020-06-07 22:09:31
    原文 在Vue中,Vue模板对应的就是Vue中的View(视图)部分,也是Vue重中之一,而在Vue中要了解Vue模板我们就需要从两个方面来着手,其一是Vue的模板语法,其二就是模板渲染。Vue模板语法是Vue中常用的技术之一,除非...
  • Vue 的编译模块包含 4 个目录: 1 2 3 4 compiler-core compiler-dom // 浏览器 compiler-sfc // 单文件组件 compiler-ssr // 服务端渲染 其中 compiler-core 模块是 Vue 编译的...其中 parse 阶段将模板字符串转化为
  • vue组件实现原理解析

    2020-11-07 18:00:00
    组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的同时,也提供了极好的复用性和可维护性。本文我们一起从源码的角度,了解一下组件的底层实现原理。组件注册...
  • Vue模板语法

    2020-12-29 01:05:57
    1、Vue,渐进式Javascript框架。渐进式的含义理解,从左到右递进关系,声明式渲染(最简单的模式)->组件系统(组件化)->客户端路由(局部更新,浏览器的历史回退功能)->集中式状态管理(Vuex做状态管理)->...
  • 概念 平时使用模板时,可以在模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那vue中为什么可以实现?...此过程可以分成两个步骤:先将模板解析成AST(abstract syntax tree,抽象...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 15,696
精华内容 6,278
关键字:

vue模板解析原理