vue中Virtual DOM源码学习

vue升级到2.0之后就加入了Virtual DOM,对于Virtual DOM的概念这里就不做过多的说明了。本文主要分析一下vue中Virtual DOM渲染到真实DOM是如何实现的。

最近我在研究关于markdown解析相关的东西,在Github上找到相关的解析器基本都是会把markdown解析为字符串的方式。在参考了多个开源库之后选择了marked作为解析库去解析markdown。选择marked的原因是因为marked相对与其他库来说代码量相对较少,一个人比较容易搞定,并且解析能力与效率都是很不错的,并且star数量也比较高,所以代码质量也是非常高的。

在编写markdown解析的时候,第一目的就是把markdown解析为vnode,然后再渲染到真实DOM。所以自己实现解析markdown部分就是基于marked实现的,在解析完成之后就得到了vnode的树形结构。

目前解析为vnode基本已经实现,但很多地方还需要进行优化。目前最需要解决的就是把vnode渲染到真实DOM中去,所以就选择vue中的Virtual DOM作为基础去研究。其实写这篇文章也是为自己写vnode的render方法整理思路,所以这也是为什么标题叫做学习而不是分析了。对于文章中错误的还望给我指正。

vue中的vnode的数据结构

vue的vnode代码部分位于项目的src/core/vdom文件夹下,vue的Virtual DOM是基于snabbdom修改的,vnode类的数据结构如下

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  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;

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array,
    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.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是如何渲染到Real DOM的

vue中负责把vnode渲染到Real DOM主要工作的是位于src/core/vdom/patch.js文件中的代码完成的
下面我们就来看看patch.js中都做了哪些工作呢,这里可以结合源码来看https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js

在阅读源码的时候一般的顺序都是看看这个文件都依赖了哪些文件,其次也是最重要的,文件导出了什么东西,导出的就是文件的核心部分了,也就是阅读源码的入口。

在文件顶部就可以看到文件的依赖了,这里引入的文件如下(只写了重要模块的作用)

import VNode from './vnode' // vnode类
import config from '../config' // vue的全局配置,包括运行环境的检测
import { SSR_ATTR } from 'shared/constants'
import { registerRef } from './modules/ref'
import { activeInstance } from '../instance/lifecycle' 

import {
  warn, // 打印警告信息
  isDef, // 判断值不为undefined和null
  isUndef, // 判断值为undefined或null
  isTrue,
  makeMap, // 把字符串按``,``分割为数组
  isPrimitive // 判断值是否为string、number或boolean
} from '../util/index' // 一些工具函数

