Vue的数据响应式
本文内容基本来自于Evan You在FrontendMaster上的《Advanced Vue.js Features form the Ground Up》课程中第二节Reactivity中的内容。课程为英文授课,根据我自己的理解总结了这篇文章,课程其余内容可见我的另一篇博客。
最后一部分内容为我个人的一些总结,及官方文档的一些内容。
什么是数据响应式
说道数据响应式,首先要说一下响应式编程。目前响应式编程中比较流行的一个框架就是Rx.js,但响应式编程不仅仅指Rx.js这一个库。
响应式编程指当改变一个状态之后,整个系统应该随状态的更新而一同更新,在Vue中,也可以特指当数据状态改变时数据所绑定的DOM应该一同改变。
这里举一个简单的例子
1 | let a = 3 |
假设我们需要有一个新的变量,其值是a的十倍,那么一个简单的写法如下
1 | let b = a * 10 |
但是这有一个明显的问题就是当a改变时b并不会随之改变
1 | a = 4 |
也许我们可以手动重新给b赋值,完成需求
1 | a = 4 |
但是这明显很过程式,并且容易疏忽。分析我们的需求,其实就类似于办公软件Excel。当我们改变一个表格内容时,里一个表格会根据预设好的公式自动更新。
假设我们有一个函数,可以实现类似Excel的功能,那么使用时的代码应该类似如下
1 | onAChanged(() => { |
在实现这个onAChanged之前,我们来看一个更加贴合前端需求的代码
1 | <span class="cell b1"></span> |
我们需要这个span的内容根据state.a的值乘以10进行显示,使用命令式写法,可以写为如下
1 | document |
使用上面还未实现的onStateChange,代码应该类似如下
1 | onStateChanged(() => { |
当我们把与DOM交互的部分抽离出去,那么代码应该如下
1 | <span class="cell b1"> |
1 | onStateChanged(() => { |
其中view = render(state)这个公式,就是一个高度的抽象,涉及到了与DOM交互的种种细节,我们这里先不管。那么onStateChanged这个函数,就是关于数据响应式的核心,我们也许可是实现如下
1 | let update // 使用全局变量保存更新方法 |
当我们使用上述实现时,就比较像React了
1 | onStateChanged(() => { |
不过Vue中的数据响应式明显并非如此实现,而是类似如下
1 | onStateChanged(() => { |
Vue将数据对象转换为一个响应式的对象。
Vue中的数据响应式
Vue使用了ES5规范中的Object.defineProperty方法,改写了数据对象中所有属性的getter和setter方法,将上述的onStateChanged方法变更为类似如下
1 | autorun(() => { |
Object.defineProperty
我们可以首先实现一个convert方法,接受一个对象为参数,在convert方法内部使用Object.defineProperty来将传入的参数对象添加功能,除正常操作外,每次获取、更改对象属性值就在控制台进行打印,使用时类似如下
1 | const obj = { foo: 123 } |
我们可以实现如下
1 | function convert(obj) { |
可见,我们使用了Object.defineProperty来更改了对象的默认行为,在获取或设置对象的属性值时,我们添加了“副作用”,这里的副作用就是控制台打印。显然,我们不仅仅可以控制台打印一些字符串,还一个在获取或设置对象属性值时添加其他行为。
依赖追踪
接下来我们可以实现一个依赖追踪系统和一个autorun函数,我们使用class语法实现依赖追踪。这个class应该有两个方法,depend和notify。autorun函数应该接受一个update函数,并在内部调用depend方法。这样调用notify时就会调用传入autorun内的函数,使用类似如下:
1 | const dep = new Dep(); |
我们可以实现如下:
1 | class Dep { |
Observe
接下来我们可以将上面两个功能结合起来,实现一个observe方法。
observe方法使用类似如下
1 | const state = { |
代码实现
1 | class Dep { |
我们可以把autorun内的update函数替换view = render(state),至此,我们可以说是实现了一个与Vue原理类似的数据响应式系统。
Vue数据响应式系统的一些限制
须在声明时初始化data对象的全部属性
了解了Vue数据响应系统的一些原理,就可以立即Vue官方文档中深入响应式原理的一些内容。
首先,我们只能将对象中已经存在的属性的getter和setter进行转化,所以最好在创建Vue实例是就将需要的属性在data对象上创建好,举例来说
1 | const vm = new Vue({ |
所以我们最好将所需要的全部属性在data对象上声明好,而不是后期动态的添加。
我们也可以使用绑定在Vue构造函数原型上的set方法或者实例对象上的$set方法来手动的将属性变为响应式的。
data对象关于数组的一些转换
在data对象中含有数组时,由于无法感知数组的长度,为了能够将数组变为响应式的,Vue在原生的数组中增加了一层,提供了一些方法来替换原生数组变异(mutate)原数组的方法,分别为
- push
- pop
- shift
- unshift
- splice
- reverse
- sort
Vue组件中使用函数返回对象而不是直接使用对象
在单独的Vue实例中,data属性使用对象和函数返回一个对象并没有什么不同。但是如果我们多次复用同一组件,在直接使用对象时,多个组件的data属性所指向的对象的引用为同一对象,由于Vue的响应式系统,同一对象无疑会共享所有的数据及响应方式,会造成一些意向不到的情况。所以在将Vue实例作为组件时,推荐使用函数返回对象的方式进行使用,这样即可使组件间data对象的形状(shape)相同,又不是同一引用,保持了各个data对象的独立性。