前言
前边有分享过 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
就分为了两种
- render 函数 => vNode => 真实 dom 运行环境
- 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) => 截取操作 结束标签
- 开始标签通过
indexOf
判断是否包含开始标签<
的方式 - 结束标签 同上 =>
>
- 文本则是长度 >= 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() 方法
- … 还有很多啊,如: 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 的编译过程