Vue 是目前主流的前端框架之一,相信也是其中最容易上手的(没有之一),开发者只需具备 HTML、CSS 和 JavaScript 等基础知识,就可以根据官方教程开启 Vue 编程之旅。Vue 推崇使用 template
的方式实现页面,这使得开发者很容易将设计好的原型改造成 Vue 单文件组件,利用 Vue 提供的响应式系统,我们只需要关注数据的存储和变化,Vue 会自动在数据发生变化的时候更新页面的内容。那么这一切是如何实现的呢?本文将结合 MVVM(Model-View-ViewModel) 设计模式,剖析 Vue 响应式系统的原理。
MVVM
MVVM 模型由 Model、View 和 ViewModel 三部分组成,简单来讲,Model 是我们定义的数据和这些数据的业务逻辑;View 是这些数据展示给用户的形式;而 ViewModel 则是连接 Model 和 View 的桥梁,通过数据绑定保证了 Model 和 View 的一致性,即:当 Model 变化时,自动更新 View;当用户操作 View,并作出了更改时,同步 View 的数据到 Model。
从上图可见,VM 是 MVVM 模型的关键,而数据绑定又 VM 的核心技术。通常, ModelView 由框架来实现,Model 和 View 则由开发者自己定义。
Vue 模版组件示例
下面我们通过一个 Vue 模版组件示例来体会一下 MVVM 模型为开发者带来的好处:
可以在 CodeSandbox 中运行 https://codesandbox.io/s/vue-template-component-0692e
1 | <template> |
如果你对 Vue 有一定的了解,相信不用运行也能想象出这段代码的执行效果。这个组件的内容展示和交互都非常简单,只有一个输入框供用户输入数据,但是,当输入框中的输入数据变化,下面的文字也会跟着变化。结合上面讲到的 MVVM 模型,我们可以很容易的将组件中的 template
和 script
模块和 MVVM 模型中的 View 和 Model 对应起来。那么 VM 是谁来实现的呢?答案就是 Vue。
在阅读 Vue 官方文档的时候,我们经常会看到类似下面的代码:
1 | var vm = new Vue({ |
按照官方的说法:「Vue 没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm
(ViewModel 的缩写)这个变量名表示 Vue 实例 」。
注意上面加粗的几个字,我们在后面会做进一步分析。
Vue 响应式系统
关于 Vue 的响应式系统,Vue 官方教程中有一节专门的描述。下图就来自该文档,从这幅图中,我们可以对 Vue 响应式系统有一个大致的了解。
总体上来说,要实现一个响应式系统,至少需要完成两件事:1. 变化侦测:感知数据的变化;2. 依赖收集:收集哪些地方使用了数据,以便在数据发生变化时通知其进行更新。
变化侦测
对于对象和数组,Vue 采用了不同的方式来观测其变化,下面是 Vue 2.x 及之前版本的实现方式。
对于对象
Vue 采用的是 Object.defineProperty()
方法来实现的,即:对组件中 data
函数返回对象中的每一个属性通过 Object.defineProperty()
转换为 getter/setter
。通过这种方式,Vue 就可以感知到程序运行中对变量的赋值操作。
Vue 源码:https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
对于数组
数组与对象不同,Vue 通过覆盖 Array 原型方法的方式来实现的。JavaScript 中改变数组元素的方法有:push
、pop
、shift
、unshift
、splice
、sort
和reverse
共 7 个,只要拦截 Array 的这些操作我们就可一获知绝大多数情况下数组中元素的变化。
Vue 源码:https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js
注意事项
由于实现方式的限制,Vue 无法侦测一些变化,如果对这些操作缺乏了解,可能导致项目中出现一些预期之外的 Bug,即:修改了数据,但是却没有触发页面更新或者对应的 watch 方法。
- 无法检测属性的添加和删除;
- 对于已经创建的实例,无法添加根级别的响应式属性;
- 利用索引直接设置一个数组项的值;
- 修改数组长度
依赖收集
侦测到数据变化后,我们还需要更新使用了这个数据的地方,如:计算属性、watch
函数或者 template
模版,如何判断一个变量或属性在哪些地方使用了呢?这就轮到 getter
发挥作用了。我们可以认为使用了某个变量的地方依赖了这个变量,而使用的时候必定会触发其 getter
函数,所以我们可以在 getter
函数中进行相关的依赖收集,并在数据变化的时候通知这些依赖。
结合上面的 Vue 响应式原理图,我们所写的 template
最终会被 vue-template-compiler 转换为 render 函数,再通过 render 函数生成虚拟 DOM 树,在生成虚拟 DOM 树的时候,因为我们使用了 v-model
和 “Mustache” 语法 (双大括号) 绑定了 username
,所以,Vue 将 render 函数视为 username
的一个依赖,当我们在输入框中输入文字时,由于使用了双向绑定,username
会更新为当前输入的值,进一步触发 re-render
,span
中的 username
也更新为最终的值。