Vue2依赖收集原理(Dep、Watcher、Observer)

这篇具有很好参考价值的文章主要介绍了Vue2依赖收集原理(Dep、Watcher、Observer)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

观察者模式定义了对象间一对多的依赖关系。即被观察者状态发生变动时,所有依赖于它的观察者都会得到通知并自动更新。解决了主体对象和观察者之间功能的耦合。

Vue中基于 Observer、Dep、Watcher 三个类实现了观察者模式

  • Observer类 负责数据劫持,访问数据时,调用dep.depend()进行依赖收集;数据变更时,调用dep.notify() 通知观察者更新视图。我们的数据就是被观察者
  • Dep类 负责收集观察者 watcher,以及通知观察者 watcher 进行 update 更新操作
  • Watcher类 为观察者,负责订阅 dep,并在订阅时让 dep 同步收集当前 watcher。当接收到 dep 的通知时,执行 update 重新渲染视图

Vue2依赖收集原理(Dep、Watcher、Observer)

dep 和 watcher 是一个多对多的关系。每个组件都对应一个渲染 watcher,每个响应式属性都有一个 dep 收集器。一个组件可以包含多个属性(一个 watcher 对应多个 dep),一个属性可以被多个组件使用(一个 dep 对应多个 watcher)

Vue2依赖收集原理(Dep、Watcher、Observer)

Dep

我们需要给每个属性都增加一个 dep 收集器,目的就是收集 watcher。当响应式数据发生变化时,更新收集的所有 watcher

  1. 定义 subs 数组,当劫持到数据访问时,执行 dep.depend(),通知 watcher 订阅 dep,然后在 watcher内部执行dep.addSub(),通知 dep 收集 watcher
  2. 当劫持到数据变更时,执行dep.notify() ,通知所有的观察者 watcher 进行 update 更新操作

Dep有一个静态属性 target,全局唯一,Dep.target 是当前正在执行的 watcher 实例,这是一个非常巧妙的设计!因为在同一时间只能有一个全局的 watcher

注意:
渲染/更新完毕后我们会立即清空 Dep.target,保证了只有在模版渲染/更新阶段的取值操作才会进行依赖收集。之后我们手动进行数据访问时,不会触发依赖收集,因为此时 Dep.target 已经重置为 null

let id = 0

class Dep {
  constructor() {
    this.id = id++
    // 依赖收集,收集当前属性对应的观察者 watcher
    this.subs = []
  }
  // 通知 watcher 收集 dep
  depend() {
    Dep.target.addDep(this)
  }
  // 让当前的 dep收集 watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 通知subs 中的所有 watcher 去更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 当前渲染的 watcher,静态变量
Dep.target = null

export default Dep

Watcher

不同组件有不同的 watcher。我们先只需关注渲染watcher。计算属性watcer和监听器watcher后面会单独讲!

watcher 负责订阅 dep ,并在订阅的同时执行dep.addSub(),让 dep 也收集 watcher。当接收到 dep 发布的消息时(通过 dep.notify()),执行 update 重新渲染

当我们初始化组件时,在 mountComponent 方法内会实例化一个渲染 watcher,其回调就是 vm._update(vm._render())

import Watcher from './observe/watcher'

// 初始化元素
export function mountComponent(vm, el) {
  vm.$el = el

  const updateComponent = () => {
    vm._update(vm._render())
  }

  // true用于标识是一个渲染watcher
  const watcher = new Watcher(vm, updateComponent, true)
}

当我们实例化渲染 watcher 的时候,在构造函数中会把回调赋给this.getter,并调用this.get()方法。
这时!!!我们会把当前的渲染 watcher 放到 Dep.target 上,并在执行完回调渲染视图后,立即清空 Dep.target,保证了只有在模版渲染/更新阶段的取值操作才会进行依赖收集

import Dep from './dep'

let id = 0

class Watcher {
  constructor(vm, fn) {
    this.id = id++
    this.getter = fn
    this.deps = []  // 收集当前 watcher 对应被观察者属性的 dep
    this.depsId = new Set()
    this.get()
  }
  // 收集 dep
  addDep(dep) {
    let id = dep.id
    // 去重,一个组件 可对应 多个属性 重复的属性不用再次记录
    if (!this.depsId.has(id)) {
      this.deps.push(dep)
      this.depsId.add(id)
      dep.addSub(this) // watcher已经收集了去重后的 dep,同时让 dep也收集 watcher
    }
  }
  // 执行 watcher 回调
  get() {
    Dep.target = this // Dep.target 是一个静态属性

    this.getter() // 执行vm._render时,会劫持到数据访问,调用 dep.depend() 进行依赖收集

    Dep.target = null // 渲染完毕置空,保证了只有在模版渲染阶段的取值操作才会进行依赖收集
  }
  // 重新渲染
  update() {
    this.get()
  }
}

我们是如何触发依赖收集的呢?

在执行this.getter()回调时,我们会调用vm._render() ,在_s()方法中会去 vm 上取值,这时我们劫持到数据访问走到 getter,进而执行dep.depend()进行依赖收集

流程:vm._render() ->vm.$options.render.call(vm) -> with(this){ return _c('div',null,_v(_s(name))) } -> 会去作用域链 this 上取 name

在 MDN 中是这样描述 with 的

JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值

Observer

我们只会在 Observer 类 和 defineReactive 函数中实例化 dep。在 getter 方法中执行dep.depend()依赖收集,在 setter 方法中执行dep.notity()派发更新通知

依赖收集

依赖收集的入口就是在Object.defineProperty的 getter 中,我们重点关注2个地方,一个是在我们实例化 dep 的时机,另一个是为什么递归依赖收集。我们先来看下代码

class Observer {
  constructor(data) {
    // 给数组/对象的实例都增加一个 dep
    this.dep = new Dep()

    // data.__ob__ = this 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false, // 将__ob__ 变成不可枚举
    })
    if (Array.isArray(data)) {
      // 重写可以修改数组本身的方法 7个方法
      data.__proto__ = newArrayProto
      this.observeArray(data) 
    } else {
      this.walk(data)
    }
  }

  // 循环对象"重新定义属性",对属性依次劫持,性能差
  walk(data) {
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
  }

  // 观测数组
  observeArray(data) {
    data.forEach(item => observe(item))
  }
}

