Vue 模版编译

模版编译

如何识别用户自己定义的模版,也就是 标签中类似于原生 HTML,还会有一些变量插值,或一些 Vue 指令,如 v-on、v-if 等。

这些不属于元素的语法,归功于 Vue 的模版编译。把原生 HTML 提取出来,把非原生 HTML 找出来,经过一系列的逻辑处理生成渲染函数,也就是 render 函数。

抽象语法树 AST

如何将 template 中的一堆字符串解析为元素标签、属性、变量。需要借助抽象语法树.

  1. 模版解析阶段:使用正则的方式解析成抽象语法树 AST。

  2. 优化阶段:遍历 AST,找出静态节点,打上标记。

  3. 将 AST 转换为渲染函数。

// src/compiler/index.js

const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 模版解析阶段:用正则等方式解析 template 模版中的数据。
  const ast = parse(template.trim(), options)

  // 优化阶段,遍历 AST,找出静态节点,打上标记。
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 代码生成阶段:将 AST 转为渲染函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

模版编译之生成AST

解析 template 对应 parser 模块。定义了解析阶段主线函数 parse 函数。在 template 中除了常规的 HTML 标签外,还有一些文本信息以及文本中包含过滤器。

所以在解析整个模版的时候是以 HTML 解析器为主线,先用 HTML 解析器进行解析整个模版(parseHTML),在解析的过程中遇到文本就调用文本解析器解析文本(parseText), 如果碰到文本中包含过滤器就调用过滤器解析器来解析(parseFilters).

// src/compiler/parser/index.js
function parse (template, options) {
    parseHTML (template, {
        // ...
    })
}

HTML 解析器

// src/compiler/parser/index.js
function parse (template, options) {
    parseHTML (template, {
        warn,
        expectHTML: options.expectHTML,
        isUnaryTag: options.isUnaryTag,
        canBeLeftOpenTag: options.canBeLeftOpenTag,
        shouldDecodeNewlines: options.shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
        shouldKeepComment: options.comments,

        // 当解析道开始标签时,调用该函数
        start(tag, attrs, unary) {},

        // 当解析到结束标签时,调用该函数
        end() {},

        // 当解析到文本时,调用该函数
        chars(text) {},

        // 当解析到注释时,调用该函数
        comment(text) {}
    })
}

template 就是模版字符串,options 是解析时用到的一些参数,同时还定义了四个钩子函数。用这四个钩子函数将解析出来的不同内容生成对应的 AST。

根据正则匹配不同的内容

我们通常写的 template 模版中一般会包含这些内容:

  • 文本 ‘我是文本’
  • HTML 注释
  • 开始标签
  • 结束标签
  1. 解析注释
const comment = /^<!\--/
if (comment.test(html)) {
    const commendEnd = html.indexOf('-->')
    if (commentEnd >= 0) {
        // 提取注释节点,创建注释类型的AST节点
        options.comment(html.substring(4, commentEnd))
    }
    // 将游标移动到 --> 之后,继续解析
    advance(commentEnd + 3)
    continue;
}

function advance (n) {
    index += n;
    html = html.substring(n)
}

每次解析完之后将游标向后移动一段,接着再从解析游标后解析,保障解析过的内容不会被重复解析。

  1. 条件注释

  2. 开始标签
    需要通过正则去匹配到是否具有开始标签的特征, 然后需要解析标签的属性。通过解析标签是否是自闭合的。 然后会调用 start 钩子函数创建元素的 AST 节点的所有过程。

  3. 解析结束标签
    只需要判断剩下的模版字符串是否符合结束标签的特征。如果符合,就调用 4 个钩子函数中的 end 函数就好。

  4. 解析文本
    先找到第一个 < 开始的位置,如果第一个 < 在第一个位置,说明是其他五种类型,如果第一个 < 不在第一个位置,说明模版字符串是以文本开头的,到第一个 < 的位置都是文本。如果找不到 < , 代表都是文本。

如果遇到一个 < , 后面的内容匹配不到其他几种情况,就认为是出现了 1<2, 先截取出来 < 前出现的文本。再去匹配。

最后截取文本内容 text 并调用 4 个钩子函数中的 chars 函数创建 AST 节点。

保证AST节点的层级关系

创建出不同的类型的 AST 类型,但是真正的 DOM 是具有层次结构的。

Vue 在 HTML 解析器的开头定义了一个栈结构。用这个栈来维护层级。HTML 解析器从前向后解析,如果解析到开始标签,就调用 start 钩子函数,在 start 钩子函数内部可以将解析到的开始标签推入到栈中,遇到结束标签就调用 end 钩子函数,在 end 钩子函数内部将解析得到的结束标签同对应的开始标签从栈中弹出。

如果没有正确的闭合,就会报警告。 tag has no matching end tag.

文本解析器

HTML 解析器会将匹配到的文本内容调用 chars 函数来创建文本型的 AST 节点。然后 chars 函数会根据文本内容是否包含变量在细分为创建含有变量的 AST 节点和不包含 AST 的节点。

chars (text) {
  if(res = parseText(text)){
       let element = {
           type: 2,
           expression: res.expression,
           tokens: res.tokens,
           text
       }
    } else {
       let element = {
           type: 3,
           text
       }
    }
}

