Cosen95/blog

Vue源码探秘(Virtual DOM)

Opened this issue · 0 comments

引言

Virtual DOM(后文简称vdom)的概念大规模的推广得益于react的出现,vdom也是react框架比较重要的特性之一。相比较频繁的手动去操作dom而带来性能问题,vdom很好的将dom做了一层映射关系,进而将在我们本需要直接进行dom的一系列操作,映射到了操作vdom

Vue.js 2.0引入vdom,比Vue.js 1.0的初始渲染速度提升了 2-4 倍,并大大降低了内存消耗。那么,什么是vdom呢?

让我们进入今天的文章。

VNode

VNodeVirtual DOMVue.js 中的数据结构定义,它被定义在 src/core/vdom/vnode.js 中:

// src/core/vdom/vnode.js
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

  // 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;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional 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.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;
  }

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

可以看到 VNode 是一个类,有很多属性。每一个vnode都映射到一个真实的dom节点上。我们这里先了解几个重要的属性:

  • tag: 对应真实节点的标签名
  • data: 当前节点的相关数据(节点上的class,attribute,style以及绑定的事件),是 VNodeData 类型。该类型声明在 flow/vnode.js 中:
// flow/vnode.js
declare interface VNodeData {
  key?: string | number;
  slot?: string;
  ref?: string;
  is?: string;
  pre?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | Array<Object> | Object;
  normalizedStyle?: Object;
  props?: { [key: string]: any };
  attrs?: { [key: string]: string };
  domProps?: { [key: string]: any };
  hook?: { [key: string]: Function };
  on?: ?{ [key: string]: Function | Array<Function> };
  nativeOn?: { [key: string]: Function | Array<Function> };
  transition?: Object;
  show?: boolean; // marker for v-show
  inlineTemplate?: {
    render: Function,
    staticRenderFns: Array<Function>
  };
  directives?: Array<VNodeDirective>;
  keepAlive?: boolean;
  scopedSlots?: { [key: string]: Function };
  model?: {
    value: any,
    callback: Function
  };
}
  • children: vnode的子节点
  • text:当前节点的文本
  • elm: 当前虚拟节点对应的真实节点
  • parent: 当前节点的父节点

看完VNodeVue.js中的数据结构定义,我想你已经大概知道vdom是什么了吧。

Virtual DOM 是什么?

本质上来说,vdom只是一个简单的js对象,并且最少包含tagpropschildren三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名(tag)属性(props)子元素对象(children)。下面是举一个经典的vdom例子:

<div>
  Hello jack-cool
  <ul>
    <li id="1" class="li-1">
      我是森林
    </li>
  </ul>
</div>

vdomdom对象有着一一对应的关系,上面的html对应生成的vdom如下:

{
    tag: "div",
    props: {},
    children: [
        "Hello jack-cool",
        {
            tag: "ul",
            props: {},
            children: [{
                tag: "li",
                props: {
                    id: 1,
                    class: "li-1"
                },
                children: ["我是", "森林"]
            }]
        }
    ]
}

Virtual DOM 有什么作用?

vdom的最终目标是将vnode渲染到视图上。但是如果直接使用新节点覆盖旧节点的话,会有很多不必要的DOM操作。

我们先来看下引入vdom前后,实现视图更新的不同流程:

引入 vdom 之前

  • 数据 + 模板生成真实 DOM
  • 数据发生改变
  • 新的数据 + 模板生成新的 DOM
  • 新的 DOM 替换掉原来的 DOM

这么做的缺点在于:即使模板中只有一个元素发生了变化,也会把整个模板替换掉。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,会有很多不必要的DOM操作而造成性能上的损失。

为了避免不必要的DOM操作,vdomvnode映射到视图的过程中,将vnode与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM

引入 vdom 之后

  • 数据 + 模板生成虚拟 DOM
  • 虚拟 DOM 生成真实 DOM
  • 数据发生改变
  • 新的数据 + 模板生成新的虚拟 DOM 而不是真实 DOM
  • 用新的虚拟 DOM 和原来的虚拟 DOM 作对比(diff 算法,后面会详细介绍)【性能 up↑
  • 找出发生改变的元素
  • 直接修改原来的真实 DOM【性能 up↑

总结

这一节我带大家大概了解了Virtual DOM的概念。Vue.jsVNode其实是借鉴了 snabbdom 的实现。

VNode 到真实 DOM 需要经过 creatediffpatch 等几个过程。本小节呢,我们只是大概了解一下vdom是什么以及它有什么作用。关于vdom的一些详细概念、流程和内部实现,我会在后面的章节中和大家分享(其实关于Virtual DOM单独出一个系列文章也不为过)。