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<VNode>;
  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<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.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,用于<transition-group>组件
  • 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部分内容,并且这篇文章讲的十分的详细,也推荐去看看

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容