原文链接:https://geniuspeng.github.io/2018/02/07/vue-compile/

之前整理了vue的响应式原理,在这里有一点是一直很模糊的,就是何时去new一个watcher,当data变化时又如何通知视图去进行怎样的更新…这里面涉及到了模板渲染和虚拟DOM的diff以及更新操作。其实模板渲染过程在实际使用vue的过程可能并不需要太深理解,但就vue来说,这些底层思想可以更好地让我们理解这个框架,以及了解为什么Vue的API要如此设计…

上一次也提过,vue2+与vue1+的模板渲染过程完全不同,vue1使用的是DocumentFragment API,具体就不介绍了(可以直接跳到MDN去了解),而vue2开始则使用了Virtual DOM,基于Virtual DOM,vue2支持了服务端渲染SSR,以及JSX语法。介绍渲染流程之前,先说明两个数据结构:抽象语法树AST,以及VNode。

AST数据结构

AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而vue就是将模板代码映射为AST数据结构,进行语法解析。这里采用了flow的语法,flow是一个JS静态类型检查工具。

首先看一下 Vue 2.0 源码中 AST 数据结构 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
declare type ASTNode = ASTElement | ASTText | ASTExpression
declare type ASTElement = { // 有关元素的一些定义
type: 1;
tag: string;
attrsList: Array<{ name: string; value: string }>;
attrsMap: { [key: string]: string | null };
parent: ASTElement | void;
children: Array<ASTNode>;
//......
}
declare type ASTExpression = {
type: 2;
expression: string;
text: string;
tokens: Array<string | Object>;
static?: boolean;
// 2.4 ssr optimization
ssrOptimizability?: number;
};
declare type ASTText = {
type: 3;
text: string;
static?: boolean;
isComment?: boolean;
// 2.4 ssr optimization
ssrOptimizability?: number;
};

VNODE数据结构

VNODE就是vue中的虚拟dom节点,VNODE 数据结构 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}

真实DOM存在什么问题,为什么要用虚拟DOM

我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document.createElement 这个方法创建的真实 DOM 元素会带来性能上的损失。我们来看一个 document.createElement 方法的例子

1
2
3
4
let div = document.createElement('div');
for(let k in div) {
console.log(k);
}

打开 console 运行一下上面的代码,会发现打印出来的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,关联着真实的dom,比如属性elm,只包括我们需要的属性,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。

模板渲染流程

先来一张图:

