原稿:语雀 · 【原理二】模板渲染 · 原目录:Vue › 【原理二】模板渲染
Vnode 概述
参考链接:
什么是 Vnode
export default class VNode {
// 主要属性
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
//...
}
Vnode 即 Vue 中的 Virtual DOM,这是通过 js 对象来描述真实的 DOM,将 DOM 的 tag、attrs、content 都当作对象的属性。它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。Vue 在渲染时,会将 template 描述成 Vnode,将 Vnode 形成 DOM 挂载到指定位置。
为什么要使用虚拟 DOM
- 减少 DOM 操作:浏览器对于真实 DOM 的设计非常复杂,因此直接构建并操作 DOM 的开销很大,如果构造任何页面变化,都只使用 Vnode 进行操作对比,只需要在最后一步挂载更新 DOM,就能提高性能节省开销
- 提高跨平台能力:不管是 Node 或浏览器都能解析 JS 对象,从而支持web端、服务端渲染
- 虚拟 DOM 不一定可以提高性能:
- 首次渲染的时候会增加开销
- 复杂视图情况下提升渲染性能
Vnode 生成到挂载
源码分析
这里涉及到模板渲染过程,一步步描述 2. 在 initMixin() 的最后进行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
- 在 mountComponent() 中会创建为当前实例创建一个 watcher(),并传入 updateComponent()
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
- watcher() 中会让传入 的updateComponent() 作为当前实例的 getter(),构造函数最后会直接调用 getter(),即updateComponent()
- updateComponent() 中会先调用 vm._render() 获取 Vnode,再调用 vm._update()
// vm._render() 生成虚拟DOM
// vm._update用来将虚拟DOM转换为真实DOM,再渲染到页面中
vm._update(vm._render(), hydrating);
- vm._render() 中获取并调用 render 函数(通常由 Vue 编译模板从优化后 AST 生成,可手动传入)
vnode = render.call(vm._renderProxy, vm.$createElement);
下方是一个解析模板后生成的 render 函数
function (){
with(this){
return _c('div',{attrs:{"href":"xxxx"}},["1111"]).
}
}
- 渲染函数会执行 _c,即 $createElement,而 $createElement 实际调用 createElement()
// 对编译生成的render进行渲染的方法
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// 对手写render函数进行渲染的方法
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
- createElement() 中只是处理的参数,最后调用 _createElement()
return _createElement(context, tag, data, children, normalizationType)
- _createElement() 是最终生成 Vnode 的地方,该函数中先做了以下处理:
- 将传入的 Children 规范成 Vnode 类型:Children 可能是嵌套组件或 <slot> 生成的多维数组,normalize 与 simpleNormalize 分别处理这两种情况,拍平数组
- 规范化 Children 后,创建Vnode实例
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
之后若 tag 是标签,则会直接生成 Vnode,否则会调用 createComponent ,后者也会调用 Vnode 构造函数
function _createElement(
context, tag, data,
children, normalizationType
) {
var vnode;
if (tag是正常html标签) {
vnode = new VNode(
tag, data, children, undefined,
undefined, context
);
}
else if (tag 是组件) {
vnode = createComponent(
Ctor, data, context,
children, tag
);
}
return vnode
}
- 调用 Vnode 构造函数,一个 Vue 组件可能有多个 Vnode 构成树形结构
function VNode(
tag, data, children,
text, elm, context,
componentOptions
) {
this.tag = tag; // 标签名
this.data = data;
this.children = children; // 子元素
this.text = text; // 文本内容
this.elm = elm; // Dom 节点
this.context = context;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.isStatic = false; // 是否静态节点
this.isComment = false; // 是否是注释节点
this.isCloned = false; // 是否克隆节点
};
- 当vm._render() 调用完毕,Vnode tree 渲染完毕,开始执行 update(),_vnode 存放的是旧节点,接收的 vnode 是 render() 最新生成的 Vnode
Vue.prototype._update = function (vnode, hydrating) {
var prevVnode = vm._vnode;
vm._vnode = vnode;
if (!prevVnode) {
// 没有旧节点,直接使用vnode创建DOM并挂载
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// 有旧节点,则比较新旧节点,打补丁后再挂载
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
示例
假设有一个 template
<div class="parent" style="height:0" href="2222">
111111
</div>
经过上述流程,最终生成的 Vnode
{
tag: 'div',
data: {
attrs:{href:"2222"}
staticClass: "parent",
staticStyle: {
height: "0"
}
},
children: [{
tag: undefined,
text: "111111"
}]
}
之后可以根据这个对象构造真实 DOM,因为这个过程也可以发生在服务端,因此有了服务端渲染
Patch & Diff 算法
上面的 vm.__patch__() 会调用 patch(),之后又会调用 patchVnode()
patchVnode()
该方法主要负责对比新旧 Vnode,找到差异,并更新真实 DOM。当新旧 Vnode 有不同子节点时,会执行 updateChildren()
function patchVnode (oldVnode, vnode, hydrating, removeOnly) {
// 新旧节点是同一个对象,直接返回
if (oldVnode === vnode) {
return
}
// 新节点无text
if (isUndef(vnode.text)) {
// 新旧节点都有children
if (isDef(oldCh) && isDef(ch)) {
// 若新旧节点children不同,执行updateChildren()
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
}
// 只有新节点有children
else if (isDef(ch)) {
{ // 子节点是否有重复key
checkDuplicateKeys(ch);
}
// 清空旧节点text
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
// 执行addVnodes
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
}
// 只有旧节点有children
else if (isDef(oldCh)) {
// 执行removeVnodes
removeVnodes(oldCh, 0, oldCh.length - 1);
}
// 新旧节点都无children,旧节点有text
else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
}
// 新旧节点text不一致
else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
}
patch 算法小结
updateChildren()
对比同一层的新旧子节点,找到可复用的节点,一共涉及8个变量:
- 旧节点开始索引 oldStartIdx;新节点开始索引 newStartIdx;