前言
本文用最简单的方式来解释VUE2 最重点的响应式原理,看不懂算我输!
一、 响应式原理
什么是响应式原理?
意思就是在改变数据的时候,视图会跟着更新。这意味着你只需要进行数据的管理,给我们搬砖提供了很大的便利。
VUE是利用了Object.defineProperty的方法里面的setter 与getter方法的观察者模式来实现。
所以在学习VUE的响应式原理之前,先学习两个预备知识:
Object.defineProperty 与 观察者模式。
对这两个概念不懂的可以看文尾。
二、原理解析
在学完了前面的铺垫之后,我们终于可以开始讲解VUE的响应式原理了。
官网用了一张图来表示这个过程,但是刚开始看可能看不懂,等到文章的最后,我们再来看,应该就能看懂了。
总共分为三步骤:
1、init 阶段:VUE 的 data的属性都会被reactive化,也就是加上 setter/getter函数。
function defineReactive(obj: Object, key: string, ...) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
....
dep.depend()
return value
....
},
set: function reactiveSetter (newVal) {
...
val = newVal
dep.notify()
...
}
})
}
class Dep {
static target: ?Watcher;
subs: Array<Watcher>;
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
其中这里的Dep就是一个观察者类,每一个data的属性都会有一个dep对象。当getter调用的时候,去dep里注册函数, 至于注册了什么函数,我们等会再说。
setter的时候,就是去通知执行刚刚注册的函数。
2、mount 阶段:
mountComponent(vm: Component, el: ?Element, ...) {
vm.$el = el
...
updateComponent = () => {
vm._update(vm._render(), ...)
}
new Watcher(vm, updateComponent, ...)
...
}
class Watcher {
getter: Function;
// 代码经过简化
constructor(vm: Component, expOrFn: string | Function, ...) {
...
this.getter = expOrFn
Dep.target = this // 注意这里将当前的Watcher赋值给了Dep.target
this.value = this.getter.call(vm, vm) // 调用组件的更新函数
...
}
}
看过我之前这篇 学习vue源码(4) 手写vm.$mount方法文章 的同学,相信会对mountComponent这个函数很熟悉,如图所示:
我们已经讲过了。
mount 阶段的时候,会创建一个Watcher类的对象。这个Watcher实际上是连接Vue组件与Dep的桥梁。
每一个Watcher对应一个vue component。
这里可以看出new Watcher的时候,constructor 里的this.getter.call(vm, vm)函数会被执行。getter就是updateComponent。这个函数会调用组件的render函数来更新重新渲染。
而render函数里,会访问data的属性,比如
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
此时会去调用这个属性blogTitle的getter函数,即:
// getter函数
get: function reactiveGetter () {
....
dep.depend()
return value
....
},
// dep的depend函数
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
在depend的函数里,Dep.target就是watcher本身(我们在class Watch里讲过,不记得可以往上第三段代码),这里做的事情就是给blogTitle注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了blogTitle,那么这个组件相对应的Watcher对象都会被注册到blogTitle的Dep中。
这个过程就叫做依赖收集。
收集完所有依赖blogTitle属性的组件所对应的Watcher之后,当它发生改变的时候,就会去通知Watcher更新关联的组件。
3、更新阶段:
当blogTitle 发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用update函数更新。
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
可以用一张图来表示:
由此图我们可以看出Watcher是连接VUE component 跟 data属性的桥梁。
总结
最后,我们通过解释官方的图来做个总结。
1、第一步:组件初始化的时候,先给每一个Data属性都注册getter,setter,也就是reactive化。然后再new 一个自己的Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM。在调用render的时候,就会需要用到data的属性值,此时会触发getter函数,将当前的Watcher函数注册进sub里。
2、第二步:当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。
整个过程就是那么简单啦~
附录
2.2 观察者模式
什么是观察者模式?它分为注册环节跟发布环节。
以报社订阅报纸来说明:
报社的业务就是出版报纸
- 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订阅客户,你就一直会收到新报纸。
- 当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
- 只要报社还在运营,就会一直有人(或单位)向他们订阅或取消订阅报纸。
出版者+订阅者=观察者模式
观察者模式定义了一系列对象之间的一对多的关系,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
这里来简单实现一个观察者模式的类
// 出版社
function Observer() {
this.dep = [];
register(fn) {
this.dep.push(fn)
}
notify() {
this.dep.forEach(item => item())
}
}
const reader = new Oberver();
// 每来一个订阅者就注册一个想执行的函数
reader.register(() => {'console.log("call daisy")'})
reader.register(() => {'console.log("call anny")'})
reader.register(() => {'console.log("call sunny")'})
// 最后新书籍出版之后,通知所有的客户
wantCake.notify()
2.1 Object.defineProperty
相信你既然看到源码了,这个也已经是不必介绍了。
VUE给data里所有的属性加上set,get这个过程就叫做Reactive化。