【Vue2.x 源码学习】第十二篇 - 生成 ast 语法树-流程说明

382 阅读3分钟

这是我参与更文挑战的第12天,活动详情查看: 更文挑战

一,前言

上篇,主要介绍了 vue 数据渲染核心流程,涉及以下几个点:

初次渲染时

  • template 模板被编译为 ast 语法树;
  • 通过 ast 语法树生成 render 函数;
  • 通过 render 函数返回 vnode 虚拟节点;
  • 使用 vnode 虚拟节点生成真实 dom 并进行渲染;

视图更新时

  • 调用 render 函数生成新的 vnode 虚拟节点;
  • 通过 diff 算法对新老 vnode 虚拟节点进行对比;
  • 根据虚拟节点比对结果,更新真实 dom;

本篇,生成 ast 语法树-流程说明


二,Vue 提供的使用方式

1,三种模板写法及优先级

<body>
  <!-- 第一种 -->
  <div id=app>{{message}}</div>
  <script src="./vue.js"></script>
  <script>
    debugger;
    let vm = new Vue({
      el: '#app',
      data() {
      },
      // 第二种
      template:'',
      // 第三种
      render(){}
    });
  </script>
</body>

三种写法的优先级【由高到低】:

  1. 使用render函数;
  2. 使用template模板;
  3. 使用页面元素中的内容;即使用 <div id=app>{{message}}</div> 中的 {{message}}

2,两种数据挂载方式

Vue2 中,提供了两种挂载方式:

let vm = new Vue({
  // el: '#app',	// 挂载方式 1
  data() {
  },
}).$mount('#app');	// 挂载方式 2

1,当new Vue实例化时,通过options选项指定el挂载点,

2,或者,通过Vue实例,调用Vue的原型方法$mount

最终,都会通过Vue上的原型方法$mount对数据进行挂载操作;


三,数据挂载的实现原理

1,Vue 的原型方法 $mount

通过以上分析,当vm.$options.el存在或直接调用Vue原型方法$mount时,就会对数据进行挂载操作;

<body>
  <div id=app>{{message}}</div>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {...},
    }); 
  </script>
</body>

所以,设置vm.$options.el或调用vm.$mount,内部都是调用Vue原型方法$mount进行处理的;

$mount中,需要拿到el挂载点所指向的真实dom元素,并使用新内容将它替换掉:

// src/init.js#initMixin

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = options;
    
    initState(vm);

    if (vm.$options.el) {
      // 将数据挂载到页面上(此时数据已被观测)
      vm.$mount(vm.$options.el)
    }
  }

  // 支持 new Vue({el}) 和 new Vue().$mount 两种情况
  Vue.prototype.$mount = function (el) {
    const vm = this;
    el = document.querySelector(el); // 获取真实的元素
    vm.$el = el; // vm.$el:当前页面上的真实元素
  }
}

那么,如何拿到el挂载点指向的真实dom元素?

2,如何拿到"id = app"对应的元素

dom操作中,获取内容有两个方法:outerHTMLinnerHTML

分别测试两个方法:

  • outerHTML执行结果:<div id=app>{{message}}</div>
  • innerHTML{{message}}

这里,由于希望能够使用新内容替换掉老内容,所以,需要使用outerHTML来拿到全部内容;

3,三种模板的优先级实现

todo:可以将上边的三种写法优先级与此处内容进行合并

  • render函数优先级最高,如果有render函数,就不会再编译template模板了;
  • 没有render函数,会去找template模板,使用template模板,编译成为render函数;
  • 既没有render函数,也没有template模板,使用el标签作为模板,编译成为render函数;(第十一篇中的 Vue2 模板编译结果,就是此场景)

在后续内容中,三种模板分别称呼为:render函数、template模板、html模板;

结合Vue中不同模板的优先级特性,大致逻辑如下:

// src/init.js

