VNode 是什么? 有什么用?
VNode 表示 虚拟节点 Virtual DOM,为什么叫虚拟节点呢,因为不是真的 DOM 节点,是一个 DOM 副本。
是什么?
一个纯 JavaScript 对象,本质上是轻量的 JavaScript 数据格式来表示真实的 DOM 在特定时间的外观。
{
tag: 'div',
data: {attrs: {}, ...}
children: []
}
有什么用?
减少DOM 操作。用 JavaScript 直接操作 DOM 的计算成本很高,比在 JavaScript 环境执行 JavaScript 代码更消耗资源
假设有 1000个元素列表,创建 1000个JavaScript 对象是非常节省的,也非常快。但是创建 1000 个实际的 DOM 节点要昂贵得多。
在需要更新列表项时,将更改应用至 JavaScript 副本、虚拟 DOM 中,然后在它们和实际 DOM 之间执行 diff。
最后,才对已更改的内容,进行批量处理调用,并一次性更改 DOM,对 UI 进行高效的更新!
跨平台。
虚拟 DOM 把渲染逻辑,从真实 DOM 中分离出来,所以需要更新时先 计算差异,然后再将这些更改,再应用到 DOM 上。
如果去掉 更新到 DOM 这一步,那么所有的更新逻辑实际上都是在 更改 JavaScript 对象而已,因为它不需要接触 DOM 了。
只要在操作 DOM 做这一层抽象,就可以应用到任何能运行 JavaScript 环境的应用,可以是原生渲染 (例如,IOS或Android),让 虚拟DOM 进行原生渲染 如:WEEX。
VNode 怎么生成的?
在 Vue 源码中,是通过一个类(class)去表示 VNode 的,最终也是 通过实例化 new 创建/生成 一个 VNode 节点。
class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*当前节点的编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*是否为静态节点*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
比如 使用 VNode 去描述这样一个 template
Template
VNode
VNode 什么时候生成的?
在初始化完选项,解析完模板之后,就需要挂载 DOM了。此时就需要生成 VNode,才能根据 VNode 生成 DOM 然后挂载,也就是在 beforeMount
之后,在 mounted
之前生成,当 mounted
被触发时已经完成 DOM 挂载。
挂载 DOM 第一步,就是先执行渲染函数,得到整个模板的 VNode
比如 有以下渲染函数,执行会返回 VNode,就是 _c 返回的VNode
function (){
with(this){
return _c('div',{attrs:{"href":"https://baidu.com"}},["1111"]).
}
}
渲染函数执行时 会通过
**with**
绑定执行时上下文_c
其实就是vm._c
```javascript vm._c = function(a, b, c, d) {return createElement(vm, a, b, c, d, false);
};
```javascript
function createElement(context, tag, data, children, normalizationType) {
let vnode, ns
if (typeof tag === 'string') {
/* 判断是否是保留的标签 */
if (config.isReservedTag(tag)) {
// 是保留标签 则创建相应的节点的 VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else {
/*
* 从 vm 实例的 option 的 components 中寻找该 tag
* 存在则就是一个组件,创建相应节点,Ctor 为组件的构造类
*/
vnode = createComponent(Ctor, data, context, children, tag)
}
} else {
// tag 不是字符串的时候 则是组件
vnode = createComponent(tag, data, context, children)
}
}
从上面看到 普通 HTML 标签 跟 组件标签 都不同流程
1. 普通 HTML 标签
Template
<div href="xxxx"></div>
render Function
function (){
with(this){
return _c('div',{
attrs:{"href":"xxxx"}},
["1111"]
)
}
}
new VNode
new VNode(tag, data, children, undefined, undefined, context);
2. 组件标签
Template
<div>
<test :name="name">hello</test>
</div>
render Function
with(this){
return _c('div',[
_c('test',
{attrs:{"name":name}},
["hello"]
)
],1)
}
create Component
createComponent(Ctor, data, context, children, tag);}
function createComponent (Ctor, data, context, children, tag) {
/** 父组件给子组件绑定的props */
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
/** 父组件给子组件绑定的 events 事件 */
const listeners = data.on
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
/** 组件数据 */
data,
undefined, undefined, undefined,
/** 组件实例上下文 */
context,
/** 组件数据对象 */
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
VNode 存放在哪里?
1. _vnode
vm._vnode
存放表示当前节点的 VNode
什么叫当前,也就是可以通过这个 VNode 直接映射成 当前真实DOM ,也可以说是旧节点,当进行 Diff 比较时所用的 VNode。
赋值
// src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 保存 VNode
vm._vnode = vnode
if (!prevVnode) {
// initial render
// 初始时 渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
// 更新时 渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
2. $vnode
vm.$vnode
只有组件实例的才有,存放的是外壳节点,页面实例中内容是不存放在 $vnode 的,例如:
/*
<test :name="name"></test>
*/
vm.$vnode = {
data: { class: [name], ...},
tag: "vue-component-1-test"
}
赋值
// src\core\instance\lifecycle.js
function updateChildComponent(
vm, parentVnode
) {
vm.$options._parentVnode = parentVnode;
vm.$vnode = parentVnode;
if (vm._vnode) {
vm._vnode.parent = parentVnode;
}
}