阅读 299

数据响应式原理 - 03

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

本文是关于数据响应式原理的学习笔记,目的在于更好的理解 Vue 的底层原理,篇幅较长,故而拆分为几篇,此篇为完结篇。
传送门《数据响应式原理 - 01》《数据响应式原理 - 02》

在上一篇中,我们实现了数组的响应式处理,本篇将对依赖收集和 Watcher 类进行介绍。

收集依赖

前面我们已经将数据变成了响应式数据,但是怎么让用户定义的方法可以在这些数据改变时调用呢?
这就要用到 Watcher,new Watcher 时传入的参数里会有个 callback,这个 callback 就可以是用户定义的方法,通过 watcher 那些响应式数据,当数据改变时,调用 callback。

那么为什么 watcher 的数据改变时能够调用 callback 呢?
这就涉及到依赖的收集,首先这些数据已经是 observe 处理了,也就是说已经是响应式的了。在 Watcher 的构造函数中会去获取要订阅的数据的值,这就会触发数据的 getter,一旦触发 getter 就会把这个 watcher 实例收集到一个数组 subs 里,一旦这个数据被改动,就会触发 setter,然后在 setter 里会循环 subs 数组,一个个去通知,执行 update 方法,通过 update 方法里最终触发 callback。

依赖是什么?

  • 需要用到数据的地方称为依赖。在 vue2.x 中,用到数据的组件是依赖。当数据变化时通知组件,在组件内通过虚拟 dom 进行 diff 算法
  • 在 getter 中收集依赖,在 setter 中触发依赖

Dep 类

Dep 类用来封装依赖收集的代码,管理依赖

// Dep.js
export default class Dep {
  constructor(arg) {
     // 用数组存储自己的订阅者, 数组里是 Watcher 实例
     this.subs = []
  }
  
  // 添加订阅
  addSub(sub) {
    this.subs.push(sub)
  }
  
  // 添加依赖
  depend() {
    // Dep.target 就是我们指定的一个全局唯一位置,换成 window.target 也一样
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  
  // 通知更新
  notify() {
    const subs = this.subs.slice() // 浅克隆
    subs.forEach(item => {
      item.update()
    })
  }
}
复制代码

每个 Observer 实例中都有一个 Dep 的实例

在 Observer 类的constructor 函数中 const dep = new Dep()

// Observer.js
...
import Dep from './Dep.js'
export default class Observer {
  constructor(value) {
    this.dep = new Dep() // 本次笔记的案例中,这里其实不写也可以
    ...
  }
  ...
}
复制代码

还有个地方也会创建 Dep 实例,就是在 defineReactive 里

目的是在于可以在被侦测的对象 setter 时去发通知 dep.notify()

// defineReactive.js
import Dep from './Dep.js'
export default function defineReactive(data, key, value) {
  const dep = new Dep()
  ...
  Object.defineProperty(data, key, {
    ...
    set(newValue) {
      ...
      // 在 setter 中触发依赖
      dep.notify()
    }
  })
}
复制代码

Dep 使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有 Watcher 都通知一遍
这样,一旦去修改 obj,比如 obj.b = 3,就会执行 Dep 的 notify 方法。当然,通过 7 种能改变数组本身的方法改变数组时,也需要能够通知,所以在 array.js 改写 7 中方法时也加上 ob.dep.notify()

// array.js 
...
methodsCouldChange.forEach(item => {
  ...
  def(arrayMethods, item, function() { 
    ...
    ob.dep.notify()
    ...
  }, false)
})
...
复制代码

Watcher 类

先说目的

我们最终目的是在 index.js 新建一个 watcher 类的实例去监控我们指定的对象的指定的属性,并希望在 new Watcher() 的第 3 个参数,回调函数里得到对象属性修改前后的值,这样就可以去做一些我们想做的事情了。

// index.js
import observe from './observe.js'
import Watcher from './Watcher.js'

let obj = {
  a: {
    m: {
      n: 1
    }
  }
}
observe(obj)
new Watcher(obj, 'a.m.n', (val, oldValue) => {
  console.log('watcher', val, oldValue)
})
obj.a.m.n = 2
复制代码

期望得的到结果是

image (1).png
也就是说只要我 new 了一个 Watcher 实例,并把想要监控的属性(a.m.n)和对象(obj)传进去,那么在 Watcher 的第 3 个参数,也就是个回调函数里就能得到 obj.a.m.n 的新旧属性,并能继续做一些事情,比如进行 diff 算法等等。下面开始书写 Watcher 类:

新建 Watcher.js 文件

// Watcher.js
import Dep from './Dep.js'
let uid = 0
export default class Watcher {
  constructor(target, expression, callback) {
    this.id = uid++ // 让每个 watcher 实例有一个自己的 id
    this.target = target // target 为新建实例时传入的要监控的对象(obj)
    this.getter = parsePath(expression) // getter 会是一个函数, 在下面定义的 get 里调用
    this.callback = callback // callback 就是传入的回调函数
    this.val = this.get() // 获取对象 target 的 expression 属性的值
  }