// 深层次嵌套会递归处理,递归多了性能就差 
function dependArray(value) {
  for (let i = 0; i < value.length; i++) {
    let current = value[i]
    current.__ob__ && current.__ob__.dep.depend()
    if (Array.isArray(current)) {
      dependArray(current)
    } 
  }
}

export function defineReactive(target, key, value) {
  // 深度属性劫持;给所有的数组/对象的实例都增加一个 dep,childOb.dep 用来收集依赖
  let childOb = observe(value)

  let dep = new Dep() // 每一个属性都有自己的 dep

  Object.defineProperty(target, key, {
    get() {
      // 保证了只有在模版渲染阶段的取值操作才会进行依赖收集
      if (Dep.target) {   
        dep.depend() // 依赖收集
        if (childOb) {
          childOb.dep.depend() // 让数组/对象实例本身也实现依赖收集,$set原理
          if (Array.isArray(value)) { // 数组需要递归处理
            dependArray(value)
          }
        }
      }
      return value
    },
    set(newValue) { ... },
  })
}

实例化 dep 的时机

我们只会在 Observer 类 和 defineReactive 函数中实例化 dep

  1. Observer类:在 Observer 类中实例化 dep,可以给每个数组/对象的实例都增加一个 dep
  2. defineReactive函数:在 defineReactive 方法中实例化 dep,可以让每个被劫持的属性都拥有一个 dep,这个 dep 是被闭包读取的局部变量,会驻留到内存中且不会污染全局

我们为什么要在 Observer 类中实例化 dep?

  • Vue 无法检测通过数组索引改变数组的操作,这不是 Object.defineProperty() api 的原因,而是尤大认为性能消耗与带来的用户体验不成正比。对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如10000条
  • Object.defineProperty() 无法监听数组的新增

如果想要在通过索引直接改变数组成员或对象新增属性后,也可以派发更新。那我们必须要给数组/对象实例本身增加 dep 收集器,这样就可以通过 xxx.__ob__.dep.notify() 手动触发 watcher 更新了

这其实就是 vm.$set 的内部原理!!!

递归依赖收集

数组中的嵌套数组/对象没办法走到 Object.defineProperty,无法在 getter 方法中执行dep.depend()依赖收集,所以需要递归收集

举个栗子:data: {arr: ['a', 'b', ['c', 'd', 'e', ['f', 'g']], {name: 'libc'}]}

我们可以劫持 data.arr,并触发 arr 实例上的 dep 依赖收集,然后循环触发 arr 成员的 dep依赖收集。对于深层数组嵌套的['f', 'g'],我们则需要递归触发其实例上的 dep 依赖收集

派发更新

对于对象

在 setter 方法中执行dep.notity(),通知所有的订阅者,派发更新通知
注: 这个 dep 是在 defineReactive 函数中实例化的。 它是被闭包读取的局部变量,会驻留到内存中且不会污染全局

Object.defineProperty(target, key, {
  get() { ... },

  set(newValue) {
    if (newValue === value) return
    // 修改后重新观测。新值为对象的话,可以劫持其数据。并给所有的数组/对象的实例都增加一个 dep
    observe(newValue)
    value = newValue

    // 通知 watcher 更新
    dep.notify()
  },
})

对于数组

在数组的重写方法中执行xxx.__ob__.dep.notify(),通知所有的订阅者,派发更新通知

注: 这个 dep 是在 Observer 类中实例化的,我们给数组/对象的实例都增加一个 dep。可以通过响应式数据的__ob__获取到实例,进而访问实例上的属性和方法文章来源地址https://www.toymoban.com/news/detail-409332.html

let oldArrayProto = Array.prototype // 获取数组的原型
// newArrayProto.__proto__  = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto)

// 找到所有的变异方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'] // concat slice 都不会改变原数组

