浅谈Vue2.0模板渲染
Vue 2.0 中模板渲染与 Vue 1.0 完全不同,1.0 中采用的 DocumentFragment,而 2.0 中借鉴 React 的 Virtual DOM。基于 Virtual DOM,2.0 还可以支持服务端渲染(SSR),也支持 JSX 语法。
相关概念介绍
AST (abstract syntax tree)
AST即抽象语法树,是源代码的抽象语法结构的树状表现形式。Vue在mount的过程中,template会被编译成AST语法树。
Vue中的AST数据结构定义如下:
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: ArrayASTNode>;
//......
}
declare type ASTExpression = {
type: 2;
expression: string;
text: string;
static?: boolean;
}
declare type ASTText = {
type: 3;
text: string;
static?: boolean;
}
可以看到Vue中的AST数据结构有三种类型,以type区分:
- ASTElement 标签
- ASTExpression 包含字面量表达式的文本节点
- ASTText 普通文本节点或注释节点
Virtual DOM - 轻量级的模拟DOM结构
- 结构为有序的二叉树
- 与真实DOM树每个位置的属性一一对应
- 某一时刻真实DOM状态的内存映射
VNode - Vue中的VDOM对象
数据结构定义:
constructor {
this.tag = tag //元素标签
this.data = data //属性
this.children = children //子元素列表
this.text = text
this.elm = elm //对应的真实 DOM 元素
this.ns = undefined
this.context = context
this.functionalContext = 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
}
为什么使用VDOM?
-
创建真实DOM的代价高
真实的DOM节点实现的属性很多,仅一个<div>
就有多达200种属性,而VNode仅仅实现一些必要的属性,相比起来,创建一个VNode的成本比较低 - 真实DOM频繁排版与重绘的效率是相当低的
- 虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分
模板渲染流程解析
模板渲染流程模板渲染大致上分为图中的三个阶段,其中
$mount()
函数是下面所有函数的入口函数,所做的工作主要分为三步:
- 如果你的option里面没有自定义
render()
函数,那么,通过compileToFunctions()
将HTML模板编译成可以生成VNode的render函数 - new 一个
Watcher()
实例,触发updateComponent()
方法 - 生成VNode,经过
patch()
,把VNode更新到DOM上
模板编译
模板编译阶段涉及两个函数,compileToFunctions()
和compile()
,其中compileToFunctions()
是compile()
的入口函数,模板编译的结果是生成render函数和staticRenderFns函数的字符串,其中staticRenderFns函数包含被标记为静态节点的 VNode,与后续的diff算法优化相关
在进入
compileToFunctions()
以后,会先检查缓存中是否有已经编译好的结果,如果有结果则直接从缓存中读取,这样做防止每次同样的模板都要进行重复的编译工作。若无可用缓存,则调用compile()
,最后对编译结果进行缓存
compile()
做了三件事情,分别是生成AST、优化静态内容、生成render函数,对应三个函数:
-
parse ()
主要功能是将 template字符串解析成 AST,采用了 jQuery 作者 John Resig 的 HTML Parser。前面定义了ASTElement的数据结构,parse 函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST -
optimize()
主要功能就是标记静态节点,获得AST对象的最大静态子树,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较
最大静态子树:
不包含参数data属性的dom节点,每次data数据改变导致页面重新渲染的时候,最大静态子树不需要重新计算生成
-
generate()
根据 AST 结构拼接生成 render 函数的字符串,另外还会生成staticRenderFns函数字符串
DOM更新
Vue中的DOM更新是一个响应式工作流程,当数据发现变化后,会执行 Watcher 中的update()
函数,update()
函数会执行这个渲染函数,输出一个新的 VNode 树形结构的数据。然后再调用patch()
函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM 树上
patch.js 就是新旧 VNode 对比的 diff 函数,主要是为了优化dom,通过算法使操作dom的行为降到最低,diff 算法来源于 snabbdom,是 VDOM 思想的核心。snabbdom 的算法为了 DOM 操作跨层级增删节点较少的这一目标进行优化,它只会在同层级进行, 不会跨层级比较。
总结
- VDOM
VDOM总损耗 = 虚拟DOM增删改 + diff增删改 + (较少的节点)排版与重绘
真实DOM总损耗 = 真实DOM完全增删改 + (可能较多的节点)排版与重绘
目的是为了避免频繁引发大面积的DOM操作,提升性能
-
模板编译
将 template 转换为 AST,优化 AST,再将 AST 转换为 render函数
目的是为了得到render函数 -
DOM更新
render函数通过Watcher与数据产生关联,在数据发生变化时调用patch函数,
执行此render函数,生成新VNode,与旧VNode进行diff,最终更新DOM树
参考链接
- Vue技术内幕
http://hcysun.me/vue-design/ - Vue渲染原理
https://www.cnblogs.com/zhaodagang8/p/7819414.html - Vue源码--解读Vue响应式原理
https://geniuspeng.github.io/2018/01/05/vue-reactivity/