vue中$watch源码阅读笔记

项目中使用了vue,一直在比较computed和$watch的使用场景,今天周末抽时间看了下vue中$watch的源码部分,也查阅了一些别人的文章,暂时把自己的笔记记录于此,供以后查阅:

实现一个简单的$watch:

 1 const v = new Vue({
 2 data:{
 3   a: 1,
 4   b: {
 5     c: 3
 6   }
 7 }
 8 })
 9 // 实例方法$watch,监听属性"a"
10 v.$watch("a",()=>console.log("你修改了a"))
11             //当Vue实例上的a变化时$watch的回调
12 setTimeout(()=>{
13 v.a = 2
14 // 设置定时器,修改a
15 },1000)

这个过程大概分为三部分:实例化Vue、调用$watch方法、属性变化,触发回调

一、实例化Vue:面向对象的编程

 1 class Vue { //Vue对象
 2     constructor (options) {
 3       this.$options=options;
 4       let data = this._data=this.$options.data;
 5       Object.keys(data).forEach(key=>this._proxy(key));
 6       // 拿到data之后,我们循环data里的所有属性,都传入代理函数中
 7       observe(data,this);
 8     }
 9     $watch(expOrFn, cb, options){  //监听赋值方法
10       new Watcher(this, expOrFn, cb);
11       // 传入的是Vue对象
12     }
13
14     _proxy(key) { //代理赋值方法
15       // 当未开启监听的时候,属性的赋值使用的是代理赋值的方法
16       // 而其主要的作用,是当我们访问Vue.a的时候,也就是Vue实例的属性时,我们返回的是Vue.data.a的属性而不是Vue实例上的属性
17       var self = this
18       Object.defineProperty(self, key, {
19         configurable: true,
20         enumerable: true,
21         get: function proxyGetter () {
22           return self._data[key]
23           // 返回 Vue实例上data的对应属性值
24         },
25         set: function proxySetter (val) {
26           self._data[key] = val
27         }
28       })
29     }
30   }

注意这里的Object.defineProperty ( obj, key , option) 方法

总共参数有三个,其中option中包括  set(fn), get(fn), enumerable(boolean), configurable(boolean)

set会在obj的属性被修改的时候触发,而get是在属性被获取的时候触发,(其实属性的每次赋值,每次取值,都是调用了函数);

constructor :Vue实例的构造函数,传入参数(options)的时候,constructor 就会被调用,让Vue对象和参数data产生关联,让我们可以通过this.a 或者vm.a来访问data属性,建立关联之后,循环data的所有键名,将其传入到_proxy方法

$watch:实例化Watcher对象

_proxy:这个方法是一个代理方法,接收一个键名,作用的对象是Vue对象,

回头来看Object.defineProperty ( obj, key , option) 这个方法

 1 Object.defineProperty(self, key, {
 2         configurable: true,
 3         enumerable: true,
 4         get: function proxyGetter () {
 5           return self._data[key]
 6           // 返回 Vue实例上data的对应属性值
 7         },
 8         set: function proxySetter (val) {
 9           self._data[key] = val
10         }
11       })

这个方法的第一个Obj的参数传入的是self,也就是Vue实例本身,而get方法里,return出来的却是self._data[key], _data在上面的方法当中,已经和参数data相等了,所以当我们访问Vue.a的时候,get方法返回给我们的,是Vue._data.a。

例如:

1 var vm = Vue({
2    data:{
3        a:1,
4        msg:‘我是Vue实例‘
5      }
6 })
7 console.log(vm.msg) //打印 ‘我是Vue实例‘
8 // 理论上来说,msg和a,应该是data上的属性,但是却可以通过vm.msg直接拿到

