精华内容
下载资源
问答
  • 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 模板编译原理

    千次阅读 2019-03-19 21:16:37
    关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,这三个部分是有前后关系的: 第一步是将 模板字符串 转换成 element ASTs(解析器) 第二步是对 AST 进行静态节点标记,主要用来做虚拟...

    关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,这三个部分是有前后关系的:

    • 第一步是将 模板字符串 转换成 element ASTs(解析器)

    • 第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)

    • 第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)

     

     

    来源于:http://www.sohu.com/a/212505132_500651

    展开全文
  • 本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成...

    本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。

    这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏。

    编译过程

    模板编译是Vue中比较核心的一部分。关于Vue编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:

    第一步:将模板字符串转换成element ASTs(解析器)

    第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)

    第三步:使用element ASTs生成render函数代码字符串(代码生成器)

    对应的Vue源码如下,源码位置在src/compiler/index.js

    export const createCompiler = createCompilerCreator(function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      // 1.parse,模板字符串 转换成 抽象语法树(AST)
      const ast = parse(template.trim(), options)
      // 2.optimize,对 AST 进行静态节点标记
      if (options.optimize !== false) {
        optimize(ast, options)
      }
      // 3.generate,抽象语法树(AST) 生成 render函数代码字符串
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })
    

    这篇文档主要讲第一步将模板字符串转换成对象语法树(element ASTs),对应的源码实现我们通常称之为解析器。

    解析器运行过程

    在分析解析器的原理前,我们先举例看下解析器的具体作用。

    来一个最简单的实例:

    <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中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。

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

    我们先看下解析器整体的代码结构,源码位置src/compiler/parser/index.js

    parseHTML(template, {
      warn,
      expectHTML: options.expectHTML,
      isUnaryTag: options.isUnaryTag,
      canBeLeftOpenTag: options.canBeLeftOpenTag,
      shouldDecodeNewlines: options.shouldDecodeNewlines,
      shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
      shouldKeepComment: options.comments,
      outputSourceRange: options.outputSourceRange,
      // 每当解析到标签的开始位置时,触发该函数
      start (tag, attrs, unary, start, end) {
        //...
      },
      // 每当解析到标签的结束位置时,触发该函数
      end (tag, start, end) {
        //...
      },
      // 每当解析到文本时,触发该函数
      chars (text: string, start: number, end: number) {
        //...
      },
      // 每当解析到注释时,触发该函数
      comment (text: string, start, end) {
        //...
      }
    })
    

    实际上,模板解析的过程就是不断调用钩子函数的处理过程。整个过程,读取template字符串,使用不同的正则表达式,匹配到不同的内容,然后触发对应不同的钩子函数处理匹配到的截取片段,比如开始标签正则匹配到开始标签,触发start钩子函数,钩子函数处理匹配到的开始标签片段,生成一个标签节点添加到抽象语法树上。

    还举上面那个例子来说:

    <div>
      <p>{{name}}</p>
    </div>
    

    整个解析运行过程就是:解析到

    时,会触发一个标签开始的钩子函数start,处理匹配片段,生成一个标签节点添加到AST上;然后解析到

    时,又触发一次钩子函数start,处理匹配片段,又生成一个标签节点并作为上一个节点的子节点添加到AST上;接着解析到{{name}}这行文本,此时触发了文本钩子函数chars,处理匹配片段,生成一个带变量文本(变量文本下面会讲到)标签节点并作为上一个节点的子节点添加到AST上;然后解析到

    ,触发了标签结束的钩子函数end;接着继续解析到
    ,此时又触发一次标签结束的钩子函数end,解析结束。

    正则匹配

    模板解析过程会涉及到许许多多的正则匹配,知道每个正则有什么用途,会更加方便之后的分析。

    那我们先来看看这些正则表达式,源码位置在src/compiler/parser/index.js

    export const onRE = /^@|^v-on:/
    export const dirRE = process.env.VBIND_PROP_SHORTHAND
      ? /^v-|^@|^:|^\.|^#/
      : /^v-|^@|^:|^#/
    export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
    export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
    const stripParensRE = /^\(|\)$/g
    const dynamicArgRE = /^\[.*\]$/
    
    const argRE = /:(.*)$/
    export const bindRE = /^:|^\.|^v-bind:/
    const propBindRE = /^\./
    const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
    
    const slotRE = /^v-slot(:|$)|^#/
    
    const lineBreakRE = /[\r\n]/
    const whitespaceRE = /\s+/g
    
    const invalidAttributeRE = /[\s"'<>\/=]/
    

    上面这些正则相对来说比较简单,基本上都是用来匹配Vue中自定义的一些语法格式,如onRE匹配 @ 或 v-on 开头的属性,forAliasRE匹配v-for中的属性值,比如item in items、(item, index) of items。

    下面这些就是专门针对html的一些正则匹配,源码位置在src/compiler/parser/html-parser.js

    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${unicodeRegExp.source}]*`
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    const startTagOpen = new RegExp(`^<${qnameCapture}`)
    const startTagClose = /^\s*(\/?)>/
    const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
    const doctype = /^<!DOCTYPE [^>]+>/i
    const comment = /^<!\--/
    const conditionalComment = /^<!\[/
    

    这些正则表达式相对来说就复杂一些,如attribute用来匹配标签的属性,startTagOpen、startTagClose用于匹配标签的开始、结束部分等。这些正则表达式的写法就不多说了,有兴趣的朋友可以针对这些正则一个一个的去测试一下。

    HTML解析器

    这里我们来看看HTMl解析器。

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

    我们通过源码,就可以看到整个函数逻辑就是被一个while循环包裹着。源码位置在:src/compiler/parser/html-parser.js

    export function parseHTML (html, options) {
      const stack = []
      const expectHTML = options.expectHTML
      const isUnaryTag = options.isUnaryTag || no
      const canBeLeftOpenTag = options.canBeLeftOpenTag || no
      let index = 0
      let last, lastTag
      while (html) {
        //...
      }
      
      parseEndTag()
      
      //...
    }
    

    下面我用一个简单的模板,模拟一下HTML解析的过程,以便于更好的理解。

    <div>
      <p>{{text}}</p>
    </div>
    

    最初的HTML模板:

    <div>
      <p>{{text}}</p>
    </div>
    

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

    ,解析出是div开始标签并且触发钩子函数start,截取后的结果为:

    
      <p>{{text}}</p>
    </div>
    

    第二轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:

      <p>{{text}}</p>
    </div>
    

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

    ,解析出是p开始标签并且触发钩子函数start,截取后的结果为:

      {{text}}</p>
    </div>
    

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

      </p>
    </div>
    

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

    ,解析出是p闭合标签并且触发钩子函数end,截取后的结果为:
    
    </div>
    

    第六轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:

    </div>
    

    第七轮循环时,截取出一段字符串,解析出是div闭合标签并且触发钩子函数end,截取后的结果为:

    第八轮循环时,发现只有一个空字符串,解析完毕,循环结束。

    现在,是不是就对HTML解析过程很清楚了。其实循环过程对每次匹配到的片段进行分析记录还是很复杂的,因为被截取的片段分很多种类型,比如:

    开始标签,例如<div>

    结束标签,例如</div>

    HTML注释,例如<!-- 注释 -->

    DOCTYPE,例如<!DOCTYPE html>

    条件注释,例如<!--[if !IE]>-->注释<!--<![endif]-->

    文本,例如’字符串’

    对每个片段的具体处理这里就不说了,有兴趣的直接看源码去。

    文本解析器

    文本解析器是对HTML解析器解析出来的文本进行二次加工。文本其实分两种类型,一种是纯文本,另一种是带变量的文本。如下:

    这种就是纯文本:

    这里有段文本
    

    这种就是带变量的文本:

    文本内容:{{text}}
    

    上面HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。

    我们知道,HTML解析器在碰到文本时,会触发chars钩子函数,我们先来看看钩子函数里面是怎么区分普通文本和变量文本的。

    源码位置在:src/compiler/parser/html-parser.js

    chars (text: string, start: number, end: number) {
      //...
      let child: ?ASTNode
      if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
        child = {
          type: 2,
          expression: res.expression,
          tokens: res.tokens,
          text
        }
      } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
        child = {
          type: 3,
          text
        }
      }
      //...
      children.push(child)
    }
    

    我们重点看res = parseText(text,delimiters)这一行,通过条件判断设置不同的类型。事实上type=2表示表达式类型,type=3表示普通文本类型。

    我们再来看看parseText函数具体做了什么

    export function parseText (
      text: string,
      delimiters?: [string, string]
    ): TextParseResult | void {
      const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
      // 匹配不到带变量时直接返回了
      if (!tagRE.test(text)) {
        return
      }
      const tokens = []
      const rawTokens = []
      let lastIndex = tagRE.lastIndex = 0
      let match, index, tokenValue
      // 对匹配到的变量循环处理成表达式
      while ((match = tagRE.exec(text))) {
        index = match.index
        // push text token
        // 先把 { { 前边的文本添加到tokens中
        if (index > lastIndex) {
          rawTokens.push(tokenValue = text.slice(lastIndex, index))
          tokens.push(JSON.stringify(tokenValue))
        }
        // tag token
        const exp = parseFilters(match[1].trim())
        // 使用_s对变量进行包装
        // 把变量改成`_s(x)`这样的形式也添加到数组中
        tokens.push(`_s(${exp})`)
        rawTokens.push({ '@binding': exp })
        // 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
        lastIndex = index + match[0].length
      }
      // 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
      if (lastIndex < text.length) {
        rawTokens.push(tokenValue = text.slice(lastIndex))
        tokens.push(JSON.stringify(tokenValue))
      }
      return {
        expression: tokens.join('+'),
        tokens: rawTokens
      }
    }
    

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

    那么对于上面示例处理结果如下:

    parseText('这里有段文本')
    // undefined
    
    parseText('文本内容:{{text}}')
    // '"文本内容:" + _s(text)'
    

    好了,对于文本解析器就这么多内容。

    总结一下

    模板解析是Vue模板编译的第一步,即通过模板得到AST(抽象语法树)。

    生成AST的过程核心就是借助HTML解析器,当HTML解析器通过正则匹配到不同的片段时会触发对应不同的钩子函数,通过钩子函数对匹配片段进行解析我们可以构建出不同的节点。

    文本解析器是对HTML解析器解析出来的文本进行二次加工,主要是为了处理带变量的文本。

    相关

    展开全文
  • vue模板编译 模板编译的概念 在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数,渲染函数的执行就会产生最新状态下的vnode,然后使用这个vnode进行重新渲染视图 模板编译的作用:输入模板,输出渲染函数 Vue.js...

    vue模板编译

    模板编译的概念

    在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数,渲染函数的执行就会产生最新状态下的vnode,然后使用这个vnode进行重新渲染视图

    模板编译的作用:输入模板,输出渲染函数

    Vue.js中将模板编译成渲染函数的步骤:

    1. 将模板解析为AST 解析器完成
    2. 遍历AST标记静态节点 优化器完成
    3. 使用AST生成渲染函数 代码生成器完成

    备注:AST即抽象语法树,是用于描述一个节点信息的JavaScript对象

    整体pipeline:模板->解析器->优化器->代码生成器->渲染函数

    解析器

    解析器的作用:解析器将模板解析成AST,解析器内部又有很多小的解析器,比如HTML解析器,文本解析器,过滤解析器

    AST(abstract syntax tree):是用JavaScript中的对象来描述一个节点(对象中的属性保存了节点的各种数据),一个对象表示一个节点

    <div>
    	<p>{{name}}</p>
    </div>
    

    以上的html代码转换成AST后

    {
    	tag: "div",
    	type: 1,
    	plain: true,
    	parent: undefined,
    	attrsList: [],
    	attrsMap: {},
    	children: [
    		{
    			tag: "p",
    			type: 1,
    			parent: {tag: "div"},
    			children: [{
    				type: 2,
    				text: "{{name}}",
    				expression: "_s(name)"
    			}]
    			//...
    		}
    	]
    }
    

    当很多个独立的节点通过parent属性和children属性连在一起,就变成了一棵树,而这样一个用对象描述的节点树其实就是AST

    用栈来构建AST层级关系

    将节点连成一棵树,或者说构建AST层级关系我们只需要维护一个栈即可,用栈来记录层级关系,栈顶元素一定是下一个入栈元素的父元素,下一个入栈元素就是栈顶元素的子元素

    这个层级关系也可以理解为DOM的深度

    基于HTML解析器的逻辑,当开始解析一个标签时,把当前节点入栈,当解析结束时,把栈顶元素弹出

    那么,我们如何知道什么时候开始解析,什么时候解析结束?-----> 钩子函数

    解析器中的钩子函数

    解析器中HTML解析器是最主要的,用于解析HTML,并且在解析过程中会不断触发各种钩子函数

    start end chars commet
    
    • start 解析到一个元素标签开始标签文本节点时触发
    • end 解析元素结束标签时触发
    • chars 解析文本时触发
    • comment 解析到注释时触发
    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)
    	}
    	end(){}
    	chars(text){
    		let element = {type: 3, text}
    	}
    	comment(text){
    		let element = {type: 3, text, isComment: true}
    	}
    })
    

    当解析到一个开始标签或文本节点时,会产生一个AST节点对象并把其压入栈中

    当解析到一个结束标签时,将栈顶元素弹出

    解析HTML过程就是循环匹配的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML代码中截取一小段字符串

    如何精确地匹配或截取那些字符串呢? -----> 需要正则表达式的帮助

    优化器

    优化器的作用:遍历AST,检测出所有静态子树(即永远都不会发生变化的DOM节点)并给其打上标记

    静态子树是指那些在AST中永远都不会发生变化的节点,比如纯文本节点,并且静态节点的特征是除了自身是静态节点外,它的子节点必须是静态节点

    标记静态子树的好处

    • 每次重新渲染时,不需要为静态子树创建新节点,而是克隆已存在的静态子树
    • 在虚拟DOM中打补丁的过程中将跳过比对和更新过程

    优化器内部实现主要分为两个步骤

    1. 在AST中找出所有静态节点并打上标记(static)
    2. 在AST中找出所有静态根节点并打上标记(staticRoot)

    那么,标记是什么呢?

    // 在AST对象中会新增如下两个属性
    static: true,
    staticRoot: false
    

    优化器框架

    export function optimize(root){
    	if(!root) return;
    	// 第一步:从根出发递归标记所有静态节点
    	markStatic(root);
    	// 第二部:标记所有静态根节点
    	markStaticRoots(root);
    }
    

    找出所有静态节点

    补充知识:关于AST中type类型

    1. type===1 元素节点
    2. type===2 带变量的动态文本节点
    3. type===3 不带变量的纯文本节点
    function isStatic(node){
    	if (node.type === 2){
    		return false;
    	}
    	if (node.type === 3){  //不带变量的纯文本节点直接返回是静态节点
    		return true;
    	}
    	return !!(node.pre || (
    		!node.hasBindings &&
    		!node.if && !node.for &&
    		!isBuiltInTag(node.tag) &&
    		isPlatformReversedTag(node.tag) &&
    		!isDerectChildOfTemplateFor(node) &&
    		Object.keys(node).every(isStaticKey)
    	))
    }
    function markStatic(node){
    	node.static = isStatic(node)
    	if(node.type === 1){ //即元素节点 可能存在子节点
    		for(let i = 0, l = node.children.length; i < l; i++){  //遍历孩子
    			const child = node.children[i];
    			markStatic(child);  //递归mark
    		}
    		if (!child.static){ // 如果子节点是动态节点,则其父节点也修改成动态节点
    			node.static = false;
    		}
    	}
    }
    

    找出所有静态根节点

    静态节点有个特点就是静态节点的所有子节点也都必须为静态节点,那么我们找到第一个静态节点,就一定是一颗静态子树的根节点

    如果找到了第一个静态节点,将其判定为静态根节点,标记staticRoot为true,那么不会继续向他的子级继续寻找

    另外:除了节点不是静态节点我们不会标记为静态根节点,还有

    • 如果一个静态节点的子节点只有一个文本节点
    • 没有子节点的静态节点

    我们不会将它标记为静态根节点,因为这两种情况,优化成本大于收益

    代码生成器

    代码生成器的作用:是模板编译的最后一步,它将AST转换成渲染函数中的内容(这个内容也可以称作代码字符串

    首先看个例子

    <div id="el">
    	<p>Hello {{name}}</p>
    </div>
    
    with(this){return _c("div", {attrs: {"id": "el"}}, [_c("p", [_v("Hello" + _s(name))])])}
    

    代码生成器通过AST递归生成代码字符串,可以发现代码字符串是嵌套的函数调用,函数_c中又有执行其他函数

    三种节点创建方法与别名

    • _c createElement 创建元素节点
    • _v createTextVNode 创建文本节点
    • _e createEmptyVNode 创建注释节点

    其中,_c方法中有三个参数

    1. 标签名 String
    2. 属性 Object
    3. 子节点列表 Array

    代码生成器步骤:genElement->genData->genChildren->genNode(genNode中调用genElement,genComment,genText)

    // el是AST节点
    function genElement(el, state){
    	// plain是在编译过程中发现的,当节点没有属性,将设置plain为true
    	const data = el.plain ? undefined : genData(el, state);
    	const children = genChildren(el, state)
    }
    function genData(el, state){
    	let data = "{";
    	if(el.key){
    		data += `key:${el.key}`
    	}
    	//... 将el中的各属性进行拼接
    }
    function genChildren(el, state){
    	const children = el.children;
    	if(children.length){
    		// 生成子节点并以都好拼接
    		// 模板字符串
    		return `[${children.map(c=>genNode(c, state)).join(',')}]`
    	}
    }
    function genNode(node, state){
    	if(node.type === 1){
    		return genElement(node, state);
    	}
    	if(node.type === 3 && node.isComment){
    		return genComment(node); 
    	}
    	if(node.type === 2){
    		return genText(node);
    	}
    }
    function genComment(node){
    	return `_e(${JSON.stringify(node.text)})`;
    }
    function genText(node){
    	return `_v(${text.type===2? node.expression : JSON.stringify(node.text)})`
    }
    

    JSON.stringify()可以包装一层字符串

    展开全文
  • Vue.js 模板解析原理

    千次阅读 2019-04-03 05:18:13
    本文来自《深入浅出Vue.js》模板编译原理篇的第九章,主要讲述了如何将模板解析成AST,这一章的内容是全书最复杂且烧脑的章节。本文排版较为紧凑和图片是未经加工的原稿,真实纸质书的排版和图片会更加精致。 通过...
  • 本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-...
  • 模板解析阶段, html转换成ast抽象语法树 2. 优化阶段,优化标记静态节点和静态根节点 主要是为了, 提高虚拟DOM中patch过程的性能 3. 代码生成阶段,render函数 组件只要调用的这个render函数就 可以得到...
  • 概念平时使用模板时,可以在模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那...将模板编译成渲染函数此过程可以分成两个步骤:先将模板解析成AST(abstract syntax tree,抽象语法树),然后使用...
  • VUE原理解析

    2020-03-16 09:37:21
    vue双向数据绑定原理 Observer:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 Compile:对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 Watcher:...
  • vue之mvvm原理解析

    2019-10-12 09:01:42
    文章目录mvvm 原理解析mvvm 面试论述mvvm的编译过程以及使用分解vue实例实现Complie编译模板vue类-入口文件编译模板节点转文档碎片编译模板数据劫持数据劫持 observer类订阅者的Watcher的实现订阅者watcher存放订阅...
  • VUE源码解析——模板编译底层原理一、什么是模板编译?二、整体渲染过程三、模板编译内部流程抽象语法树AST总结 一、什么是模板编译? 把写在<template></template>标签中的类似于原生HTML的内容称之为...
  • Vue模板渲染原理

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

    2020-12-29 16:53:37
    模板解析成AST(Abstract Syntax Tree,抽象语法树) 遍历AST标记静态节点。这样在虚拟DOM中更新节点时,如果发现有静态标记,则不会重新渲染它。 使用AST生成渲染函数 二、解析器 解析器内部分了好几个子解析...
  • 概念 平时使用模板时,可以在模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那vue中为什么可以实现?...此过程可以分成两个步骤:先将模板解析成AST(abstract syntax tree,抽象...
  • 1、解析一般指令(以v-text为例) 其他普通指令(v-text, v-model, v-html, v-class)和上面的原理类似 2、解析事件指令(以v-on:click为例) 3、总结 事件指令解析步骤: 1) 从指令名中取出事件...
  • 文章目录流程总览数据代理模板解析数据绑定 流程总览 对Vue实例中的属性实现数据代理–>利用observer对象并监视其变化–风吹草动–>代理至me._data[key] = newVal;(注意此时只没有更新)–>触发observer中...
  • Compile(解析模板、创建 Watcher、保存更新函数) Watcher(执行更新函数、被 Dep 收集) Dep(管理 Watcher、一旦数据更新就通知相应的watcher更新视图) 一、MyVue的构造函数 new MyVue({ el: '#app', data: ...
  • Vue的nextTick原理解析

    千次阅读 2018-04-23 14:44:00
    假设当前的模板代码为&lt;div id="a"&gt;{{a}}&lt;/div&gt;,这时我们在mounted钩子里写下如下的代码: this.a = '纳尼?!'; this.$nextTick(function(){ console.log($('#a')[0]....
  • 只有将模板解析程AST后,才能基于AST优化或者生成代码字符串,那么解析器是如何将模板解析成AST的呢?

空空如也

空空如也

1 2 3 4 5 ... 15
收藏数 289
精华内容 115
关键字:

vue模板解析原理

vue 订阅