vue中的虚拟dom与vnode

300 阅读5分钟

一种技术的出现一定是为了解决某一个问题。那么已经有DOM为什么又会出现虚拟DOM呢?

最早时期,我们看到的页面是完全静态的。这就意味着这就像一本已经出版的书,传达的内容就是当前所见的,无法更改。再后来随着js脚本的不断进化发展与ajax技术的出现,我们看见的页面不再是纯静态的,我们可以点击按钮,可以发送请求,然后随着这些操作来对页面的内容进行改变-也就是操作dom。随着互联网的不断发展与人们使用网络浏览内容的需求渐盛,页面上呈现出来的内容越发的丰富。我们所进行的某一个操作,可能会带来大量的dom变更。于是我们,为每一个dom取上不同的名字或是id或是class,我们要在这一次操作之后分别获取它们,然后把他们的内容变成需要变更的内容。如果一个变量在很多处都用到了,我们可能需要把这几处地方都找出来,然后一个一个的改正过来。在这种情况下,漏改,错改的情况会时常发生,状态维护变成了一件繁琐而艰难的事。于是为了解决控制DOM操作的这一个问题,我们衍生出来两种方案。一种是当状态变更的时候,重新生成一份dom替换掉原来的,这种方法简单粗暴出错少,缺点就是成本太高(操作DOM的成本远大于js脚本运行的成本)。另一种方案就是我们将变更的dom替换成原来的dom,这样大大减少了dom操作的成本,改了哪个变哪个,很明显这种方案是比较可取的,并且现在前端的各大框架都有自己的一套处理办法。

react与vue2.以后的版本中使用虚拟dom,angular中使用脏值检查

综上所述,虚拟dom只是为了解决操作dom的其中一种解决方案,并不是必须使用它

vue中所使用的虚拟dom

我们平时会写一些vue的文件,并通过组件之间的引入,把有联系的组件import进来,类似如此:

<template>
  <div>
    <div class="title">标题</div>
    <!-- 子组件 -->
    <sub-title></sub-title>
  </div>
</template>

<script>
import SubTitle from './SubTile.vue'
export default {
   name:'TestComp',
   components:{
     SubTitle
   }
}
</script>

<style lang="scss">

</style>

vue在处理组件的时候,会将·<template>...</template>这一部分进行编译,编译成代码字符串放入render函数执行,从而生成当前组件的vnode虚拟节点。当vue把所有组件处理完成后就形成了一个虚拟节点树(每一个虚拟节点都是一个组件,也就是形成了一颗用vnode表达的组件树)首次渲染的时候根据虚拟节点来生成真实的dom,并把当前的vnode缓存起来,再次渲染的时候使用这一次生成的vnode与上一次生成的vnode进行对比,哪一个节点发生了变化就改变哪一个真实的dom节点。

  • vnode虚拟节点:一个VNode实例(js对象),描述了当前节点的信息,如节点类型、节点属性及名称等。
  • 虚拟节点树:由N个虚拟节点组成的一颗节点间的关系树。

由此可见,vue中的虚拟dom实际上只需要做好两件事:

  • 提供与真实节点所对应的虚拟节点vnode
  • 将新的vnode与旧的vnode进行对比,然后更新视图

vue的vnode

vnode的本质其实就是一个普通的js对象,它是VNode类的一个实例const vnode = new VNode(...),里面以属性值的方式存储了当前节点的描述信息,Vnode的模样如下:

export default class VNode {
  tag: string | void;  // 标签类型
  data: VNodeData | void;  // 存储当前节点的一些属性style、id ....
  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

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  functionalContext: Component | void; // real context vm for functional nodes
  functionalOptions: ?ComponentOptions; // for SSR caching
  functionalScopeId: ?string; // functioanl scope id support

  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.functionalContext = undefined
    this.functionalOptions = undefined
    this.functionalScopeId = 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
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

我们通过执行渲染函数而得到一份vnode,处理完所有的模板从而得到一颗vnode树,这棵树包含了所有我们对当前模板的描述信息。由于节点有不同的类型,那么生成的vnode也有很多类型:注释节点、文本节点、元素节点、组件节点、函数式节点、克隆节点。比如当我们遇到返回元素节点的渲染函数:

render(){
  return h('div',{...},'我是div的内容')
}

执行完了之后我们可能得到这样的一个vnode对象

{
    tag:'div',
    data:{...},
    children:[vnode], // 因为包含一个文本节点
    // ...
}

同理,注释节点、文本节点、元素节点、组件节点、函数式节点、克隆节点等都会生成自己的描述对象,有一些节点有特定的属性,比如注释节点的isComment为true,静态节点(无响应式数据的节点)的isStatic为true。

总结: 虚拟dom是一种处理dom操作的解决方案,vue2.x后采用了这种方案。在vue中虚拟dom并不只是vnode,vnode的概念是虚拟节点。vue中的虚拟dom是指:生成能够生成真实节点的vnode,并通过vnode渲染成真实节点后,将当前这个vnode缓存起来,在下次状态更新导致dom更新的时候(会生成一份新的vnode)进行对比,那么只改变需要变更的地方从而最大化节省dom操作成本的一种解决方案。