当我们在new Vue的时候,传进去的data很可能包括子对象,例如在使用Vue.data.a = {a1:1 , a2:2 }的时候,这种情况是十分常见的,但是刚才的_proxy函数只是循环遍历了key,如果我们要给对象的子对象增加set和get方法的时候,最好的方法就是递归;

  方法也很简单,如果有属性值 == object,那么久把他的属性值拿出来,遍历一次,如果还有,继续遍历,代码如下:

 1 function defineReactive (obj, key, val) {//  类似_proxy方法,循环增加set和get方法,只不过增加了Dep对象和递归的方法
 2    var dep = new Dep()
 3     var childOb = observe(val)
 4     //这里的val已经是第一次传入的对象所包含的属性或者对象,会在observe进行筛选,决定是否继续递归
 5     Object.defineProperty(obj, key, {//这个defineProperty方法,作用对象是每次递归传入的对象,会在Observer对象中进行分化
 6    enumerable: true,
 7    configurable: true,
 8    get: ()=>{
 9         if(Dep.target){//这里判断是否开启监听模式(调用watch)
10           dep.addSub(Dep.target)//调用了,则增加一个Watcher对象
11         }
12         return val//没有启用监听,返回正常应该返回val
13       },
14    set:newVal=> {var value =  val
15         if (newVal === value) {//新值和旧值相同的话,return
16           return
17         }
18         val = newVal
19         childOb = observe(newVal)
20               //这里增加observe方法的原因是,当我们给属性赋的值也是对象的时候,同样要递归增加set和get方法
21         dep.notify()
22               //这个方法是告诉watch,你该行动了
23    }
24  })
25 }
26 function observe (value, vm) {//递归控制函数
27     if (!value || typeof value !== ‘object‘) {//这里判断是否为对象,如果不是对象,说明不需要继续递归
28       return
29     }
30   return new Observer(value)//递归
31 }

Opserver对象是使用defineReactive方法循环给参数value设置set和get方法,同时顺便调了observe方法做了一个递归判断,看看是否要从Opserver对象开始再来一遍。

Dep起到连接的作用:

 1 class Dep {
 2     constructor() {
 3      this.subs = []  //Watcher队列数组
 4     }
 5     addSub(sub){
 6       this.subs.push(sub) //增加一个Watcher
 7     }
 8    notify(){
 9       this.subs.forEach(sub=>sub.update()) //触发Watcher身上的update回调(也就是你传进来的回调)
10     }
11 }
12 Dep.target = null //增加一个空的target,用来存放Watcher

