首页 > 其他分享 >Vue2依赖收集原理

Vue2依赖收集原理

时间:2023-04-03 09:22:05浏览次数:52  
标签:依赖 数组 收集 dep watcher Dep 实例 Vue2

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

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

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

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

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__获取到实例,进而访问实例上的属性和方法

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
  }
})

标签:依赖,数组,收集,dep,watcher,Dep,实例,Vue2
From: https://www.cnblogs.com/burc/p/17254663.html

相关文章

  • springboot起步依赖
    <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.0.RELEASE</version><relativePath/><!--lookupparentfromrepository--></......
  • VUE2.0 学习 第一组
    本笔记主要参考菜鸟教程和官方文档编写。 1.对于Vue2.0来说每个vue应用都需要实例化vue来实现。   varvm=newVue({//选项}) 2.首先,DOM是一种api,它可以动态地访问程序和脚本,更新其内容、结构和文档,我认为符合这类功能的都可以叫dom,其次,每一个DOM元素的id(......
  • 02-依赖管理和自动配置
    依赖管理什么是依赖管理spring-boot-starter-parent还有父项目,声明了开发中常用的依赖的版本号并且进行自动版本仲裁,即如果程序员没有指定某个依赖jar的版本,则以父项目指定的版本为准修改自动仲裁/默认版本号e.g.将SpringBootmysql驱动修改成5.1.49starte......
  • 从原理上理解Spring如何解决循环依赖
    上图展示了循环依赖是什么,类A存在B类的成员变量,所以类A依赖于类B,类B同样存在类A的成员变量,所以类B也依赖于类A,就形成了循环依赖问题。Spring是如何创建Bean的Spring中Bean初始化的精简流程如下:简要描述一下SpringBean的创建流程:(1)首先Spring容器启动之后,会根据使用不同类型......
  • 健康知识收集
    高反药物阿咖酚散、加合百服宁、芬必得或散利痛,能够有效缓解因高原反应引起的头痛症状;肌苷口服液是缓解高原反应比较有效的药物;服用西洋参含片和葡萄糖口服液对缓解在高原引起的疲劳有一定帮助。在拉萨还可以买到一种叫做高原康的药物,比较好的抗急性高原反应药物,但此药品有一......
  • 第二十一篇 vue - 深入组件 - 依赖注入 - provide 和 inject
    Prop逐级透传问题provide和inject可以帮助我们解决这一问题。[1]一个父组件相对于其所有的后代组件,会作为依赖提供者任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖Prop逐级透传问题通常情况下,当我们需要从父组件向子组件传递数据时,会使用pr......
  • vue2 + sass + sass-loader
    本地vue脚手架版本:5.0.8本地node版本:v18.13.0项目创建:vueinitwebpackdemo由于项目本身不支持sass,需要首先安装:npminstallsasssass-loader-D,记住:此处无需安装node-sass,安装后报错。由于sass和sass-loader版本不兼容会出现报错(TypeError:this.getOptionsisnotafunc......
  • 通过Sysmon+Nxlogs收集Windows Server 2012服务器日志-并以Syslog形式发送Json格式数
    0x01环境介绍WindowsServer2012已经安装部署好了域控,目的除了收集Windows服务器本身的日志外还收集域控环境下的各种日志。0x02Nxlog配置和使用使用社区版本即可,下载地址:https://nxlog.co/downloads/nxlog-ce#nxlog-community-edition使用的版本是当前最新版本安装过程就省略,......
  • vue2中使用antv/G6节点内容可滚动的ER图
    先举一个栗子: 如果不会请移步到官网的栗子,请点击查看狠人话不多,直接给大家上代码:整体代码片段<template><divid="container"style="position:relative;"></div></template><script>/*eslint-disable*/importG6from"@antv/g6";i......
  • gulp笔记 2 (进阶一点点:使用bower来管理前端依赖)
    其实gulp比例1中的内容已经基本满足开发要求了。此文为进阶的一点点知识#1 安装bower(bower是个纯web前端依赖管理工具。)   npminstall-gbower #版本为1.8.14,必须安装在全局   bowerinit#会生成一个bower.json文件,选项寂寞默认就行,bower的库户自动放到bowe......