Vue.js(以下简称Vue)是时下流行的前端开发库,一般搭配其插件Vue-Router,Vuex一起使用,行业中称为Vue全家桶。

Vue使用了MVVM的理念,将表现层(DOM)和数据层进行了分离,其基本思想是数据和DOM的一体化,操作数据即可变更DOM,表单交互亦可通过v-model指令改变数据,将前端开发者从DOM、数据两手抓的泥潭中解放出来。

但是,使用这种方法的代价还是明显的,那就是Vue本身,因为这对大多数开发者而言是一个黑盒子。如果不能大概了解它做了哪些事情,那么有的时候遇到一些问题还是很头疼的。

本文关于Vue实现组件化的基本思路,总结了一些其中发生的故事,希望对今后的开发学习有所帮助。

最基本的模型

Vue2.0之后并没有将DOM和数据进行直接绑定,而是采用了VNode类,也就是常说的虚拟DOM。虚拟DOM其实并不神奇,它就是一个JS对象,描述了DOM元素的一些特征,但是它的属性要比真实DOM少得多,这就使得操作更加方便省时。

数据层位于Vue实例中(以下简称VM),该实例的渲染函数执行可得到VNode,然后Vnode通过patch过程渲染成真实DOM,真实DOM又通过注册事件改变VM,这就是一个基本的模型。

Vue实例的由来

那么VM从何而来呢?考虑以下实际问题:

// App.js
new Vue({
name: 'App',
components: { M },
template: `
<div>
<div>swamp</div>
<M />
</div>`
}).$mount('#app') // M.vue
<template>
<div :class="containerClass">
<div>I am M</div>
<ul>
<li v-for="item in listData">{{ item }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'M',
data () {
return {
listData: ['A', 'B', 'C', 'D', 'E', 'F'],
containerClass: 'container'
}
}
}
</script>

通过Vue的模板解析(parse)和Vue-loader,上述两种形式的组件定义都会最终变成一个options对象,这个对象包含了所有我们定义的属性,template被解析成渲染函数render和静态渲染函数staticRenderFns(这是Vue针对静态模板的优化)。上例中App.js通过new Vue得到第一个VM,接着执行render函数得到VNode,紧接着此VNode进入patch阶段,模板中的html标签是可以直接生成dom元素的;但是其中有一个M标签,这不是一个html标签,然而它作为一个组件已被注册在数据的components属性中,于是Vue拿到这个组件(一个options对象),然后通过Vue.extend(options)生成一个Vue的子类Sub,然后通过实例化这个Sub类得到M组件的VM(第二个VM)。当然,这个VM也会执行render和patch,然后插入到dom中去。

初始化patch过程

Vue有两类重要的Vnode,一种是占位Vnode(placeholder Vnode, 即上文中的M节点),它是一个虚拟的节点,作为M实际内容的父节点存在,这类节点的tag属性一般为Vue-component-2-M这种形式,而普通Vnode(Actual Vnode)的tag属性则是普通的html节点名称,如div, span, ul等。

普通Vnode节点的patch过程很简单,递归patch其子元素(createChildren),然后创建实际节点插入dom即可。

占位Vnode节点的patch过程则比较复杂,包含子元素的递归创建过程,上文所述M节点的VM创建即是在这个阶段进行的。

数据变更时的patch

如果通过某种操作变更了VM的数据,比如上面例子中,我们在M组件中调用this.containerClass = 'common'; this.listData = ['C', 'B', 'F', 'D', 'E', 'G', 'A'],此时M组件会再次render得到全新的VNode(这里再次render的触发基于双向数据绑定,这是Vue的一大核心,但不是本文重点),这个全新的VNode将与之前存在的VNode进行比较,得到差异后再patch进dom,从而完成更新。patch的含义是打补丁,用在这种场景再合适不过了。

VNode拥有和DOM类似的树形结构,在patch过程中,新老VNode进行同层比较:父节点与父节点比,子节点与子节点比,不会跨层比较(这其实是Vue针对树的编辑距离问题的一种处理,将时间复杂度降低到可以接受的程度)。在本例中,我们主要分析第三层的比较,以此阐述patch过程中diff算法的核心。

