前言
前边有分享过 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
// 合并options
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) // 初始化 provide
callHook(vm, 'created') // 执行创建后
// 通过 $mount 方法挂载实例
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
截至到这里,整个 Vue 实例的初始化阶段暂且告一段落,总结下来就是 :
new Vue(options) => 调用 _init() 方法合并用户实例opstions和本身的options => 初始化一系列生命周期,事件,render, provider和inject注入 => 最后调用 $mount 挂载实例
直到 $mount 挂载 , 我们回到今天的主题 模板编译 , 顾名思义,就是 Vue 是如何将 template 变真实的 dom 渲染到页面上的,那自然是在挂载的时候才会发生的事儿
$mount
首先来看一看 $mount 方法是如何实现的,源码上不止一个 $mount ,由于我们编写 Vue 可以写 render 方法,也可以写 template 模板, 所以 $mount 就分为了两种
- render 函数 => vNode 虚拟dom => 真实 dom
- template 模板 => Ast 抽象语法树 => render => vNode 虚拟dom => 真实 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
删除多余的代码
import Vue from './runtime/index';
import { compileToFunctions } from './compiler/index'
// 缓存上面的 $mount 原型方法,最后调用
const mount = Vue.prototype.$mount;
// 重写编译模板的原型方法 $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 函数,将创建一个 render 函数
// 并挂载到当前的 options 上,来自于 compileToFunctions 方法
if (!options.render) {
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 只是把 template 模板转换成 render ,再调用回引用的 $mount , 代码就是最后一句 return mount.call(this, el, hydrating);
模板编译
通过上边的分析,可以知道没有 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 将 template 模板字符串转换为 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 :文本 , 原生 nodeType
tag:'div', // 元素
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")
)
]
)
},
staticRenderFns:.....
}
编译后,执行 render 就是传说中的虚拟 dom 了 , 到虚拟 dom 之后就是纯对象处理了,解析 => diff => 渲染 .
有人可能会说虚拟 dom 到底是什么样的,其实它跟 ast 语法树很像,也是一个类似的对象,只是 ast 树描述的是带有语法的树结构,而虚拟 dom 描述的是真实的 dom 结构 。 那什么是带有语法的树, 如:{ text : {{msg}} } , ast 里边就会包含这种的情况,而虚拟 dom 不会 。
with
// generate 函数执行后,返回一个对象. 有一个陌生的 with 函数
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
看到这个函数,可能可能会有些陌生,我也是百度了之后才知道它的用处,简单的来说就是with相当于一种简写方式 在with函数包裹的代码区域,通过节点名称就能调用对象,举个例子
var obj={ name: "hisen", age:18 };
function f(obj){
console.log(`${obj.name}:${obj.age}`)
}
// 普通调用
f(obj);
VM679:2 hisen:18
// with 调用
with(obj){
console.log(`${name}:${age}`)
}
VM744:2 hisen:18
区别就在写法上更简洁明了,不用重复编写同名对象,目前的ES6解构其实也挺好
结束
最后引用参考文章 彩云coding 的一张精简图,来描述整个 Vue 的编译过程
欢迎点赞,小小鼓励,大大成长