【手把手教你搓Vue响应式原理】(五) Watcher 与 Dep

大家好,我是 辉夜真是太可爱啦 。这是我最近在写的【手把手教你搓Vue响应式原理】系列,本文将一步步地为你解开vue响应式原理的面纱。由于本人也是在写这篇文章的过程中不断试错,不断学习改进的,所以,本文同样很适合和我一样的初学者。和 Vue 的设计理念如出一辙,那就是渐进增强

上文链接

【手把手教你搓Vue响应式原理】(一)初识Vue响应式

【手把手教你搓Vue响应式原理】(二)深度监测对象全部属性

【手把手教你搓Vue响应式原理】(三)observe 以及 ob

【手把手教你搓Vue响应式原理】(四) 数组的响应式处理

前言

之前已经将数据劫持已经全部完成了。

那么,接下来,主要的要点就是在于两点,依赖收集和触发依赖更新。

它的意义主要在于控制哪些地方使用了这个变量,然后,按照最小的开销来更新视图

首先,要先明白,依赖是什么,比方说在我们的模板中有 {{a}} ,那么,这个地方就有对于变量 a 的依赖。

在模板编译的时候,就会触发 a 变量的 getter

然后,当我们执行 a++; 的时候,那么,我们就要触发依赖的更新,当初模板中 {{a}} 的地方,就要更新,是吧!

所以,我们都是getter 中收集依赖,在 setter 中触发依赖更新

这一节的内容,主要就是用来专门讲清楚这两件事情。

依赖收集和派发更新

依赖收集和触发依赖更新主要由两个类来完成, DepWatcher

image.png

DepWatcher 在设计模式中,就是发布-订阅者的模式。

而依赖,你可以理解为所谓的订阅者。

  • Dep

Dep 说白了就是发布者,它的工作就是依赖管理,要知道哪些地方用到了这个变量,可能用到这个变量的地方有很多,所以,它会有多个订阅者。

然后,每个变量都应该有属于自己的 Dep ,因为每个变量所在的依赖位置是不一样的,所以他们的订阅者也不一样。

然后在变量更新之后,就去通知所有的订阅者(Watcher),我的变量更新了,你们该触发视图更新了。

  • Watcher

Watcher 说白了就是订阅者,它接受 Dep 发过来的更新通知之后,就去执行视图更新了。

它其实就是所谓的 watch 监听器,变量改变之后,执行一个回调函数。

Dep

初始化我们的 Dep 类

我们先按照图例来创建我们的 Dep

根据我们的需求:

  1. 首先,它要在初始化的时候,新建一个 subs 数组,用来存储依赖,也就是 Watcher 的实例
class Dep{
  constructor() {
    // 用数组存储自己的订阅者   subs 是 subscribes 订阅者的意思
    // 这个数组里放的是 Watcher 的实例
    this.subs=[]
  }
}
  1. 它需要有一个 depend() 方法,用于添加依赖,也就是将 Watcher 实例往 subs 数组中 push
class Dep{
  // 添加依赖
  depend(){
    // 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
    if(Dep.target){
      // 将监听的目标推进 subs 数组
      this.subs.push(Dep.target);
    }
  }
}
  1. 它需要有一个 notify() 方法,用于通知 Wacher 数据更新了,调用 Wacher 的 update() 方法
class Dep{
  // 通知所有订阅者
  notify(){
    // 浅克隆一份
    const subs=this.subs.slice();
    // 遍历
    for(let i=0,l=subs.length;i<l;i++){
      // 逐个更新
      subs[i].update();
    }
  }
}

使用我们的 Dep 类

  1. 每个属性都要有自己的 Dep

Dep 我们在前面也说了,每个属性都应该有它自己的 Dep ,用来管理依赖。

所以,首先,如果我们在 Observer 中创建 Dep,那不就可以了。毕竟 Observer 会遍历到每一个对象。

class Observer{
  constructor(obj){
    this.dep=new Dep();
    // ...
  }
}
  1. 在 getter 中收集依赖

所以,很明显,我们可以在 defineReactive 的 get 中收集依赖

因为有了 if(Dep.target) 的判断,所以,只有绑定 Watcher 的变量触发 getter 时,才会添加依赖