创建包含变量的 AST 节点时节点的 type 属性为 2. 还会多出两个属性: expression 和 tokens。

把解析到的文本 text 传递给 parseText 函数,根据 parseText 函数解析返回值判断是否包含变量,以及从返回值中取到需要的 expression 和 tokens。

let text = '我的账号 {{username}} , 我的密码 {{password}}'

let res = parseText(text);
res = {
    expression: '我的账号' + _s(username)+', 我的密码' + _s(password),

    tokens: [
        '我的账号',
        {'@binging': username},
        ', 我的密码',
        {'@binding': password}
    ]
}

生成的这个主要是为了给 render 函数使用。

模版编译之优化阶段

经过解析阶段,已经已经生成好 AST 语法树,然后进入生成 render 函数。但是 Vue 注重性能。在生成好 AST 后,多进行一步优化阶段。 虚拟 DOM 在对比的时候,如果新旧VNode都是静态几点,就可以直接退出程序。

<ul>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
</ul>

上面的就是静态节点。 并且将 ul 标记为静态根节点。li 为静态节点。

通过 patch 过程中,可以直接忽略静态节点,可以提高性能。

优化阶段干的事情就是

  1. 在 AST 中找出静态节点打上标记。
  2. 在 AST 中找出所有静态根节点并标记。
// src/compiler/optimizer.js

function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

模版编译之生成render函数

Vue 实例在挂载的时候就是调用自身的 render 函数来生成实例上的 template 选项所对应的 VNode.

render 函数可以是由用户定义,也可以是系统生成。我们平时在开发中,可以在选项中配置 render 选项。如果用户定义了,Vue 在挂载该组件的时候,会调用用户手写的 render 函数,如果没有 Vue 就根据模版内容生成一个 render 函数供挂载阶段调用。

根据 AST 生成 render 函数

<div id="NLRX"><p>Hello {{name}}</p></div>
ast = {
    'type': 1,
    'tag': 'div',
    'attrsList': [
        {
            'name':'id',
            'value':'NLRX',
        }
    ],
    'attrsMap': {
      'id': 'NLRX',
    },
    'static':false,
    'parent': undefined,
    'plain': false,
    'children': [{
      'type': 1,
      'tag': 'p',
      'plain': false,
      'static':false,
      'children': [
        {
            'type': 2,
            'expression': '"Hello "+_s(name)',
            'text': 'Hello {{name}}',
            'static':false,
        }
      ]
    }]
  }

如何根据一个已有的 AST 去生成对应的 render 函数。生成 render 函数的过程就是一个递归的过程。从顶向下依此递归 AST 中每一个节点,根据不同的 AST 节点类型创建不同的 VNode 类型。

  1. 根节点 div 是一个元素型 AST 节点。创建一个元素型 VNode。把创建元素型 VNode 的方法 _c(tagName, data, children).

_c(‘div’, {attrs: {‘id’: ‘NLRX’}}, [/子节点列表/])

  1. 进入 children 遍历 , 发现子节点 p 也是元素类型。
    _c(‘div’, {attrs: {‘id’: ‘NLRX’}}, [_c(‘p’), [/子节点列表/]])

  2. 然后是一个文本
    _c(‘div’, {attrs: {‘id’: ‘NLRX’}}, [_c(‘p’), [_v(‘hello’+_s(name))]])

  3. 然后做一次包装。

with(this) {
    return _c(
        'div',
        {
            attrs: {}
        },
        [
            _c('p'),
            [
                _v('hello'+_s(name))
            ]
        ]
    )
}
  1. 最后将得到的函数字符串交给 createFunction 函数。createFunction 可以将函数字符串得到真正的函数, 赋值给组件中的 render 选项。

挂载阶段调用 render


Vue.prototype.$mount = function (el){
  const options = this.$options
  // 如果用户没有手写render函数
  if (!options.render) {
    // 获取模板,先尝试获取内部模板,如果获取不到则获取外部模板
    let template = options.template
    if (template) {

    } else {
      template = getOuterHTML(el)
    }
    const { render, staticRenderFns } = compileToFunctions(template, {
      shouldDecodeNewlines,
      shouldDecodeNewlinesForHref,
      delimiters: options.delimiters,
      comments: options.comments
    }, this)
    options.render = render
    options.staticRenderFns = staticRenderFns
  }
}

从 Vue 实例属性选择中获取 render 选项,如果没有获取到,就去将 template 转为 render 函数。

先去获取内部模版,获取不到就去获取外部模版。

获取到之后。调用 compileToFunctions 函数转为 render 函数,再将 render 挂载到 options.render 上。

将 template 给到 compileToFunctions ,就可以获取到 render 函数。

compileToFunctions 函数是 createCompiler 函数的返回值。 创建一个编译器。

createCompiler 函数是调用 createCompilerCreator 函数返回的。 createCompilerCreator 函数接收一个 baseCompile 函数作为参数。

baseCompile 是模版编译三大阶段的主函数。

最后compileToFunctions返回了抽象语法树( ast ),渲染函数( render ),静态渲染函数( staticRenderFns ).

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值