项目中使用了vue,一直在比较computed和$watch的使用场景,今天周末抽时间看了下vue中$watch的源码部分,也查阅了一些别人的文章,暂时把本身的笔记记录于此,供之后查阅:vue
实现一个简单的$watch:编程
const v = new Vue({ data:{ a: 1, b: { c: 3 } } }) // 实例方法$watch,监听属性"a" v.$watch("a",()=>console.log("你修改了a")) //当Vue实例上的a变化时$watch的回调 setTimeout(()=>{ v.a = 2 // 设置定时器,修改a },1000)
这个过程大概分为三部分:实例化Vue、调用$watch方法、属性变化,触发回调数组
1、实例化Vue:面向对象的编程函数
class Vue { //Vue对象 constructor (options) { this.$options=options; let data = this._data=this.$options.data; Object.keys(data).forEach(key=>this._proxy(key)); // 拿到data以后,咱们循环data里的全部属性,都传入代理函数中 observe(data,this); } $watch(expOrFn, cb, options){ //监听赋值方法 new Watcher(this, expOrFn, cb); // 传入的是Vue对象 } _proxy(key) { //代理赋值方法 // 当未开启监听的时候,属性的赋值使用的是代理赋值的方法 // 而其主要的做用,是当咱们访问Vue.a的时候,也就是Vue实例的属性时,咱们返回的是Vue.data.a的属性而不是Vue实例上的属性 var self = this Object.defineProperty(self, key, { configurable: true, enumerable: true, get: function proxyGetter () { return self._data[key] // 返回 Vue实例上data的对应属性值 }, set: function proxySetter (val) { self._data[key] = val } }) } }
注意这里的Object.defineProperty ( obj, key , option) 方法this
总共参数有三个,其中option中包括 set(fn), get(fn), enumerable(boolean), configurable(boolean)spa
set会在obj的属性被修改的时候触发,而get是在属性被获取的时候触发,(其实属性的每次赋值,每次取值,都是调用了函数);代理
constructor :Vue实例的构造函数,传入参数(options)的时候,constructor 就会被调用,让Vue对象和参数data产生关联,让咱们能够经过this.a 或者vm.a来访问data属性,创建关联以后,循环data的全部键名,将其传入到_proxy方法code
$watch:实例化Watcher对象server
_proxy:这个方法是一个代理方法,接收一个键名,做用的对象是Vue对象,对象
回头来看Object.defineProperty ( obj, key , option) 这个方法
Object.defineProperty(self, key, { configurable: true, enumerable: true, get: function proxyGetter () { return self._data[key] // 返回 Vue实例上data的对应属性值 }, set: function proxySetter (val) { self._data[key] = val } })
这个方法的第一个Obj的参数传入的是self,也就是Vue实例自己,而get方法里,return出来的倒是self._data[key], _data在上面的方法当中,已经和参数data相等了,因此当咱们访问Vue.a的时候,get方法返回给咱们的,是Vue._data.a。
例如:
var vm = Vue({ data:{ a:1, msg:'我是Vue实例' } }) console.log(vm.msg) //打印 '我是Vue实例' // 理论上来讲,msg和a,应该是data上的属性,可是却能够经过vm.msg直接拿到
当咱们在new Vue的时候,传进去的data极可能包括子对象,例如在使用Vue.data.a = {a1:1 , a2:2 }的时候,这种状况是十分常见的,可是刚才的_proxy函数只是循环遍历了key,若是咱们要给对象的子对象增长set和get方法的时候,最好的方法就是递归;
方法也很简单,若是有属性值 == object,那么久把他的属性值拿出来,遍历一次,若是还有,继续遍历,代码以下:
function defineReactive (obj, key, val) {// 相似_proxy方法,循环增长set和get方法,只不过增长了Dep对象和递归的方法 var dep = new Dep() var childOb = observe(val) //这里的val已是第一次传入的对象所包含的属性或者对象,会在observe进行筛选,决定是否继续递归 Object.defineProperty(obj, key, {//这个defineProperty方法,做用对象是每次递归传入的对象,会在Observer对象中进行分化 enumerable: true, configurable: true, get: ()=>{ if(Dep.target){//这里判断是否开启监听模式(调用watch) dep.addSub(Dep.target)//调用了,则增长一个Watcher对象 } return val//没有启用监听,返回正常应该返回val }, set:newVal=> {var value = val if (newVal === value) {//新值和旧值相同的话,return return } val = newVal childOb = observe(newVal) //这里增长observe方法的缘由是,当咱们给属性赋的值也是对象的时候,一样要递归增长set和get方法 dep.notify() //这个方法是告诉watch,你该行动了 } }) } function observe (value, vm) {//递归控制函数 if (!value || typeof value !== 'object') {//这里判断是否为对象,若是不是对象,说明不须要继续递归 return } return new Observer(value)//递归 }
Opserver对象是使用defineReactive方法循环给参数value设置set和get方法,同时顺便调了observe方法作了一个递归判断,看看是否要从Opserver对象开始再来一遍。
Dep起到链接的做用:
class Dep { constructor() { this.subs = [] //Watcher队列数组 } addSub(sub){ this.subs.push(sub) //增长一个Watcher } notify(){ this.subs.forEach(sub=>sub.update()) //触发Watcher身上的update回调(也就是你传进来的回调) } } Dep.target = null //增长一个空的target,用来存放Watcher
new Watcher:
class Watcher { // 当使用了$watch 方法以后,无论有没有监听,或者触发监听,都会执行如下方法 constructor(vm, expOrFn, cb) { this.cb = cb //调用$watch时候传进来的回调 this.vm = vm this.expOrFn = expOrFn //这里的expOrFn是你要监听的属性或方法也就是$watch方法的第一个参数(为了简单起见,咱们这里补考录方法,只考虑单个属性的监听) this.value = this.get()//调用本身的get方法,并拿到返回值 } update(){ // 还记得Dep.notify方法里循环的update么? this.run() } run(){//这个方法并非实例化Watcher的时候执行的,而是监听的变量变化的时候才执行的 const value = this.get() if(value !==this.value){ this.value = value this.cb.call(this.vm)//触发你穿进来的回调函数,call的做用,我就不说了 } }22 get(){ //向Dep.target 赋值为 Watcher Dep.target = this //将Dep身上的target 赋值为Watcher对象 const value = this.vm._data[this.expOrFn];//这里拿到你要监听的值,在变化以前的数值 // 声明value,使用this.vm._data进行赋值,而且触发_data[a]的get事件 Dep.target = null return value } }
class Watcher在实例化的时候,重点在于get方法,咱们来分析一下,get方法首先把Watcher对象赋值给Dep.target,随后又有一个赋值,const value = this.vm._data[this.exOrFn],以前所作的就是修改了Vue对象的data(_data)的全部属性的get和set?,而Vue对象也做为第一个参数,传给了Watcher对象,这个this.vm._data里的全部属性,在取值的时候,都会触发以前defineReactive 方法.
回过头来再看看get:
function defineReactive (obj, key, val) { /*.......*/ Object.defineProperty(obj, key, { /*.......*/ get: ()=>{ if(Dep.target){ //触发这个get事件以前,咱们刚刚对Dep.target赋值为Watcher对象 dep.addSub(Dep.target)//这里会把咱们刚赋值的Dep.target(也就是Watcher对象)添加到监听队列里 } return val }, /*.......*/ } }
在吧Watcher对象放再Dep.subs数组中以后,new Watcher对象所执行的任务就告一段落,此时咱们有:
1.Dep.subs数组中,已经添加了一个Watcher对象,
2.Dep对象身上有notify方法,来触发subs队列中的Watcher的update方法,
3.Watcher对象身上有update方法能够调用run方法能够触发最终咱们传进去的回调
那么如何触发Dep.notify方法,来层层回调,找到Watcher的run呢?
set:newVal=> { var value = val if (newVal === value) { return } val = newVal childOb = observe(newVal) dep.notify()//触发Dep.subs中全部Watcher.update方法 }
这里造成了一个回路,当修改了所监听的那个值的时候,这个set方法被触发。