function defineReactive(obj,key,val) {
  let dep=new Dep();
  let childOb;
  // 判断当前入参个数,两个的话直接返回当前层的对象
  if(arguments.length===2){
    val=obj[key];
    childOb = observe(val)
  }
  Object.defineProperty(obj,key,{
    get(){
      // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
      if(Dep.target){
        // 添加依赖
        dep.depend();
        // 如果有子属性,也要将它加入依赖
        if(childOb){
          // 给子属性添加依赖
          childOb.dep.depend();
        }
      }
      return val;
    },
  })
}

这个 Dep.target 其实就是 Watcher 的实例

image.png
  1. 在 setter 中触发依赖更新

所以,很明显,我们可以在 defineReactive 的 set 中收调用 notify() 方法告知 Watcher 实例,数据更新了。

function defineReactive(obj,key,val) {
  let dep=new Dep();
  let childOb;
  // 判断当前入参个数,两个的话直接返回当前层的对象
  if(arguments.length===2){
    val=obj[key];
    childOb = observe(val)
  }
  Object.defineProperty(obj,key,{
    // ...
    set(newValue){
      val=newValue;
      childOb = observe(val)
      // notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
      dep.notify();
    }
  })
}

至此, Dep 的所有职责,我们已经帮它完成了。

其实照道理应该有一个删除依赖,我们这里就不再扩展了。

Watcher

初始化我们的 Watcher 类

首先, Watcher 实例应该大家会相对而言更加好理解点,因为,我们有一个 watch 侦听器,大家一定都很熟悉,这两个其实一样。

我们先按照图例来创建我们的 Watcher

根据我们的需求:

  1. 首先,它要在初始化的时候,需要传入目标对象 target , 属性名 expression , 回调函数 callback
class Watcher{
  // target 目标对象
  // expression 属性名
  // callback 回调函数
  // value 属性的值
  constructor(target,expression,callback) {
    this.target=target;
    // parsePath 为一个高阶函数
    this.getter=parsePath(expression);
    this.callback=callback;
    // get为我们之后要写的获取值的方法
    this.value=this.get();
  }
}

这个 parsePath 需要单独拎出来说一下,比方说我们现在有这么一个对象

let a={
  b:{
    c:{
      d:10
    }
  }
}

我们要监听到 a.b.c.d ,所以,我们需要下面的这种格式

new Watcher(a,'b.c.d',val=>{
  console.log('ok啦',val);
})

所以,这个 get 很明显就有点难度了。 我们需要通过循环 拿到 a.b 然后 .c 然后 .d。

我们将这个方法命名为 parsePath

function parsePath(str){
  let segments = str.split('.');
  return obj=> {
      for(let i=0;i<segments.length;i++){
        if(!obj) return;
        obj=obj[segments[i]];
      }
      return obj;
  }
}

入参接受我们的 b.c.d ,我们可以看到 第一句执行之后 segments=['b','c','d'] ,然后进行第二层,这是返回了一个方法,按照循环,那就是 obj=obj.b => obj=obj.c => obj=obj.d ,所以,就是返回一个对象的 obj.b.c.d,相当于是遍历字符串中的属性树。

  1. 它需要有一个 get() 方法,用于获取当前的值,并将它更新,然后 return 返回
class Watcher{
  // 获取当前的值,并将它更新,然后 return 返回
  get(){
    // 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
    Dep.target=this;

    // 当前对象
    const obj=this.target;
    let value;
    // 当对象不再使用的时候,我们需要将它清空
    try{
      value=this.getter(obj)
    }finally {
      Dep.target=null;
    }
    this.value=value
    return value;
  }
}
  1. 它需要有一个 update() 方法,用于执行数据触发更新之后,保存新的值和旧的值,将它返回给 callback 回调函数
class Watcher{
  // Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
  update(){
    // this.value 由于还没触发更新,所以此时是旧的值
    const oldValue=this.value;
    // 通过我们的 getter 方法,直接获取最新的值
    const newValue=this.get();
    // 将新值和旧值返回给 callback 回调函数
    this.callback(newValue,oldValue);
  }
}

使用案例

let a={
  b:{
    c:{
      d:10
    }
  }
}

observe(a);
new Watcher(a,'b.c.d',(val,oldValue)=>{
  console.log('ok',val,oldValue);  
})
a.b.c.d=55;  // ok 55 10

在执行 a.b.c.d=55; 的同时,我们的控制台就会输出 ok 55 10 。

运行分析

observe(a)

  1. 首先, observe(a) 会将 a 对象变为响应式对象

