小丸子看vue如何响应式
几个月跟着大佬们投入在vue项目里,对vue算是从0开始啦。
相比较之前使用的regular和angular的响应式的脏检查机制,发现vue的响应式是命中式的。使用angular或regular的时候,发现更新一个值,框架会检查许多无关的值,从中挑出哪个数据脏了,可以更新视图等等。而以vue的computed
为例,发现vue只会检查改变的数据的对应的方法,其他无关的数据相关的方法,并不会打扰。
本文也以vue的computed
计算属性实现举例,揭秘vue的响应式实现。
目录
1. 举个栗子
在使用vue的时候,经常用到computed
来动态获取属性的值。
举个栗子:
<div id="example"> <input v-model="message1"/> <input v-model="message2"/> <p>Computed reversed message: "{{ reversedMessage }}"</p> </div>
var vm = new Vue({
el: '#example',
data: {
message1: 'Hello',
message2: 'world'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
console.log('改啦改啦');
return this.message1.split('').reverse().join('')
}
}
})
上述代码在页面显示了两个输入框,一行展示文字。
左侧输入框双向绑定this.message1
变量,右侧输入框双向绑定this.message2
变量, 下方的展示框双向绑定this.reversedMessage
变量,并且由计算属性获得,定义为this.message1.split('').reverse().join('')
左侧输入框字符串的翻转。
运行发现左边的输入框变化的时候,下面的展示文字立即得到响应,更新为当前左边输入框的文字的翻转,并且控制台打印出改啦改啦
。而右侧的输入框无论怎么变化,下面的展示框和控制台都没有变化。
我们发现,vue的计算属性可以自动地区分出来谁变化的时候,才去更新依赖的值。当这个属性发生变化的时候,函数仿佛可以嗅探到这个变化,并自动重新执行。
2. 原理解析
我们知道Vue的响应式原理是通过Object.defineProperty
中的getter
和setter
方法,在getter
方法中进行依赖收集,在setter
方法中进行响应通知。
我们大胆猜测,作者在computed
的设计中,在计算属性定义之时,对计算属性的数据b
和被依赖的数据a
两者之间关联了响应式的依赖,实现在被依赖项a
的setter
被触发的时候,依赖a
的数据b
的computed
方法被响应通知。
下面通过源码来揭秘吧~
2.1 Mixin
Vue中的特性,通过mixin注入
// ...
initMixin(Vue)
stateMixin(Vue) // 注入Watcher
eventMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
// ...
假设,官方的栗子:
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.getAnswer()
}
}
});
在Vue注入了Watcher之后,才能在vm.$options
的watch
方法调用的时候,具有Watch功能。
在Vue注入了Init之后,Vue具有init功能。blabla
2.2 initData(vm)
现在Vue有了init功能。
官方的Vue生命周期图中,new Vue()
之后,会进行observe data
过程,让我们来看一下,这个阶段做了什么吧。
手画initData过程图
首先,vue把vm.$options.data
转移到vm
下。
然后,对data里的数据及其属性进行响应式转化。
源码如下:
Vue.prototype._initData = function () {
this.$options=options;
// 把options下的data转移到vm下
let data = this._data=this.$options.data;
// 遍历所有的data,添加响应式
Object.keys(data).forEach(key=>this._proxy(key));
// 遍历所有的data及其属性,添加响应式
observe(data,this);
}
// vue的响应式原理,不说了
Vue.prototype._proxy = function (key) {
var self = this;
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key];
},
set: function proxySetter (val) {
self._data[key] = val;
}
})
}
}
// 遍历所有的data及其属性,添加响应式
export function observe (value, vm) {
if (!value || typeof value !== 'object') { return; }
var ob;
if ( hasOwn(value, '__ob__') && value.__ob__ instanceof Observer ) {
ob = value.__ob__ ;
} else if (
shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) {
ob = new Observer(value); // 保证每个属性都有一个__ob__属性,它是一个Objserver对象,也就是这个东西添加的响应式
}
if (ob && vm) {
ob.addVm(vm);
}
return ob;
}
看一下Observer
类的实现
export default class Observer{
constructor(value) {
this.value = value
this.walk(value)
}
//递归。。让每个属性可以observe
walk(value){
Object.keys(value).forEach(key=>this.convert(key,value[key]))
}
convert(key, val){
defineReactive(this.value, key, val)
}
}
// 此函数看不懂Dep.target没关系,后面说,先知道是创建了响应式
export function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set:newVal=> {
var value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify()
}
})
}
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
说白了,给每个属性添加Observer
实例作为属性的目的就是实现给每个属性添加getter
和setter
,以保证响应式。
2.3 Dep: 消息订阅器
之前有没有亲手实践过javascript的消息通知订阅设计模式? Dep
的设计就是一个消息订阅器。
export default class Dep {
constructor() {
this.subs = []
}
// 事件订阅
addSub(sub){
this.subs.push(sub) } // 事件通知 notify(){
this.subs.forEach(sub=>sub.update()) } }
一个简单的事件通知订阅模型。
在看之前的defineReactive
函数:
// 此函数看不懂Dep.target没关系,还是后面说
export function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
if(Dep.target){
dep.addSub(Dep.target) // 订阅
}
return val
},
set:newVal=> {
var value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify() // 发布
}
})
}
手画了一张图:
每一个data及其属性都有一个Observer
,它赋予data响应式,它依赖全局的Dep
消息订阅器,getter
的时候去订阅消息,由Dep
实例存储实例队列,setter
的时候去触发消息,通知队列里的watcher
实例,运行用户想要的事件。
现在实现了每个data及其属性的Observer
属性成功通知和订阅了Dep
实例,那么,Watcher
加入Dep
实例队列具体是怎么做的呢?
2.4 this.$watch
手画this.$watch
过程图(包括上一节的initData过程)
Wacher
类的实现:
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb
this.vm = vm
this.expOrFn = expOrFn
// 2. 关键的地方来啦,此处调用了data/property的get方法
this.value = this.get()
}
update(){
this.run()
}
run(){
const value = this.get()
if(value !==this.value){
this.value = value
this.cb.call(this.vm)
}
}
get(){
// 1. 重点来了: 把全局变量Dep.target只想当前的watcher实例
Dep.target = this
const value = this.vm._data[this.expOrFn]
Dep.target = null
return value
}
}
其实Watcher做了什么呢?
第一步:Dep.target = this
,为了第二步,执行data的get方法时,告诉data,是watcher触发的get方法,不是别人哦。
第二步:this.value = this.get()
,触发data的get方法。
data的get方法做了什么呢?之前贴过代码。
回顾一下:
get: ()=>{
if(Dep.target){
dep.addSub(Dep.target)
}
return val
}
get方法判断Dep.target
有值(即是watcher
触发的),就进行依赖收集。把当前闯进来的watcher
加入自身dep
实例的依赖队列里。
到此实现了vue的响应式过程。把下图三者串联了起来。
3. 小结
结束。
参考:
https://segmentfault.com/a/1190000010408657
https://www.imooc.com/article/14466