Vue模板编译

前言

前边有分享过 React 的工作原理,那么 Vue 又是怎样的编译原理呢,经过几天的翻阅,视频,现在来分享给大家,共勉 。

参考

在分享 Vue 响应式原理的时候,我们有看到一张图,很详细的体现了Vue实例的整个生命周期,那么今天我们就从这张图来作为入口

从图上来看,结合我们之前分享的响应式原理,可以知道,new Vue(options) 主要执行了 this._init(options) 初始化方法,而这个方法就定义在 init.js 中 ,下方简约代码详解

// 入口文件 src/core/instance/index.js
// 删掉多余的代码
import { initMixin } from './init' 
function Vue (options) {
  // _init 是Vue的原型方法,定义在 initMixin 中,看上边导入路径
  this._init(options) 
}
initMixin(Vue) // 初始化各个_init方法, 包含初始化 options, render, events,beforCreated,created 等
export default Vue // 导出 Vue 构造函数

好,来看看 initMixin 模块到底做了些什么事儿 ? code position : src/core/instance/init.js

export function initMixin (Vue) {
 // 原型上的 _init , 也是那句 this._init(options) 
  Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    vm._self = vm 
    initLifecycle(vm) // 初始化生命周期 
    initEvents(vm) // 初始化事件
    initRender(vm) // 初始化 render
    callHook(vm, 'beforeCreate') // 执行创建前生命周期
    initInjections(vm) // 注入 data/props
    initState(vm) // 初始化state
    initProvide(vm) // 解析 data/props
    callHook(vm, 'created') // 执行创建后
    
    // 通过 $mount 方法挂载实例 
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

截至到这里,整个 Vue 实例的初始化阶段暂且告一段落,总结下来就是 :

new Vue(options) => 调用 _init() 方法 => 初始化生命周期 ,事件 , render => 回调创建前生命周期 => 然后注入 data/props => 初始化 state => 解析注入data/props => 回调创建后生命周期 => 调用 $mount 挂载

直到 $mount 挂载 , 我们回到今天的主题 模板编译 , 顾名思义,就是 Vue 是如何将 template 变真实的 dom 渲染到页面上的,那自然是在挂载的时候才会发生的事儿

$mount

首先来看一看 $mount 方法是如何实现的吧,源码上不止一个 $mount ,是基于浏览器和运行环境做的区分,由于我们编写 Vue 可以写render 方法,也可以写 template 模板, 所以 $mount 就分为了两种

  1. render 函数 => vNode => 真实 dom 运行环境
  2. template 模板 => Ast 抽象语法树 => render => vNode => 真实 dom 编译环境

而我们今天分享的主题是 模板编译 , 那自然就是第2种了,走,去看源码

不需要模板编译,原型上定义的 $mount
src/platforms/web/runtime/index.js

import { mountComponent } from 'core/instance/lifecycle';

Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  
  return mountComponent(this, el, hydrating);
};

需要模板编译,引用 原型上定义的 $mount

src/platforms/web/entry-runtime-with-compiler.js

删除多余的代码, 直接看 $mount

import Vue from './runtime/index';
import { compileToFunctions } from './compiler/index' 

// 缓存上面的 $mount 方法
const mount = Vue.prototype.$mount; 

Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el);

  // 不能挂载到 body 和 html 上 
  if (el === document.body || el === document.documentElement) {     
    return this;
  }

  const options = this.$options;
  // 如果没有 render 函数
  if (!options.render) { 
  	  // ... 将 render 函数添加到 options 上
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange : process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters : options.delimiters,
        comments : options.comments,
      }, this);

      options.render = render;
      options.staticRenderFns = staticRenderFns;
    // ......
  }
  // 调用引用的 mount 方法
  return mount.call(this, el, hydrating);
};

看了两段 $mount 的方法实体,其实最终都需要走到第一个原型上定义的 $mount ,因为第二个只是做了模板编译,转换成 render ,再调用回引用的 $mount , return mount.call(this, el, hydrating); => 第一个$mount . 好,我们来看看传说中的模板编译

模板编译

通过上边的代码,可以看出来,没有 render 则需要调用 compileToFunctions 方法,将 template 模板字符串转换为 render 方法 。

compileToFuncitons 来自 import { compileToFunctions } from './compiler/index' 如下代码

/* @flow */
import { parse } from './parser/index' 
import { optimize } from './optimizer' 
import { generate } from './codegen/index' 
// 导入 compileToFunctions 方法
import { createCompilerCreator } from './create-compiler'

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
 // 通过 parse 生成 ast
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
   // 优化 ast
    optimize(ast, options)
  }
  // 生成 render 函数字符串,然后通过 new Function(code) 转换为函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
编译核心

好,看到这里,我们知道调用 $mount 挂载后,会调用 compileToFunctions 将html模板字符串转换为 render 函数