new Watcher:

 1 class Watcher { // 当使用了$watch 方法之后,不管有没有监听,或者触发监听,都会执行以下方法
 2    constructor(vm, expOrFn, cb) {
 3      this.cb = cb  //调用$watch时候传进来的回调
 4      this.vm = vm
 5      this.expOrFn = expOrFn //这里的expOrFn是你要监听的属性或方法也就是$watch方法的第一个参数(为了简单起见,我们这里补考录方法,只考虑单个属性的监听)
 6      this.value = this.get()//调用自己的get方法,并拿到返回值
 7    }
 8    update(){  // 还记得Dep.notify方法里循环的update么?
 9      this.run()
10    }
11    run(){//这个方法并不是实例化Watcher的时候执行的,而是监听的变量变化的时候才执行的
12      const  value = this.get()
13      if(value !==this.value){
14        this.value = value
15        this.cb.call(this.vm)//触发你穿进来的回调函数,call的作用,我就不说了
16      }
17    }22             get(){ //向Dep.target 赋值为 Watcher
18      Dep.target = this  //将Dep身上的target 赋值为Watcher对象
19      const value = this.vm._data[this.expOrFn];//这里拿到你要监听的值,在变化之前的数值
20      // 声明value,使用this.vm._data进行赋值,并且触发_data[a]的get事件
21      Dep.target = null
22      return value
23    }
24  }

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:

 1 function defineReactive (obj, key, val) {
 2     /*.......*/
 3     Object.defineProperty(obj, key, {
 4   /*.......*/
 5   get: ()=>{
 6     if(Dep.target){ //触发这个get事件之前,我们刚刚对Dep.target赋值为Watcher对象
 7       dep.addSub(Dep.target)//这里会把我们刚赋值的Dep.target(也就是Watcher对象)添加到监听队列里
 8     }
 9     return val
10   },
11   /*.......*/
12  }
13 }

在吧Watcher对象放再Dep.subs数组中之后,new Watcher对象所执行的任务就告一段落,此时我们有:

  1.Dep.subs数组中,已经添加了一个Watcher对象,

  2.Dep对象身上有notify方法,来触发subs队列中的Watcher的update方法,

  3.Watcher对象身上有update方法可以调用run方法可以触发最终我们传进去的回调

那么如何触发Dep.notify方法,来层层回调,找到Watcher的run呢?

1 set:newVal=> {
2  var value =  val
3  if (newVal === value) {
4    return
5  }
6  val = newVal
7  childOb = observe(newVal)
8  dep.notify()//触发Dep.subs中所有Watcher.update方法
9 }

时间: 2024-10-08 09:51:30

vue中$watch源码阅读笔记的相关文章

CI框架源码阅读笔记3 全局函数Common.php

从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作). 打开Common.php中,第一行代码就非常诡异: if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 上一篇(CI框架源码阅读笔记2 一切的入口 index

源码阅读笔记 - 1 MSVC2015中的std::sort

大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格式化,去掉或者展开用于条件编译或者debug检查的宏,依重要程度重新排序函数,但是不会改变命名方式(虽然MSVC的STL命名实在是我不能接受的那种),对于代码块的解释会在代码块前(上面)用注释标明. template<class _RanIt, class _Diff, class _Pr> in

CI框架源码阅读笔记5 基准测试 BenchMark.php

上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功能,各模块之间可以相互调用,共同构成了CI的核心骨架. 从本篇开始,将进一步去分析各组件的实现细节,深入CI核心的黑盒内部(研究之后,其实就应该是白盒了,仅仅对于应用来说,它应该算是黑盒),从而更好的去认识.把握这个框架. 按照惯例,在开始之前,我们贴上CI中不完全的核心组件图: 由于BenchMa

CI框架源码阅读笔记2 一切的入口 index.php

上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里这次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中,我们并不会逐行进行解释,而只解释核心的功能和实现. 1.       设置应用程序环境 define('ENVIRONMENT', 'development'); 这里的development可以是任何你喜欢的环境名称(比如dev,再如test),相对应的,你要在下面的switch case代码块中

Apache Storm源码阅读笔记

欢迎转载,转载请注明出处. 楔子 自从建了Spark交流的QQ群之后,热情加入的同学不少,大家不仅对Spark很热衷对于Storm也是充满好奇.大家都提到一个问题就是有关storm内部实现机理的资料比较少,理解起来非常费劲. 尽管自己也陆续对storm的源码走读发表了一些博文,当时写的时候比较匆忙,有时候衔接的不是太好,此番做了一些整理,主要是针对TridentTopology部分,修改过的内容采用pdf格式发布,方便打印. 文章中有些内容的理解得益于徐明明和fxjwind两位的指点,非常感谢.

CI框架源码阅读笔记4 引导文件CodeIgniter.php

到了这里,终于进入CI框架的核心了.既然是"引导"文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.com/usr/reg 经过引导文件,实际上会交给Application中的UsrController控制器的reg方法去处理. 这之中,CodeIgniter.php做了哪些工作?我们一步步来看. 1.    导入预定义常量.框架环境初始化 之前的一篇博客(CI框架源码阅读笔记2 一切的入

IOS测试框架之:athrun的InstrumentDriver源码阅读笔记

athrun的InstrumentDriver源码阅读笔记 作者:唯一 athrun是淘宝的开源测试项目,InstrumentDriver是ios端的实现,之前在公司项目中用过这个框架,没有深入了解,现在回来记录下. 官方介绍:http://code.taobao.org/p/athrun/wiki/instrumentDriver/ 优点:这个框架是对UIAutomation的java实现,在代码提示.用例维护方面比UIAutomation强多了,借junit4的光,我们可以通过junit4的

jdk源码阅读笔记之java集合框架(二)(ArrayList)

关于ArrayList的分析,会从且仅从其添加(add)与删除(remove)方法入手. ArrayList类定义: p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Monaco } span.s1 { color: #931a68 } public class ArrayList<E> extends AbstractList<E> implements List<E> ArrayList基本属性: /** *

Yii源码阅读笔记 - 日志组件

?使用 Yii框架为开发者提供两个静态方法进行日志记录: Yii::log($message, $level, $category);Yii::trace($message, $category); 两者的区别在于后者依赖于应用开启调试模式,即定义常量YII_DEBUG: defined('YII_DEBUG') or define('YII_DEBUG', true); Yii::log方法的调用需要指定message的level和category.category是格式为“xxx.yyy.z