代码如下:

  1. <div id="app">
  2. {{someVar}}
  3. </div>
  4. <script type="text/javascript">
  5. new Vue({
  6. el: '#app',
  7. data: {
  8. someVar: 'init'
  9. },
  10. mounted(){
  11. setTimeout(() => this.someVar = 'changed', 3000)
  12. }
  13. })
  14. </script> 

页面初始会显示 "init" 字符串,3秒钟之后,会更新为 "changed" 字符串。

为了便于理解,将流程分为两个阶段:

  1. 首次渲染,生成 vnode,并将其挂载到页面中
  2. 再次渲染,根据更新后的数据,再次生成 vnode,并将其更新到页面中

第一阶段

流程

  1. vm.$mount(vm.$el) => render = compileToFunctions(template).render => updateComponent() => vnode = render() => vm._update(vnode) => patch(vm.$el, vnode)

说明

由 render() 方法生成 vnode,然后由 patch() 方法挂载到页面中。

render() 方法

render() 方法根据当前 vm 的数据生成 vnode。

该方法可以是新建 Vue 实例时传入的 render() 方法,也可以由 Vue 的 compiler 模块根据传入的 template 自动生成。

本例中该方法是由 el 属性对应的 template 生成的,代码如下:

  1. (function() {
  2. with (this) {
  3. return _c('div', {
  4. attrs: {
  5. "id": "app"
  6. }
  7. }, [_v("\n" + _s(someVar) + "\n")])
  8. }

实例化 Vue 时传入这样的参数可以达到相似的效果(区别在于变量两边的空格):

  1. new Vue({
  2. data: {
  3. someVar: 'init'
  4. },
  5. render: function(createElement){
  6. return createElement(
  7. 'div',
  8. {
  9. attrs: {
  10. "id": "app"
  11. }
  12. },
  13. [
  14. this.someVar
  15. ]
  16. )
  17. },
  18. mounted(){
  19. setTimeout(() => this.someVar = 'changed', 3000)
  20. }
  21. }).$mount('#app')

Vnode() 类

Vnode 是虚拟 DOM 节点类,其实例 vnode 是一个包含着渲染 DOM 节点所需要的一切信息的普通对象。

上述的 render() 方法调用后会生成 vnode 对象,这是第一次生成,将其称为 initVnode,结构如下(选取部分属性):

  1. {
  2. children: [
  3. {
  4. children: undefined,
  5. data: undefined,
  6. elm: undefined,
  7. tag: undefined,
  8. text: 'init'
  9. }
  10. ],
  11. data: {
  12. attrs: {
  13. id: 'app'
  14. }
  15. },
  16. elm: undefined,
  17. tag: 'div',
  18. text: undefined
  19. }

简要介绍其属性:

  1. children 是当前 vnode 的子节点(VNodes)数组,当前只有一个文本子节点
  2. data 是当前 vnode 代表的节点的各种属性,是 createElement() 方法的第二个参数
  3. elm 是根据 vnode 生成 HTML 元素挂载到页面中后对应的 DOM 节点,此时还没有挂载,所以为空
  4. tag 是当前 vnode 对应的 html 标签
  5. text 是当前 vnode 对应的文本或者注释

children 和 text 是互斥的,不会同时存在。

生成了 vnode 之后,就要根据其属性生成 DOM 元素并挂载到页面中了,这是 patch() 方法要做的事情,下面看其内部的流程:

  1. patch(vm.$el, vnode) => createElm(vnode, [], parentElm, nodeOps.nextSibling(oldElm)) => removeVnodes(parentElm, [oldVnode], 0, 0)

patch(oldVnode, vnode) 方法

根据参数的不同,该方法的处理方式也不同,oldVnode 有这几种可能的取值:undefined、ELEMENT_NODE、VNode,vnode 有这几种可能的取值:undefined、VNode,所以组合起来一共是 3 * 2 = 6 种处理方式:

oldVnode vnode 操作
undefined undefined -
ELEMENT_NODE undefined invokeDestroyHook(oldVnode)
Vnode undefined invokeDestroyHook(oldVnode)
undefined Vnode createElm(vnode, [], parentElm, refElm)
ELEMENT_NODE Vnode createElm(vnode, [], parentElm, refElm)
Vnode Vnode patchVnode(oldVnode, vnode)

可以看到,处理方式可以分为3种情况:

  1. 如果 vnode undefined,就要删除节点
  2. 如果 oldVnode undefined 或者是 DOM 节点,vnode VNode 实例的话,表示是第一次渲染 vnode,调用 createElm() 方法创建新节点
  3. 如果 oldVnode vnode 都是 VNode 类型的话,就要调用 patchVnode() 方法来对 oldVnode vnode 做进一步处理了,第二阶段流程会介绍这种情况

本阶段流程是首次渲染,符合第 2 种情况,下面看 createElm() 方法的实现:

createElm(vnode, [], parentElm, refElm) 方法

该方法根据 vnode 的属性创建组件或者普通 DOM 元素,有如下几种处理方式:

  1. 调用 createComponent() 方法对 component 做处理,这里就不再展开讨论。
  2. vnode.tag 存在:
  3. 调用 nodeOps.createElement(tag, vnode) 创建 DOM 元素,
  4. 调用 createChildren() 方法递归创建子节点。
  5. 调用 invokeCreateHooks() 方法调用生命周期相关的 create 钩子处理 vnode.data 数据
  6. vnode 是文本类型,调用 nodeOps.createTextNode(vnode.text) 创建文本元素

对于2,3 这两种情况,最后都会调用 insert() 方法将生成的 DOM 元素挂载到页面中。此时,页面的 DOM 结构如下:

  1. <body>
  2. <div id="app">
  3. {{someVar}}
  4. </div>
  5. <div id="app">
  6. init
  7. </div>

可以看到,原始的 DOM 元素还保留在页面中,所以在createElm() 方法调用之后,还会调用 removeVnodes() 方法,将原始的 DOM 元素删除掉。

这样,就完成了首次视图的渲染。在这个过程中,Vue 还会做一些额外的操作:

将 vnode 保存到 vm._vnode 属性上,供再次渲染视图时与新 vnode 做比较

  1. {
  2. children: [
  3. {
  4. children: undefined,
  5. data: undefined,
  6. elm: Text, // text
  7. tag: undefined,
  8. text: 'init'
  9. }
  10. ],
  11. data: {
  12. attrs: {
  13. id: 'app'
  14. }
  15. },
  16. elm: HTMLDivElement, // div#app
  17. tag: 'div',
  18. text: undefined

可以看到,vnode 及其子节点的 elm 属性更新为了页面中对应的 DOM 节点,不再是 undefined,也是为了再次渲染时使用。

第二阶段

流程

updateComponent() => vnode = render() => vm._update(vnode) => patch(oldVnode, vnode)

第二阶段渲染时,会根据更新后的 vm 数据,再次生成 vnode 节点,称之为 updateVnode,结构如下:

  1. {
  2. children: [
  3. {
  4. children: undefined,
  5. data: undefined,
  6. elm: undefined,
  7. tag: undefined,
  8. text: 'changed'
  9. }
  10. ],
  11. data: {
  12. attrs: {
  13. id: 'app'
  14. }
  15. },
  16. elm: undefined,
  17. tag: 'div',
  18. text: undefined
  19. }

可以看到, updateVnode 与 最初生成的 initVnode 的区别就是子节点的 text 属性由 init 变为了 changed,正是符合我们预期的变化。

生成新的 vnode 之后,还是要调用 patch 方法对 vnode 做处理,不过这次参数发生了变化,第一个参数不再是要挂载的DOM节点,而是 initVnode,本次 patch() 方法调用的流程如下:

  1. patch(oldVnode, vnode) => patchVnode(oldVnode, vnode) => updateChildren(elm, oldCh, ch) => patchVnode(oldCh, ch) => nodeOps.setTextContent(elm, vnode.text)

其中 oldVnode 就是第一阶段保存的 vm._vnode,elm 就是第一阶段更新的 elm 属性。

根据上面对 patch() 方法的分析,此时 oldVnode 和 vnode 都是 VNode 类型,所以调用 patchVnode() 方法做进一步处理。

patchVnode(oldVnode, vnode) 方法

该方法包含两个主要流程:

  1. 更新自身属性,调用 Vue 内置的组件生命周期 update 阶段的钩子方法更新节点自身的属性,类似之前的 invokeCreateHooks() 方法,这里不再展开说明
  2. 更新子节点,根据子节点的不同类型调用不同的方法

根据 vnode 的 children 和 text 属性的取值,子节点有 3 种可能:

  1. children 不为空,text 为空
  2. children 为空,text 不为空
  3. children text 都为空

由于 oldVnode 和 vnode 的子节点都有 3 种可能:undefined、children 或 text,所以一共有 3 * 3 = 9 种操作:

oldCh ch 操作
children text nodeOps.setTextContent(elm, vnode.text)
text text nodeOps.setTextContent(elm, vnode.text)
undefined text nodeOps.setTextContent(elm, vnode.text)
children children updateChildren(elm, oldCh, ch)
text children setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1)
undefined children addVnodes(elm, null, ch, 0, ch.length - 1)
children undefined removeVnodes(elm, oldCh, 0, oldCh.length - 1)
text undefined nodeOps.setTextContent(elm, '')
undefined undefined -

可以看到,大概分为这几类处理方式:

  1. 如果 ch 是 text ,那么就对 DOM 节点直接设置新的文本;
  2. 如果 ch 为 undefined 了,那么就清空 DOM 节点的内容
  3. 如果 ch 是 children 类型,而 oldCh是 文本或者为 undefined ,那么就是在 DOM 节点内新增节点
  4. ch 和 oldCh 都是 children 类型,那么就要调用 updateChildren() 方法来更新 DOM 元素的子节点

updateChildren(elm, oldCh, ch) 方法

updateChildren() 方法是 Vnode 处理方法中最复杂也是最核心的方法,它主要做两件事情:

  1. 递归调用 patchVnode 方法处理更下一级子节点
  2. 根据各种判断条件,对页面上的 DOM 节点进行尽可能少的添加、移动和删除操作

下面分析方法的具体实现:

oldCh 和 ch 是代表旧和新两个 Vnode 节点序列,oldStartIdx、newStartIdx、oldEndIdx、newEndIdx 是 4 个指针,指向 oldCh 和 ch 未处理节点序列中的的开始和结束节点,指向的节点命名为 oldStartVnode、newStartVnode、oldEndVnode、newEndVnode。指针在序列中从两边向中间移动,直到 oldCh 或 ch 中的某个序列中的全部节点都处理完毕,这时,如果另一个序列尚有未处理完毕的节点,会再对这些节点进行添加或删除。

先看 while 循环,在 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx 条件下,分为这几种情况:

  1. isUndef(oldStartVnode) 和 isUndef(oldEndVnode) 在第一次循环时是不会触发的,需要后续条件才可能触发,下面会分析到
  2. sameVnode(oldStartVnode, newStartVnode) 和 sameVnode(oldEndVnode, newEndVnode) 情况下不用移动 DOM 节点,只移动指针,比如:[A, B] => [A, C]
  3. sameVnode(oldStartVnode, newEndVnode) 情况下,是要将 oldStartVnode 向右移动到 oldEndIdx 对应的节点后面,比如:[A, B] => [C, A]
  4. sameVnode(oldEndVnode, newStartVnode) 情况下,是要将 oldEndVnode 向左移动到 oldStartIdx 对应的节点前面,比如:[A, B] => [B, C]
  5. 在以上条件都不满足的情况下,就要根据 newStartVnode 的 key 属性来进一步处理:
    1. 如果 newStartVnode 没有对应到 oldCh 中的某个元素,比如:[A, B] => [C],说明这个节点是新增加的,那么就调用 createElm() 新建节点及其子节点
    2. 如果 newStartVnode 对应到了 oldCh 中的某个元素,比如:[A, B, C] => [B, A, E],那么就直接移动该元素到 oldStartIdx 对应的节点前面,同时还会将 oldCh 中对应的节点置为 undefined,表示元素已经处理过了,此时,oldCh == [A, undefined, C],这样,在后续的循环中,就可以触发 isUndef(oldStartVnode) 或 isUndef(oldEndVnode) 条件了
    3. 另外,还可能会有重复 key 或者 key 相同但是 tag 等属性不同的情况,比如:[A, B, C] => [B, A, A, C],对于这类情况,newStartVnode 也会被作为新元素处理

循环结束时,必然会满足 oldStartIdx > oldEndIdx 或 newStartIdx > newEndIdx 两种情况之一,所以对这两种情况需要进一步处理:

  1. oldStartIdx > oldEndIdx 的情况,比如 [A] => [A, B, C],循环结束时,ch 中的 B 和 C 都还没有添加到页面中,这时就会调用 addVnodes() 方法将他们依次添加
  2. newStartIdx > newEndIdx 的情况,比如 [A, B, C] => [D],循环结束时,A, B, C 都还保留在页面中,这时需要调用 removeVnodes() 将他们从页面中移除

如果循环结束时,新旧序列中的节点全部都处理完毕了,如:[A, B] => [B, A],那么,虽然也会触发这两种逻辑之一,但是并不会对 DOM 产生实际的影响。

下面通过一些例子来展示该方法对 DOM 节点的操作流程:

[A, B] => [A, C]

序号 说明 oldStartIdx oldEndIdx newStartIdx newEndIdx DOM
0 初始状态 0 1 0 1 A, B
1 第一次循环,满足 sameVnode(oldStartVnode, newStartVnode), 无 DOM 操作 1 1 1 1 A, B
2 第二次循环,满足 isUndef(idxInOld) 条件,新增 C 到 B 之前 1 1 2 1 A, C, B
2 循环结束,满足 newStartIdx > newEndIdx,将 B 移除 1 1 2 1 A, C

[A, B] => [C, A]

序号 说明 oldStartIdx oldEndIdx newStartIdx newEndIdx DOM
0 初始状态 0 1 0 1 A, B
1 第一次循环,满足 sameVnode(oldStartVnode, newEndVnode) ,移动 A 到 B 之后 1 1 0 0 B, A
2 第二次循环,满足 isUndef(idxInOld) 条件,新增 C 到 B 之前 1 1 1 0 C, B, A
2 循环结束,满足 newStartIdx > newEndIdx,将 B 移除 1 1 1 0 C, A

[A, B, C] => [B, A, E]

序号 说明 oldCh oldStartIdx oldEndIdx ch newStartIdx newEndIdx DOM
0 初始状态 [A, B, C] 0 2 [B, A, E] 0 2 A, B, C
1 第一次循环,满足 sameVnode(elmToMove, newStartVnode),移动 B 到 A 之前 [A, undefined, C] 0 2 [B, A, E] 1 2 B, A, C
2 第二次循环,满足 sameVnode(oldStartVnode, newStartVnode),无 DOM 操作 [A, undefined, C] 1 2 [B, A, E] 2 2 B, A, C
3 第三次循环,满足 isUndef(oldStartVnode),无 DOM 操作 [A, undefined, C] 2 2 [B, A, E] 2 2 B, A, C
4 第四次循环,满足 isUndef(idxInOld),新增 E 到 C 之前 [A, undefined, C] 2 2 [B, A, E] 3 2 B, A, E, C
5 循环结束,满足 newStartIdx > newEndIdx,将 C 移除 [A, undefined, C] 2 2 [B, A, E] 3 2 B, A, E

[A] => [B, A]

序号 说明 oldStartIdx oldEndIdx newStartIdx newEndIdx DOM
0 初始状态 0 0 0 1 A
1 第一次循环,满足 sameVnode(oldStartVnode, newEndVnode),无 DOM 操作 1 0 0 0 A
2 循环结束,满足 oldStartIdx > oldEndIdx ,新增 B 到 A 之前 1 0 0 1 B, A

[A, B] => [B, A]

序号 说明 oldStartIdx oldEndIdx newStartIdx newEndIdx DOM
0 初始状态 0 1 0 1 A, B
1 第一次循环,满足 sameVnode(oldStartVnode, newEndVnode),移动 A 到 B 之后 1 1 0 0 B, A
2 第二次循环,满足 sameVnode(oldStartVnode, newStartVnode) 条件,无 DOM 操作 2 1 1 0 B, A
3 循环结束,满足 oldStartIdx > oldEndIdx ,无 DOM 操作 2 1 1 0 B, A

通过以上流程,视图再次得到了更新。同时,新的 vnode 和 elm 也会被保存,供下一次视图更新时使用,欢迎指正不对之处。

vue虚拟DOM源码学习-vnode的挂载和更新流程的更多相关文章

  1. vue源码学习-vnode的挂载和更新流程

    概述 本文主要介绍在视图的渲染过程中,Vue 是如何把 vnode 解析并挂载到页面中的.我们通过一个最简单的例子来分析主要流程: <div id="app"> {{s ...

  2. vue 2.0源码学习笔记—new Vue ( Vue 初始化过程 )

    new Vue(Vue 初始化) 一个vue实例化到底经历了什么?已下是博主自己的总结,不正确的地方请指出,谢谢~ 一.简述 从使用角度来看,挂载的顺序如下 1. $slots 2. $scopedS ...

  3. ibatis源码学习1_整体设计和核心流程

    背景介绍ibatis实现之前,先来看一段jdbc代码: Class.forName("com.mysql.jdbc.Driver"); String url = "jdb ...

  4. Hadoop源码学习笔记之NameNode启动场景流程五:磁盘空间检查及安全模式检查

    本篇内容关注NameNode启动之前,active状态和standby状态的一些后台服务及准备工作,即源码里的CommonServices.主要包括磁盘空间检查. 可用资源检查.安全模式等.依然分为三 ...

  5. Hadoop源码学习笔记之NameNode启动场景流程四:rpc server初始化及启动

    老规矩,还是分三步走,分别为源码调用分析.伪代码核心梳理.调用关系图解. 一.源码调用分析 根据上篇的梳理,直接从initialize()方法着手.源码如下,部分代码的功能以及说明,已经在注释阐述了. ...

  6. Hadoop源码学习笔记之NameNode启动场景流程三:FSNamesystem初始化源码剖析

    上篇内容分析了http server的启动代码,这篇文章继续从initialize()方法中按执行顺序进行分析.内容还是分为三大块: 一.源码调用关系分析 二.伪代码执行流程 三.代码图解 一.源码调 ...

  7. Hadoop源码学习笔记之NameNode启动场景流程二:http server启动源码剖析

    NameNodeHttpServer启动源码剖析,这一部分主要按以下步骤进行: 一.源码调用分析 二.伪代码调用流程梳理 三.http server服务流程图解 第一步,源码调用分析 前一篇文章已经锁 ...

  8. (转)OpenFire源码学习之四:openfire的启动流程

    转:http://blog.csdn.net/huwenfeng_2011/article/details/43413233 openfire启动 ServerStarter 启动流程图: 启动的总入 ...

  9. vnode的挂载和更新流程 -- 简介.

    来源 vnode原理 diff图解 <div id="app"> {{someVar}} </div> <script type="text ...

随机推荐

  1. 51nod 1366 贫富差距(flody)

    http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1366 题意: 思路: 如果不是一个连通块的话,肯定是无穷大的. 用flo ...

  2. HDU 5727 Necklace(全排列+二分图匹配)

    http://acm.split.hdu.edu.cn/showproblem.php?pid=5727 题意:现在有n个阳珠子和n个阴珠子,现在要把它们串成项链,要求是阴阳珠子间隔串,但是有些阴阳珠 ...

  3. jQuery页面加载初始化常用的三种方法

    当页面打开时我们需要执行一些操作,这个时候如果我们选择使用jquery的话,需要重写他的3中方法,自我感觉没什么区 别,看个人喜好了,第二种感觉比较简单明了: 第一种: 复制代码代码如下: <s ...

  4. 折腾了好久的地图缩放 ngui 各种偷懒实现

    当时找到一篇cocos2dx 地图缩放的  很遗憾我用不了  也要记录一下 免得以后用ugui可以用 转 http://blog.csdn.net/cocosnode/article/details/ ...

  5. 【ATcoder】D - Half Reflector

    题目链接:http://agc011.contest.atcoder.jp/tasks/agc011_d 每次都是两道题惨啊.... 想了想大概做法,既然小球走过去就会导致装置变化?那么是不是有一点像 ...

  6. python 加密

    https://github.com/duanhongyi/gmssl

  7. Centos7安装JDK8以及环境配置

    下载,选择centos7 64位版本 https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.h ...

  8. node+ts的心得与坑

    首先先明确,用node+ts的目的,为什么不ng+ts.这一点后面还会反复提醒自己 node毕竟不是ng. 用node的理由: 处理js,在后端操纵dom,读写类html格式的东西,比直接用py的后端 ...

  9. Python Scrapy 爬虫框架实例(一)

    之前有介绍 scrapy 的相关知识,但是没有介绍相关实例,在这里做个小例,供大家参考学习. 注:后续不强调python 版本,默认即为python3.x. 爬取目标 这里简单找一个图片网站,获取图片 ...

  10. python三级菜单实例(傻瓜版和进阶版)

    程序: python三级菜单 要求: : 1.打印省.市.县三级菜单 2.可返回上一级 3.可随时退出程序 方案一:傻瓜版(其实傻瓜版考察的主要是思路!思路清楚了,那才不是傻瓜!O(∩_∩)O哈哈~) ...