设老VNode的序列为O,新VNode的序列为N,分别保留指向序列开头和结尾的指针,称为Os, Oe, Ns, Ne。

diff的目的是调整O使其变为N(调整老DOM得到新DOM),使用了双指针算法:

首先是四次比较,比较的目的是发现相同的节点,用修改操作取代创建从而提高效率,这四次比较分别是:

  1. Os - Ns, 如果VNode相同则把两个指针均向右移动,说明新老节点相同,不做处理;
  2. Oe - Ne, 如果VNode相同则把两个指针均向左移动,说明新老节点相同,不做处理;
  3. Os - Ne, 如果VNode相同说明在新的序列中这个节点应该被移动到Oe后边,直接在DOM层面处理移动,然后把Os右移,Ne左移;
  4. Oe - Ns, 如果VNode相同说明在新的序列中这个节点应该被移动到Os前面,直接在DOM层面处理移动,然后把Ns右移,Oe左移;
  5. 如果以上四种情况都没有找到能够匹配的VNode,则在序列O中寻找Ns。可知如果采取遍历O序列的方式,diff算法的时间复杂度为O(min{len(O), len(N)} * len(O));而如果各个VNode拥有key属性,Vue会事先建立一个[key]-[Vnode]的散列表,依据Ns的key去查表只需要O(1)时间,则diff算法的时间复杂度为O(max{len(O), len(N)})。如果查找成功,就将找到的节点移动到Os的前方,并将Ns右移;
  6. 如果上述情况均不能成功,那就说明O序列中并没有Ns处的节点,只能创建一个新的节点,把它插入Os之前,并将Ns右移。

    上述循环进行直到Os出现在Oe之前或者Ns出现在Ne之前,此时如果Oe仍然在Os之前或者同一位置,说明原始序列中的这些元素要被删除;如果Ne仍然在Ns之前或者同一位置,说明原始序列中的这些元素要被新添加,按情况执行对应的操作即可。

依据此算法,上面例子中的情形可以图示为:



以上就是最基本的Vue从定义到组件化到生成DOM的过程。

最新文章

  1. Send Push Notifications to iOS Devices using Xcode 8 and Swift 3, APNs Auth Key
  2. Google Protocol Buffer 的使用和原理[转]
  3. Linux下怎么查看当前系统的版本
  4. JS获取阴历阳历和星期
  5. MPlayer-ww 增加边看边剪切功能+生成高质量GIF功能
  6. DbUtils使用时抛出Cannot get a connection
  7. Windows 2003 AD升级Windows 2008
  8. -_-#【Dom Ready / Dom Load】
  9. android生成apk包出现Unable to add &amp;quot;XXX&amp;quot; Zip add failed问题
  10. windows tcp端口映射或端口转发
  11. 怎样在iis中发布asp.net网站
  12. using 40 logical processors based on SQL Server licensing SqlServer CPU核心数限制问题
  13. 嵌入式 RTP通话:视频流(H.264)的传输
  14. python理解描述符(descriptor)
  15. (一)flask-sqlalchemy的安装和配置
  16. python random模块(获取随机数)
  17. 命令:history
  18. flask第二十八篇——HTML【1】table标签
  19. SpringMVC 配置多个dispatcher 及WebApplicationInitializer的使用
  20. 使用docker部署STF服务(CentOS环境)

热门文章

  1. oracle 索引操作
  2. 国产低功耗Soc蓝牙语音遥控器芯片HS6621 指纹锁、体脂称等应用方案
  3. python36
  4. 在windows上搭建spark遇到的问题
  5. OSPF配置常用命令知识总结
  6. kali WiFi相关研究(学习中...)
  7. RSTP-快速生成树协议
  8. dynamics 365/crm 导入解决方案报 发生 sql server 错误
  9. fatal error: openssl/ssl.h: No such file or director
  10. Finance财务软件(支持多账套专题)