Vue.prototype.$mount = function (el) {
  const vm = this;
  const opts = vm.$options;
  
  el = document.querySelector(el);
  vm.$el = el;

  // 如果没有 render, 找 template
  if (!opts.render) {
    // 如果没有 template, 采用元素中的内容
    const template = opts.template;
    if (!template) {
      // 拿到整个元素标签
      console.log(el.outerHTML);
      template = el.outerHTML
    }else{
      console.log("有template = " + template)
    }
    // 将模板编译成为 render 函数(模板来源可能是template模板也可能是 html 标签)
    // ****这里便是后续要实现逻辑****:通过函数Fn,将模板编译为render函数
    let render = Fn(template);
  }
}

运行查看结果:

image.png

当前情况下,在options选项中,既没有render函数也没有template模板,所以通过el.outerHTML拿到app元素的整个标签;

// todo 三种模板的优先级可以做一些演示,比如同时存在 template 模板和 render 函数,或同时存在 template 模板和 html 模板的执行情况;


四,将模板编译为 ast 语法树

1,compileToFunction 入口

在前面分析vue数据渲染流程中提到:template模板->ast语法树->render函数,模板编译的最终结果就是render函数;

compileToFunction方法中,生成render函数,需要以下两步核心操作:

  1. 通过parserHTML方法:将模板(templatehtml)内容编译为ast语法树;
  2. 通过generate方法:根据ast语法树生成为render函数;
//  src/compiler/index.js

export function compileToFunction(template) {
  // 1,将模板变成 AST 语法树
  let ast = parserHTML(template);
  // 2,使用 AST 生成 render 函数
  let code = generate(ast);
}

function parserHTML(template) {
  console.log("parserHTML-template : " + template)
}

function generate(ast) {
  console.log("parserHTML-ast : " + ast)
}

Vue2中,compileToFunction方法是整个Vue编译的入口;

完成以上两步操作之后,template模板最终将会被编译成为render函数;

2,parserHTML 方法

parserHTML方法:将模板(templatehtml)编译成为ast语法树;

注意:parserHTML方法的入参template,指的是<template>标签内部的内容并不包含<template>标签本身;

compileToFunction(template) 方法,对html模板进行处理,需要传入html模板:

在 Vue 初始化时:(todo 这里的描述可以和模板优先级一起合并说明)

  • 如果options选项中设置了template,将优先使用template内容作为模板
  • 如果options选项没有设置template,将采用元素内容作为html模板:<div id="app"></div>

代码实现:

Vue.prototype.$mount = function (el) {
  const vm = this;
  const opts = vm.$options;
  el = document.querySelector(el); // 获取真实的元素
  vm.$el = el; // vm.$el 表示当前页面上的真实元素

  if (!opts.render) {
    let template = opts.template;
    console.log("template = " + template)
    if (!template) {
      console.log("没有template, el.outerHTML = " + el.outerHTML)
      template = el.outerHTML;
    }else{
      console.log("有template = " + template)
    }
    // 将模板编译成为 render 函数
    let render = compileToFunction(el.outerHTML);
    // 将编译后的 render 函数保存到当前 vm 实例上
    opts.render = render
  }else{
    console.log("render 函数已缓存,跳过编译直接使用!~");
  }
}

运行测试:

image.png

这样就实现了第一步,就拿到了html模板;

之后,还需要将html模板编译为ast语法树,用js对象的树形结构来描述HTML语法;而将html模板编译为ast语法树,就需要对 html 模板进行解析,解析方式就是使用正则不断匹配和处理;

3,render 函数的重要性

使用模板转化为一个render函数之后,当数据发生变化时,就可以通过render函数进行新老对比,并根据最终的对比结果,再对真实dom进行更新;


五,结尾

本篇,主要介绍了生成ast语法树 - 流程说明,涉及以下几个点:

  • Vue 核心渲染流程回顾
  • 三种模板写法及优先级
  • 两种数据挂载方式
  • Vue 的原型方法 $mount
  • compileToFunction -> parserHTM流程说明

todo 备注:这一篇介绍了从 template 模板到生成 render 函数的整个流程,并实现了简单的代码框架,获取到了html中的模板;

下一篇,生成 ast 语法树-正则说明


维护日志

  • 20230115:重新梳理文章内容,调整目录结构,补充少量文字说明,添加描述内容中的代码显示高亮;
  • 20230118:添加部分代码、调整少量内容描述和注释说明,标记可重构点 todo * 4;