导出模块一共有两个地方

  1. 位于[30行](https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js#L30)的导出了一个空的vnode对象
  2. 位于70行的生成主patch函数的函数

可以看到最重要的函数是createPatchFunction,createPatchFunction接受一个参数,并返回了函数patch。patch函数接受6个参数:

  • oldVnode: 旧的虚拟节点或旧的真实dom节点
  • vnode: 新的虚拟节点
  • hydrating: 是否要跟真是dom混合
  • removeOnly: 特殊flag,用于组件
  • parentElm:父节点
  • refElm: 新节点将插入到refElm之前

patch函数的主要思想:

  1. 如果vnode不存在但oldVnode存在,则表示要移除旧的node,那么就调用invokeDestroyHook(oldVnode)来进行销毁
  2. 如果oldVnode不存在但是vnode存在,说明是要创建新节点,那么就调用createElm来创建新节点
  3. 当vnode和oldVnode都存在时:
    1. 如果oldVnode与Vnode是同一节点是就调用patchVnode处理去比较两个节点的差异
    2. 当vnode和oldVnode不是同一个节点时,如果oldVnode是真实DOM节点或hydrating设置为true,需要用hydrate函数将虚拟DOM和真实DOM进行映射,然后将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点,根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置

patchVnode函数主要思路:

  1. 如果vnode===oldVnode则直接返回,不执行任何操作
  2. 如果oldVnode跟vnode都是静态节点,且具有相同的key,当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作
  3. 如果vnode不是text节点
    1. 如果oldVnode与vnode都有子节点,并且子节点不相等,就调用updateChildren执行更新子节点操作
    2. oldVnode没有子节点,vnode有子节点,则创建节点
    3. oldVnode有子节点,vnode没有子节点,就移除旧的节点
    4. 如果oldVnode为text节点,就移除文本节点
  4. vnode为text节点就设置节点文本内容

updateChildren函数实现功能:

  1. 分别获取oldVnode和vnode的第一个和最后一个节点,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode,oldStartIdx、newStartIdx、oldEndIdx、newEndIdx分别为oldVnode与vnode的子节点的下标,和最后一个子节点的下标,然后在while循环中执行比较,并且移动oldStartIdx和newStartIdx,直到oldStartIdx大于oldEndIdx或者newStartIdx大于newEndIdx
  2. 假如没有oldStartVnode就将oldStartIdx加1,并重新求得oldStartVnode,进入下一次循环
  3. 假如没有oldEndVnode就将oldEndIdx减1,并重新求得oldEndVnode,进入下一次循环
  4. 假如oldStartVnode和newStartVnode是相同类型的节点,就调用patchVnode去比较两个节点,并使oldStartIdx和newStartIdx都加1,同时开始节点也更新对应下标的节点
  5. 假如oldEndVnode和newEndVnode是同类型节点,就调用patchVnode去比较两个节点,并使oldEndIdx和newEndIdx都减去1,同时开始节点也更新对应下标的节点
  6. 假如oldStartVnode和newEndVnode是同类型节点,就调用patchVnode去比较两个节点,如果removeOnly是false,那么可以把oldStartVnode.elm移动到oldEndVnode.elm之后,并使oldStartIdx加1,newEndIdx减去1,同时更新对应节点为最新的节点
  7. 假如oldEndVnode和newStartVnode是同类型的节点,就调用patchVnode去比较两个节点,如果removeOnly是false,那么可以把oldEndVnode.elm移动到oldStartVnode.elm之前,并使oldEndIdx减1,newStartIdx加1,同时更新对应节点为最新的节点
  8. 如果以上条件都不匹配,则查找oldVnode中与vnode具有相同key的节点,并将查找的结果赋值给elmToMove。
    • 如果找不到相同key的节点,则表示是新创建的节点
    • 如果找到了,就判断这两个节点是否为同一类型的节点
      1. 若为同一类型就调用patchVnode,就将对应下标处的oldVnode设置为undefined,如果removeOnly是false,就把elmToMove.elm插入到oldStartVnode.elm之前,newStartIdx加1,且把newStartVnode设置为下一个节点
      2. 如果没有找到就直接创建新的节点,并执行newStartVnode = newCh[++newStartIdx]-
  9. 循环结束后,如果oldStartIdx > oldEndIdx,就把vnode中间没有循环到的节点添加到新DOM中
  10. 如果newStartIdx > newEndIdx,就把oldVnode中没有遍历到的节点从DOM中移除

至此,vue中实现VDOM至真实DOM的就基本讲解完成了,至于生命周期,在这里没有提及,只要在对应的地方加入生命周期回调就ok了

最后成果

在实现了vnode之后,自己编写了一个比较简单的diff-render的类,基本原理和上面讲到的差不多,实现效果也差不多,只在添加和删除新元素的时候会重新渲染同级的后面的兄弟node,地址https://github.com/markdown365/markdown365-parser,有兴趣的朋友可以下载下来看一下效果。最后写本文的时候参考了Vue原理解析之Virtual Dom部分内容,并且这篇文章讲的十分的详细,也推荐去看看

由于水平有限,文章中有不正确的地方还望原谅与指正,谢谢!

你可能感兴趣的:(vue中Virtual DOM源码学习)