首先从$mount开始,可以看到,mount其实就是拿到了html模板作为template,然后将这个template通过compileToFunctions方法编译成render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
} const { render, staticRenderFns } = compileToFunctions(template, { //对获取到的template进行编译
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns /* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}

那么这个compileToFunctions做了什么呢?主要将 template 编译成 render 函数。首先读缓存,没有缓存就调用 compile 方法拿到 render 函数 的字符串形式,再通过 new Function 的方式生成 render 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 有缓存的话就直接在缓存里面拿
const key = options && options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
const res = {}
const compiled = compile(template, options) // compile 后面会详细讲
res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 函数并缓存
const l = compiled.staticRenderFns.length
res.staticRenderFns = new Array(l)
for (let i = 0; i < l; i++) {
res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
}
......
}
return (cache[key] = res) // 记录至缓存中

现在我们具体看一下compile方法,上文中提到 compile 方法就是将 template 编译成 render 函数 的字符串形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
export function compile (
template: string,
options: CompilerOptions
): CompiledResult {
const AST = parse(template.trim(), options) //1. parse
optimize(AST, options) //2.optimize
const code = generate(AST, options) //3.generate
return {
AST,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}

这个函数主要有三个步骤组成:parse,optimize 和 generate,分别输出一个包含 AST,staticRenderFns 的对象和 render函数 的字符串。

parse 函数,主要功能是将 template字符串解析成 AST,采用了 jQuery 作者 John Resig 的 HTML Parser。前面定义了ASTElement的数据结构,parse 函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST。

optimize 函数(src/compiler/optimizer.js)主要功能就是标记静态节点,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。

generate 函数(src/compiler/codegen/index.js)主要功能就是根据 AST 结构拼接生成 render 函数的字符串。

1
2
3
4
5
6
7
8
9
10
11
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}

其中 genElement 函数(src/compiler/codegen/index.js)是会根据 AST 的属性调用不同的方法生成字符串返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
const data = el.plain ? undefined : genData(el, state) const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}

以上就是 compile 函数中三个核心步骤的介绍,compile 之后我们得到了 render 函数 的字符串形式,后面通过 new Function 得到真正的渲染函数。数据发现变化后,会执行 Watcher 中的 _update 函数(src/core/instance/lifecycle.js),_update 函数会执行这个渲染函数,输出一个新的 VNode 树形结构的数据。然后在调用 patch 函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM 树上。

mount后续

通过compile生成render方法之后,会进一步执行mount方法,在$mount中可以看到最后一句话:return mount.call(this, el, hydrating),这个mount实际上就是runtime中的mount,执行的就是lifecycle中的mountComponent方法,看一下基本逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 触发 beforeMount 生命周期钩子
callHook(vm, 'beforeMount') let updateComponent //updateComponent是watcher更新时的回调,用于更新视图操作
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}` mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag) mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
} // 以前是直接new Watch赋值给vm._watcher,现在这一步放到了watcher的构造函数中
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
hydrating = false // manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm

首先会new一个watcher对象(主要是将模板与数据建立联系),在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) 。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。

patch

patch.js 就是新旧 VNode 对比的 diff 函数,主要是为了优化dom,通过算法使操作dom的行为降到最低,diff 算法来源于 snabbdom,是 VDOM 思想的核心。snabbdom 的算法为了 DOM 操作跨层级增删节点较少的这一目标进行优化,它只会在同层级进行, 不会跨层级比较。

文末福利:

福利一:前端,Java,产品经理,微信小程序,Python等10G资源合集大放送:https://www.jianshu.com/p/e8197d4d9880

福利二:微信小程序入门与实战全套详细视频教程。

【领取方法】

关注 【编程微刊】微信公众号:

回复【小程序demo】一键领取130个微信小程序源码demo资源。

回复【领取资源】一键领取前端,Java,产品经理,微信小程序,Python等资源合集10G资源大放送。

Vue源码--深入模板渲染的更多相关文章

  1. Vue源码翻译之渲染逻辑链

    本篇文章主要要记录说明的是,Vue在Vdom的创建上的相关细节.这也是描绘了Vue在界面的创建上的一个逻辑顺序,同时我也非常拜服作者编码的逻辑性,当然或许这么庞大复杂的编码不是一次性铸就的,我想应该也 ...

  2. vue源码分析—模板解析

    福建省啦剑飞傻了剑飞撒到了看风景啊撒:

  3. Vue源码探究-虚拟DOM的渲染

    Vue源码探究-虚拟DOM的渲染 在虚拟节点的实现一篇中,除了知道了 VNode 类的实现之外,还简要地整理了一下DOM渲染的路径.在这一篇中,主要来分析一下两条路径的具体实现代码. 按照创建 Vue ...

  4. [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST

    本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...

  5. [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

    本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...

  6. Vue 源码解读(10)—— 编译器 之 生成渲染函数

    前言 这篇文章是 Vue 编译器的最后一部分,前两部分分别是:Vue 源码解读(8)-- 编译器 之 解析.Vue 源码解读(9)-- 编译器 之 优化. 从 HTML 模版字符串开始,解析所有标签以 ...

  7. 大白话Vue源码系列(01):万事开头难

    阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的 ...

  8. 大白话Vue源码系列(03):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  9. 大白话Vue源码系列(04):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

随机推荐

  1. C++ 为什么要virtual析构函数

    class A { public: A() { printf("A()\n"); } virtual ~A() { printf("~A()\n"); } }; ...

  2. 利用日志使管理Linux更轻松

    利用日志使管理Linux更轻松 操作系统的日志主要具有审计与监测的功能,通过对日志信息的分析,可以检查错误发生的原因,监测追踪入侵者及受到攻击时留下的痕迹,甚至还能实时的进行系统状态的监控.有效利用日 ...

  3. python使用大漠插件进行脚本开发的尝试(一)

    关于游戏脚本是纯然的小白,记一下学习过程遇到的问题.是在win10系统下对PC端的游戏进行脚本编辑,不知道会不会半途放弃. 一.大漠插件 大漠插件在游戏脚本编辑过程中是比较常见的工具,按我理解大致做的 ...

  4. Boost 解析xml——插入Item

    XML格式为 <?xml version="1.0" encoding="utf-8"?> <Config> <Item name ...

  5. Eclipse使用方法和技巧二十七:定义自己的高速联想词

    某天在调试代码的时候.尽管是android的project还是习惯的输入syso.然后在ALT+/一下. 旁边的同事就问了一下,这个log打印输出的tag是什么. 接着又问了为什么syso可以智能联想 ...

  6. Objective-C - 类的静态常量

    创建头文件(.h), 导出常量: // Constants.h FOUNDATION_EXPORT NSString *const MyFirstConstant; FOUNDATION_EXPORT ...

  7. 本地 oracle 安装文件夹满触发 ORA-7445 [_memmove()+64] 导致Instance Crashed 的事故

    近期处理了一个问题,原因是因为命中ORA-600 [kole_t2u], [34] - description, bugs 导致 在udump 文件夹下大量转储 出cdmp 文件, 然后这些 cdmp ...

  8. PMP杂谈--名词解释

    过程:PMP中常常提到过程,好像非常高大上,实则不然,过程说白了就是一系列的行动和活动,用来创建预定的产品,服务或成果. 就这么简单. 再深入点就是,行动和活动当然都会有ITTO(输入,工具和技术.输 ...

  9. PHP用socket连接SMTP服务器发送邮件

    PHP用socket连接SMTP服务器发送邮件 PHP用socket连接SMTP服务器发送邮件学习实验记录: 分析与SMTP会话的一般流程 1. HELO XXX \r\n //XXX就是自己起个名字 ...

  10. telnet不是内部命令也不是外部命令

    转自:https://www.cnblogs.com/sishang/p/6600977.html 处理办法: 依次打开“开始”→“控制面板”→“打开或关闭Windows功能”,在打开的窗口处,寻找并 ...