new Watcher

  1. 执行 new Watcher 之后,就会调用 Watcher 类的 constructor 。此时 target 是 a , expression 是 'b.c.d', callback(val,oldValue)=>{console.log('ok',val,oldValue); })
// target 目标对象
// expression 属性名
// callback 回调函数
// value 属性的值
constructor(target,expression,callback) {
  this.target=target;
  // parsePath 为一个高阶函数
  this.getter=parsePath(expression);
  this.callback=callback;
  // get为我们之后要写的获取值的方法
  this.value=this.get();
}
  1. this.value=this.get() 又会执行 get() 方法, 此时 Dep.target 被赋值了,就是当前 Watcher 实例。
get(){
  // 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
  Dep.target=this;

  // 当前对象
  const obj=this.target;
  let value;
  // 当对象不再使用的时候,我们需要将它清空
  try{
    value=this.getter(obj)
  }finally {
    Dep.target=null;
  }
  this.value=value
  return value;
}
  1. value=this.getter(obj) 会触发 defineReactive 中的 get() , 因为 Dep.target 之前已经被赋值了,所以,现在有值,触发 dep.depend
get(){
  // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
  if(Dep.target){
    dep.depend();   // b 在这里触发
    // 如果有子属性,也要将它加入依赖
    if(childOb){
      childOb.dep.depend();  // c d 在这里触发
    }
  }
  return val;
},
  1. 将当前 Watcher 实例推进了 subs 数组中。
// 添加依赖
depend(){
  // 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
  if(Dep.target){
    // 将 Watcher 实例添加进 subs
    this.subs.push(Dep.target)
  }
}

a.b.c.d=55;

  1. 执行代码 a.b.c.d 触发 defineReactive 中的 set 方法,然后执行 dep.notify();
set(newValue){
  val=newValue;
  childOb = observe(val)
  // notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
  dep.notify();
}
  1. 通过遍历 subs 列表,通知所有订阅者
// 通知所有订阅者
notify(){
  // 浅克隆一份
  const subs=this.subs.slice();
  // 遍历
  for(let i=0,l=subs.length;i<l;i++){
    // 逐个更新
    subs[i].update();
  }
}
  1. 相应的订阅者执行 update() ,将新值和旧值获取,然后通过 callback 回调函数返回
// Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
update(){
  // this.value 由于还没触发更新,所以此时是旧的值
  const oldValue=this.value;
  // 通过我们的 getter 方法,直接获取最新的值
  const newValue=this.get();
  // 将新值和旧值返回给 callback 回调函数
  this.callback(newValue,oldValue);
}
  1. 最终 new Watcher 实例中的回调函数成功执行,并且成功拿到 valoldValue
new Watcher(a,'b.c.d',(val,oldValue)=>{
  console.log('ok',val,oldValue);  // ok 10 5
})

所有代码

// 拷贝一份数组的原型
const arrayPrototype=Array.prototype;
// 以 Array.prototype 为原型创建 arrayMethods 对象
const arrayMethods=Object.create(arrayPrototype);