methods.forEach(method => {
  // 这里重写了数组的方法
  newArrayProto[method] = function (...args) {
    // args reset参数收集,args为真正数组,arguments为伪数组
    const result = oldArrayProto[method].call(this, ...args) // 内部调用原来的方法,函数的劫持,切片编程

    // 我们需要对新增的数据再次进行劫持
    let inserted
    let ob = this.__ob__

    switch (method) {
      case 'push':
      case 'unshift': // arr.unshift(1,2,3)
        inserted = args
        break
      case 'splice': // arr.splice(0,1,{a:1},{a:1})
        inserted = args.slice(2)
      default:
        break
    }

    if (inserted) {
      // 对新增的内容再次进行观测
      ob.observeArray(inserted)
    }

    // 通知 watcher 更新渲染
    ob.dep.notify()
    return result
  }
})

到了这里,关于Vue2依赖收集原理(Dep、Watcher、Observer)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 设计模式—观察者模式(Observer)

    目录 思维导图 一、什么是观察者模式? 二、有什么优点吗? 三、有什么缺点吗? 四、什么时候使用观察者模式? 五、代码展示 ①、双向耦合的代码 ②、解耦实践一 ③、解耦实践二 ④、观察者模式 六、这个模式涉及到了哪些知识点? 又叫发布-订阅(publish/Subscrib)模式

    2024年02月11日
    浏览(44)
  • 行为型-观察者模式(Observer Pattern)

    详细介绍观察者模式(Observer Pattern),并说明优缺点与使用场景: 观察者模式是一种行为型设计模式,它定义了对象之间的一种一对多的依赖关系,使得当一个对象的状态发生变化时,所有依赖它的对象都会自动收到通知并更新。该模式包含两个核心角色:被观察者(Subj

    2024年02月15日
    浏览(64)
  • 【设计模式-06】Observer观察者模式

    事件处理模型 场景示例:小朋友睡醒了哭,饿! 核心思路和逻辑 定义事件类 ActionEvent 定义接口类 ActionListener 和接口方法 void actionPerformed(ActionEvent e); 自定义 Button 类,模拟按钮点击事件 自定义监听者 MyActionEventListener 和 MyActionEventListener2 实现接口 void actionPerformed(ActionEvent

    2024年01月17日
    浏览(45)
  • 【设计模式】观察者模式Observer Pattern

    目录 遇到问题 梳理需求 观察者模式的实现 JDK中的实现 当一个对象发生修改时,需要通知多方。 很自然就会想到回调,这个就是观察者模式的核心,观察者模式可以将大量的回调解耦,从而使代码更加优雅。 意图: 定义对象间的一种一对多的依赖关系,当一个对象的状态

    2024年02月22日
    浏览(42)
  • 「观察者(Observer)」设计模式 Swift实现

    观察者设计模式(Observer Pattern)是一种行为型设计模式,它定义了对象之间的一种一对多的依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都会受到通知并自动更新。 在这种模式中,被观察者对象(Subject)存储其观察者对象(Observer)列表,并提供了用

    2024年02月16日
    浏览(39)
  • 设计模式二十:观察者模式(Observer Pattern)

    定义了一种一对多的依赖关系,允许多个观察者(也称为订阅者)对象同时监听一个主题对象,当主题对象发生变化时,所有依赖于它的观察者都会收到通知并自动更新。 观察者模式的使用场景 观察者模式在许多场景中都可以发挥作用,特别是在需要解耦和实现对象间动态

    2024年02月12日
    浏览(39)
  • (三)行为模式:7、观察者模式(Observer Pattern)(C++示例)

    目录 1、观察者模式(Observer Pattern)含义 2、观察者模式的UML图学习 3、观察者模式的应用场景 4、观察者模式的优缺点 (1)优点: (2)缺点 5、C++实现观察者模式的实例 1、观察者模式(Observer Pattern)含义 观察者模式(Observer)定义了一种一对多的依赖关系,让多个观察者

    2024年02月09日
    浏览(42)
  • 设计模式——观察者模式(Observer Pattern)+ Spring相关源码

    类型:行为型模式 目的:当一个对象的状态发生改变时,其所有依赖者(观察者)都会收到通知并自动更新。 2.1.1 定义观察者 2.1.2 定义被观察对象 2.1.3 使用 2.2.1 观察者接口Observer 2.2.1 被观察者对象Observable 2.3.1 观察者 2.3.2 被观察者 创建型模式 结构型模式 1、设计模式——

    2024年02月06日
    浏览(48)
  • 观察者设计模式(Observer Design Pattern)[论点:概念、组成角色、相关图示、示例代码、框架中的运用、适用场景]

            观察者设计模式(Observer Design Pattern)是一种行为型设计模式,它定义了一种对象间的一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象状态发生改变时,通知所有观察者对象,使它们能够自动更新。 主题(Subject):主题是一个抽象类或

    2023年04月24日
    浏览(46)
  • Vue2第八节 收集表单数据

    (1)文本类型/密码类型 v-model收集的是value值     用户输入的就是value值 (2)number类型:限制输入的只能是数字  (3)radio类型     v-model收集的是value值,需要手动给标签配置value值     如果要设置默认选中,在data中配置要选中的数据即可    (4)checkBox类型    ① 如果

    2024年02月15日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包