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

实现一个简单的$watch:

const v = new Vue({
data:{
a: ,
b: {
c:
}
}
})
// 实例方法$watch,监听属性"a"
v.$watch("a",()=>console.log("你修改了a"))
//当Vue实例上的a变化时$watch的回调
setTimeout(()=>{
v.a =
// 设置定时器,修改a
},)

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

一、实例化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) 方法

总共参数有三个,其中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) 这个方法

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:,
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的作用,我就不说了
}
} 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方法被触发。

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

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

    上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功 ...

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

    到了这里,终于进入CI框架的核心了.既然是“引导”文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.c ...

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

    从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap ...

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

    上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里再次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中, ...

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

    大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格 ...

  6. Three.js源码阅读笔记-5

    Core::Ray 该类用来表示空间中的“射线”,主要用来进行碰撞检测. THREE.Ray = function ( origin, direction ) { this.origin = ( or ...

  7. PHP源码阅读笔记一(explode和implode函数分析)

    PHP源码阅读笔记一一.explode和implode函数array explode ( string separator, string string [, int limit] )此函数返回由字符 ...

  8. AQS源码阅读笔记(一)

    AQS源码阅读笔记 先看下这个类张非常重要的一个静态内部类Node.如下: static final class Node { //表示当前节点以共享模式等待锁 static final Node S ...

  9. libevent源码阅读笔记(一):libevent对epoll的封装

    title: libevent源码阅读笔记(一):libevent对epoll的封装 最近开始阅读网络库libevent的源码,阅读源码之前,大致看了张亮写的几篇博文(libevent源码深度剖析 h ...

随机推荐

  1. SQL LEAD()函数 LAG()函数

    lag ,lead 分别是向前,向后:lag 和lead 有三个参数,第一个参数是列名,第二个参数是偏移的offset,第三个参数是 超出记录窗口时的默认值) SQL> select id,na ...

  2. 解决IE8不支持console

    解决IE8不支持console,代码中包含console时会报错. //解决 IE8 不支持console window.console = window.console || (function ( ...

  3. css细节复习笔记——结构与层叠

    每个合法的文档都会生成一个结构树,有了结构树元素的祖先.属性兄弟元素等等创建选择器来选择元素,这是CSS继承的核心.继承是从一个元素向后代元素传递属性值所采用的机制.面向一个元素使用哪些值时,用户代理 ...

  4. 自注意力机制(Self-attention Mechanism)——自然语言处理(NLP)

    近年来,注意力(Attention)机制被广泛应用到基于深度学习的自然语言处理(NLP)各个任务中.随着注意力机制的深入研究,各式各样的attention被研究者们提出.在2017年6月google机 ...

  5. robot framework学习笔记之三—Scalar变量

    一.变量赋值 1)Set赋值 通常使用Set Variable关键字对变量进行赋值,其他Set相关的带Variable的关键字也可以进行赋值 赋值的时候,变量后面写不写『=』都可以,如下: 如果${v ...

  6. 序列化库MessagePack应用(C++)

    介绍 MessagePack(简称msgpack)是一个小巧而高效的序列化/反序列化库,支持多种开发语言.官方网站:http://msgpack.org/ . 下面是官方的一个简介: It's lik ...

  7. Mysql6.0连接中的几个问题 Mysql6.xx

    Mysql6.0连接中的几个问题 在最近做一些Javaweb整合时,因为我在maven官网查找的资源,使用的最新版,6.0.3,发现MySQL连接中的几个问题,总结如下: 1.Loading clas ...

  8. 编译原理(六)自底向上分析之LR分析法

    自底向上分析之LR分析法 说明:以老师PPT为标准,借鉴部分教材内容,AlvinZH学习笔记. 基本概念 1. LR分析:从左到右扫描(L)自底向上进行规约(R),是规范规约,也即最右推导(规范推导) ...

  9. php 判断两个时间段是否有交集

    一开始,没啥思路,全靠百度,记录一下哈 public function demo(){ //例子 $astart = strtotime("1995-06-16 12:00:00" ...

  10. P2486 [SDOI2011]染色(树剖)区间覆盖+区间的连续段

    https://www.luogu.org/problemnew/show/P2486 值的一看https://www.cnblogs.com/Tony-Double-Sky/p/9283262.ht ...