转换为 render 函数过程 :(parse) 生成 ast => (optimize) 优化ast => (generate) 生成 render 函数

parse

在分享 AST 之前,我们首先要知道,啥叫 ast , 它能做什么 ?

一句话简单概括一下,AST 的全称是 Abstract Syntax Tree,也就是抽象语法树,用来表示代码的语法数据结构

来一个参考实例 - 大概的结构 - 参数不一定全

let str = '<div id="content">我是文本</div>'// 转化为 ast 
let ast = {
    type:1// 1 : 标签 ,2 : 表达式 , 3 :文本
    tag:'div',
    attrsMap:{id:'content'},
    parent: undefined,
    attrs: [{name: "id", value: "'content'"}],
    children:[
    	{
        	type:3,
            text:'我是文本'
        }
    ]
}

源码在这一块儿就相当的复杂了, 各种正则匹配 ,就不贴出来供大家欣赏了,有兴趣的可以去源码看看,src/compiler 下对应的 parser

我听过公开课,也阅读过几篇模板编译的文章,在这里可以简单的总结一下,parser 就是将 html 字符串,通过截取拼装的方式,来组成 ast 语法树对象,具体的截取逻辑就是:
如:

let html = "<div>123</div>"
if(html.indexOf("<") == 0) => 截取操作 开始标签
if(html.indexOf("html") >= 0) => 截取操作 文本
if(html.indexOf(">") == 0) => 截取操作 结束标签
  1. 开始标签通过 indexOf 判断是否包含开始标签 < 的方式
  2. 结束标签 同上 => >
  3. 文本则是长度 >= 0

依次截取下去,当遇到开始标签的时候就去创建一个 ast 对象,当遇到结束标签,则把当前对象记录在全局上,如此循环,就会形成一个多层的 AST 对象,如上边的参考实例 。

optimize

优化 ast , 那具体它是怎么优化的呢 , 其实只是给每一个 ast 对象添加了一个标识静态属性 isStatic :true/false , 为什么添加这个属性呢 ? 这里就牵扯到 diff , 后边的虚拟 dom , 我将会专门写一篇关于 diff 的文章,到时再具体分析 .

这里呢,简单说一下,其实就是 diff 的时候,比对到静态标签,一律跳过,因为 isStatic:true 的时候,意味着这个标签或者元素是静态的,不需要编译和解析的,不含变量的,不是slot/component 等 ,所以直接跳过,也算是一种优化吧 .

generate

这一步就比较重要了,有点像 React render 经过 bebal-loader 编译后的感觉,Vue 就是将 AST 树遍历,生成 render 函数 , 然后执行 render 就会生成虚拟 dom , 最后就是创建与diff渲染真实dom了 。 我们来看看 generate 函数

src/compiler/codegen/index.js 43行

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 通过 genElement() 转换成 _c() 的形式
  const code = ast ? genElement(ast, state) : '_c("div")'
  // 结果是返回一个使用 with 包裹的函数
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

这里我就没有继续去查看每一个方法是怎么实现的了,只了解了一下,例举几个遍历 ast 树的编译方法 :

1.genElement:用来生成基本的 createElement 结构 ,_c() 的形式

2.genData: 处理ast结构上的一些属性,用来生成data

3.genChildren:处理ast的children,并在内部调用genElement,形成子元素的 _c() 方法

  1. … 还有很多啊,如: genIf , genFor , genStatic , genOnce 等等对应的处理

可能有的童鞋对 _c 不太了解,说实话,我看到这里的时候也一脸蒙蔽,后来才发现,这些只是 vue 定义的一些特殊方法的简写,如:

_c:对应的是 createElement 方法,创建一个元素(Vnode)
_v:创建一个文本结点。
_s:把一个值转换为字符串(eg: {{data}})
_m:渲染静态内容

那好,经过 generate 的转换之后,就形成了这种形式

// 编译前
<template>
  <div id="content">
    {{msg}}
    <p>123</p>
  </div>
</template>

// 编译后
{
  render: with(this) {
    return _c('div', {
      attrs: {
        "id": "content"
      }
    }, [
    	_v("\n" + _s(msg) + "\n"),
        _c("p",
           _m("123")
        )
       ]
    )
  }
}

编译后,执行 render 就是传说中的虚拟 dom 了 , 到虚拟 dom 之后就是纯对象处理了,解析 => diff => 渲染 .

有人可能会说虚拟 dom 到底是什么样的,其实它跟 ast 语法树很像,也是一个类似的对象,只是 ast 树描述的是带有语法的树结构,而虚拟 dom 描述的是真实的 dom 结构 。 那什么是带有语法的树, 如:{ text : {{msg}} } , ast 里边就会包含这种的情况,而虚拟 dom 不会 。

结束

最后引用参考文章 彩云coding 的一张精简图,来描述整个 Vue 的编译过程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值