  // 数据更新触发
  update() {
    this.run()
  }

  get() {
    // 将 Dep.target 赋值为 new 的这个 Watcher 实例本身,代表进入依赖收集阶段
    Dep.target = this
    const obj = this.target
    
    let value
    try {
      /*
        注意,一旦这里去获取 obj 的 expression 属性的值, 
        因为 obj 已经被 observe 了,所以就会触发 defineReactive, 
        Object.defineProperty 里的 get() 就会被触发
      */
      value = this.getter(obj)
    } finally { // 在 try 语句块之后执行, 无论是否有异常抛出或捕获都将执行
      Dep.target = null // 退出依赖收集
    }
    return value
  }

  run() {
    this.getAndInvoke(this.callback)
  }

  getAndInvoke(cb) {
    const newValue = this.get()
    if (newValue !== this.val || typeof newValue === 'object') {
      const oldValue = this.val
      cb.call(this.target, newValue, oldValue)
    }
  }
}

// 传入一个属性字符串比如 a.m.n, 然后返回一个函数(getter),给这个函数传入 obj, 则可以得到 obj.a.m.n 的值
const parsePath = function(str) {
  const segments = str.split('.')
  return function(obj) {
    const value = segments.reduce((accumulator, currentValue) => {
      return accumulator = accumulator[currentValue]
    }, obj)
    return value
  }
}
复制代码

至此,我们先停下来理清下思路:为了实现开始的那个目的,我们新建了 Watcher 类,当我们在 index.js 进行 new Watcher(obj, 'a.m.n', (val, oldValue) => {console.log('watcher', val, oldValue)}) 时,就会执行 Watcher 类的构造函数,其中有这么一句 this.val = this.get(),这是条关键语句,在 get() 函数中,主要做了 2 件事:

  1. 通过 Dep.target = this 开始收集依赖,给全局变量 Dep.target 赋值,值为这个 Watcher 实例本身。
  2. 通过 value = this.getter(obj) 去查找 obj.a.m.n 的值,因为之前已经通过 observe(obj) 让 obj 的每一个属性的 getter 和 setter 都是被监听的,所以这就触发了 obj.a.m.n 的 getter。

那么,我们就可以对 defineReactive.js 做如下更改

// defineReactive.js
...
export default function defineReactive(data, key, value) {
  ...
  Object.defineProperty(data, key, {
    ...
    get() {
      // 如果处于依赖收集阶段(在 getter 中收集依赖)
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    ...
  })
}
复制代码

依赖就是 Watcher,只有 Watcher 触发的 getter 才会收集依赖,哪个 Watcher 触发了 getter,就把哪个 Watcher 收集到 Dep 中。

至此,数据响应式原理的内容学习笔记分享完毕,难免有所纰漏之处,还请斧正。

感谢.gif

点赞.png

文章分类
前端
文章标签