考虑如下代码:
<div id="app" @click="changeMsg">
{{ message }}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
changeMsg() {
this.message = 'Hello World!'
}
}
})
</script>
message更新对应的DOM更新怎么做到的呢?
这就不得不提到vue2内部的响应式原理了
响应式对象
首先需要将message变成响应式对象,核心原理就是Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty在对象上定义一个新属性,或修改对象的现有属性,并返回这个对象
参数:
- obj:要定义属性的对象
- prop:属性名称
- descriptor:属性描述符,有很多描述符,我们只关心
get、set描述符,就是分别定义getter方法和setter方法,读这个属性时会触发getter方法,修改这个属性时会触发setter方法
一个对象拥有了getter、setter,就可以称这个对象为响应式对象
那message是怎么变成了响应式对象的呢?
vue对如下属性做了初始化操作,让其变成响应式:
- props
- methods
- data
- computed
- wathcer
具体做法就是递归遍历里面的属性,给所有属性都添加getter、setter,因此遍历到message时就将message变成了响应式
依赖收集
message变成了响应式对象后,我们需要写getter方法的具体实现了
getter方法内部主要实现的就是依赖收集,具体怎么做呢?
首先我们要为message对象定义一个Dep 实例,部分代码如下:
function defineReactive(obj, key) {
const dep = new Dep() // 每个属性都有自己的 Dep
Object.defineProperty(obj, key, {
get: function () {},
set: function () {}
})
}
那么什么是Dep?
Dep
Dep是Dependency的缩写,译为依赖集
简单来说Dep实例就是收集所有的Watcher依赖
Dep部分实现:
class Dep {
constructor() {
this.subs = [] // 存储 Watcher 的数组
}
// 收集依赖 - 将当前 Watcher 添加到 subs 中
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
// 通知所有 Watcher 更新
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 静态属性,指向当前正在计算的 Watcher
Dep.target = null
那么什么是Watcher?
Watcher
Watcher译为观察者
Watcher部分实现:
class Watcher {
constructor(vm) {
this.vm = vm
// 设置 Dep.target 为当前 Watcher
Dep.target = this
}
update() {
// 数据变化时执行回调
this.vm.render()
}
}
当新建一个Vue实例的时候,会新建一个对应的Watcher实例
class Vue {
constructor() {
// 创建 Watcher
new Watcher(this)
}
意思是把当前Vue实例变成一个观察者,至于观察谁,现在还没有定
然后执行Watcher里的构造函数,执行Dep.target = this,将上面的Dep.target静态属性指向当前Watcher实例,为什么这么做呢?
都知道Dep.target是全局唯一的,也就是Watcher实例全局只能有一个,当编译到当前Vue实例的时候,Dep.target指向当前Vue实例下的Watcher实例,当编译到下一个Vue实例的时候,Dep.target指向下一个Vue实例下的Watcher实例
比如,当如下代码执行时:
<div id="app" @click="changeMsg">
{{ message }}
<ComponentA :val="message" />
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
changeMsg() {
this.message = 'Hello World!'
}
}
})
</script>
解析到app的Vue实例时,Dep.target指向app下的Watcher实例,然后解析app下的子组件ComponentA,Dep.target就指向了ComponentA下的Watcher实例
至于为什么Watcher实例全局只能有一个,下面会讲
实现getter
现在我们来实现getter方法,其实很简单,核心就是如下代码:
if (Dep.target) {
dep.depend()
}
还拿上面的代码来举例子:
<div id="app" @click="changeMsg">
{{ message }}
<ComponentA :val="message" />
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
changeMsg() {
this.message = 'Hello World!'
}
}
})
</script>
- 解析到
app的Vue实例,Dep.target指向app下的Watcher实例 - 解析到
{{ message }}时,触发message的getter方法,将app下的Watcher实例收集到message下的dep实例中 - 解析到
ComponentA的Vue实例,Dep.target指向ComponentA下的Watcher实例 - 解析到
ComponentA里的message,再次触发message的getter方法,将ComponentA下的Watcher实例收集到message下的dep实例中 - 此时
message下的dep实例中一共有两个Watcher实例
至此完成依赖收集
现在可以解释为什么Watcher实例全局只能有一个,因为解析到哪个Vue实例全局Watcher就变成当前Vue实例的Watcher,Vue从父到子全部遍历一遍以后,依赖收集就完成了,Vue遍历是线性的,所以Watcher实例全局只能有一个
总的来说就是定义一个Dep依赖集,将所有Watcher观察者添加到Dep里面,至于Watcher观察的是谁,很显然就是Dep对应的响应式对象(上面的message)
派发更新
实现完getter方法以后,我们来实现setter方法
实际上就是将Dep里所有收集到的Watcher,都触发它们的update方法过程
dep.notify()
当message更新时,对应Dep里的所有Watcher执行update方法
update方法内部核心就是执行Vue实例里的render方法
其实Watcher和Dep就是一个非常经典的观察者设计模式的实现
总结
至此一个简单的响应式原理就完成了,总的来说就是Object.defineProperty结合Watcher和Dep的观察者模式的实现