// 需要改写的数组方法列表
const methodsNeedChange=[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

for(let i=0;i<methodsNeedChange.length;i++){
  // 备份原来的方法
  const original=arrayMethods[methodsNeedChange[i]];
  // 定义新的方法
  def(arrayMethods,methodsNeedChange[i],function () {
    // 用来保存新插入的值
    let inserted=[];
    // 由于 arguments 对象是类数组,所以先通过扩展运算符转为数组之后,再进行操作。
    let args=[...arguments];
    // 先判断 是否是 push shift splice ,如果是的话,先取出插入的新值,后面进行 observeArray
    switch (methodsNeedChange[i]) {
      case 'push':
      case 'shift':
        inserted=args;
        break;
      case ' ':
        // splice(起始下标,删除个数,新添加的元素)
        inserted=args.slice(2);
    }
    // 先判断 inserted 里面有东西,才执行 observeArray
    inserted.length && observeArray(inserted);
    // 将备份的方法进行执行,毕竟不能丢失数组方法原本的功能执行
    original.apply(this,arguments)
    // 写监听到之后更新视图
  },false)
}

function defineReactive(obj,key,val) {
  let dep=new Dep();
  // eslint-disable-next-line no-unused-vars
  let childOb;
  // 判断当前入参个数,两个的话直接返回当前层的对象
  if(arguments.length===2){
    val=obj[key];
    childOb = observe(val)
  }
  Object.defineProperty(obj,key,{
    // 可枚举,默认为 false
    enumerable:true,
    // 属性的描述符能够被改变,或者是删除,默认为 false
    configurable:true,
    get(){
      // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
      if(Dep.target){
        dep.depend();
        // 如果有子属性,也要将它加入依赖
        if(childOb){
          childOb.dep.depend();
        }
      }
      return val;
    },
    set(newValue){
      val=newValue;
      childOb = observe(val)
      // notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
      dep.notify();
    }
  })
}

function def(obj,key,value,enumerable) {
  Object.defineProperty(obj,key,{
    value,
    //这个属性仅仅保存 Observer 实例,所以不需要遍历
    enumerable
  })
}

// 遍历对象当前层的所有属性,并且绑定 defineReactive
class Observer{
  constructor(obj){
    this.dep=new Dep();
    def(obj,'__ob__',this,false)
    if (Array.isArray(obj)){
      // 遍历当前数组,给所有的元素绑定 observe 响应式
      observeArray(obj)
      // 将当前数组对象的原型链强行指向 arrayMethods
      Object.setPrototypeOf(obj,arrayMethods);
    }else{
      this.walk(obj);
    }
  }
  // 遍历对象的当前层的所有属性, 给他绑定 defineReactive 响应式
  walk(obj){
    let keys=Object.keys(obj);
    for(let i =0;i<keys.length;i++){
      defineReactive(obj,keys[i])
    }
  }
}

// 响应式的入口方法 ,主要用于先判断是否是对象 ,然后判断是否有 __ob__ 属性,没有的话,肯定没有 Observer 遍历过
function observe(value) {
  // 判断传入的值是否是对象,不是对象直接返回,不进行后面的操作
  if(typeof value !== 'object') return;
  // 用来存储当前的 Observer 实例
  let ob;
  // 判定当前属性是否有 __ob__ ,并且该属性是否原型属于 Observer
  // eslint-disable-next-line no-prototype-builtins
  if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer){
    ob=value.__ob__;
  }else{
    // 没有 __ob__ 属性代表没有遍历过,先执行 new Observer(value)
    ob = new Observer(value);
  }
  return ob;
}

// 遍历数组,将他们 observe 进行响应式
function observeArray(list) {
  for(let i=0,l=list.length;i<l;i++){
    observe(list[i])
  }
}


class Dep{
  constructor() {
    // 用数组存储自己的订阅者   subs 是 subscribes 订阅者的意思
    // 这个数组里放的是 Watcher 的实例
    this.subs=[]
  }
  // 添加依赖
  depend(){
    // 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
    if(Dep.target){
      // 将 Watcher 实例添加进 subs
      this.subs.push(Dep.target)
    }
  }
  // 通知所有订阅者
  notify(){
    // 浅克隆一份
    const subs=this.subs.slice();
    // 遍历
    for(let i=0,l=subs.length;i<l;i++){
      // 逐个更新
      subs[i].update();
    }
  }
}

class Watcher{
  // target 目标对象
  // expression 属性名
  // callback 回调函数
  // value 属性的值
  constructor(target,expression,callback) {
    this.target=target;
    // parsePath 为一个高阶函数
    this.getter=parsePath(expression);
    this.callback=callback;
    // get为我们之后要写的获取值的方法
    this.value=this.get();
  }
  // Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
  update(){
    // this.value 由于还没触发更新,所以此时是旧的值
    const oldValue=this.value;
    // 通过我们的 getter 方法,直接获取最新的值
    const newValue=this.get();
    // 将新值和旧值返回给 callback 回调函数
    this.callback(newValue,oldValue);
  }
  // 获取当前的值,并将它更新,然后 return 返回
  get(){
    // 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
    Dep.target=this;

    // 当前对象
    const obj=this.target;
    let value;
    // 当对象不再使用的时候,我们需要将它清空
    try{
      value=this.getter(obj)
    }finally {
      Dep.target=null;
    }
    this.value=value
    return value;
  }
}

function parsePath(str){
  let segments = str.split('.');
  return obj=> {
      for(let i=0;i<segments.length;i++){
        if(!obj) return;
        obj=obj[segments[i]];
      }
      return obj;
  }
}

let a={
  b:{
    c:{
      d:10
    }
  }
}

observe(a)
new Watcher(a,'b.c.d',(val,oldValue)=>{
  console.log('ok',val,oldValue);  // ok 10 5
})
a.b.c.d=55;

文章参考

【尚硅谷】Vue源码解析之数据响应式原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容