一、响应式原理
什么是响应式原理?
意思就是在改变数据的时候,视图也跟着更新。这意味你只需要进行数据的管理。
Vue则是利用Object.defineProperty的方法里面的setter和getter方法的观察者模式来实现。所以在学习Vue的响应式原理之前,先学习两个预备知识:Object.defineProperty和观察者模式
二、预备知识
Object.defineProperty
这个方法就是一个对象上定义一个新的属性,或者改变一个对象现有的属性,并且返回这个对象。里面有两个字段get、set。顾名思义set就是设置属性的值,get就是获取属性的值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var bvalue;
var o ={};
Object.defineProperty(o,'b',{
get:function(){
console.log('监听设置的值')
return bvalue
},
set:function(newvalue){
console.log('监听设置的值')
bvalue = newvalue
},
enumerable:true,
configurable:true
})
o.b=38;
console.log(o.b)
// 监听设置的值
// 监听获取的值
// 38
</script>
</body>
</html>
可以实现一下简单的数据双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<input type="text" id="txt">
<span id="sp"></span>
</div>
<script>
var txt = document.getElementById('txt'),sp = document.getElementById('sp'),
data = {}
Object.defineProperty(data,'msg',{
// 这里实现的是数据变化引起内容的变化
set:function(newvalue){
txt.value = newvalue
sp.innerHTML = newvalue
}
})
// 这里是监听文本框中的变化,当文本框中的内容发生变化的时候,改变data中的数据
txt.addEventListener('keyup',function(e){
data.msg = e.target.value
})
</script>
</body>
</html>
Vue给data里的所有的属性加上get和set的这个过程叫做Reactive化。
观察者模式
之前已经说过了,观察者模式就是一对多的关系,一个Subject发生变化,依赖它的那些Obeserver就会被通知然后自动更新。 实现一个简单的观察者模式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 抽像Subject
function Subject(){
this.dep = [],
//用来添加观察者,(也可以说是添加客户)
// 因为我这里用的是箭头函数,本身没有this,所以前面的加上this
// 让他指向实例对象
this.register=(fn)=>{
this.dep.push(fn)
}
// 发布消息通知观察者(通知客户)
this.notify=()=>{
this.dep.forEach(item=>item())
}
}
const cheese = new Subject()
// 添加观察者
cheese.register(()=>{console.log('call daisy')})
cheese.register(()=>{console.log('call tom')})
cheese.register(()=>{console.log('call tomas')})
// 发布消息(通知客户)
cheese.notify()
</script>
</body>
</html>
三、原理分析
上图是官网的一张表示这个过程的图。
总共分了三个步骤:
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就是一个Subject类,每一个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) // 调用组件的更新函数
...
}
}
在mount的时候会创建一个新的watcher类,这个watcher其实是链接Vue和dep的桥梁,每一个vue component对应一个watcher(监查者)。 这里可以看出new一个watcher的时候,watcher里面的this.getter.call(vm,vm)函数会被执行。getter就是updateComponent。这个函数就会调用组件的render函数来更新重新渲染。 而render函数,会访问data的属性,比如
render:function(createElement){
return createElement('h1',this.blogTitle)
}
此时会调用这个属性blogTitle的getter属性,也就是:
get: function reactiveGetter () {
....
dep.depend()
return value
....
},
// dep的depend函数
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
在depend的代码里面,dep.target就是watcher,这里做的事情就是给blogTitle这个组件注册了watcher这个对象。每次render一个组件的时候,如果这个组件用到了blogTitle属性,那么这个组件相对应watcher对象都会被注册到blogTitle的Dep中。这个过程就叫做依赖收集。
收集完所有依赖blogTitle的watcher属性的组件之后,当blogTitle发生改变的时候会notify通知所有Watcher更新关联的组件。
3、更新阶段
当blogTitle发生改变的时候,就去调用Dep的notify函数,通知所有的watcher调用updata函数更新。
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
总结:
1、第一步:初始化组件的时候,先是给每一个data的属性都注册getter和setter,也就是reactive化。然后再new一个自己的watcher,watcher会立刻调用render函数去生成虚拟DOM。在调用render的时候就会用到data的属性值,此时触发属性值的getter函数,然后将watcher注册进属性值对应的sub中。
2、第二步:当data发生变化的时候,就会调用属性的setter方法,然后触发dep.notify,通知所有的watcher去调用updata函数更新