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
- 定义 subs 数组,当劫持到数据访问时,执行 
dep.depend(),通知 watcher 订阅 dep,然后在 watcher内部执行dep.addSub(),通知 dep 收集 watcher - 当劫持到数据变更时,执行
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
- Observer类:在 Observer 类中实例化 dep,可以给每个数组/对象的实例都增加一个 dep
 - 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
  }
})
												
											Vue2依赖收集原理的更多相关文章
- Vue 依赖收集原理分析
		
此文已由作者吴维伟授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Vue实例在初始化时,可以接受以下几类数据: 模板 初始化数据 传递给组件的属性值 computed wat ...
 - 深入浅出Vue基于“依赖收集”的响应式原理(转)
		
add by zhj: 文章写的很通俗易懂,明白了Object.defineProperty的用法 原文:https://zhuanlan.zhihu.com/p/29318017 每当问到VueJS ...
 - 【Vue源码学习】依赖收集
		
前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...
 - 使用 Proxy + Promise 实现 依赖收集
		
(深入浅出Vue基于“依赖收集”的响应式原理) ,这篇文章讲的是通过一个通俗易懂例子,介绍了 如何用Object.defineProperty 实现的“依赖收集”的原理.Object.definePr ...
 - Vue.js依赖收集
		
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
 - 三、vue依赖收集
		
Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集,这一节我们来详细分析这个过程 Dep Dep 是整个 getter 依赖收集的核心,它的定义在 src/core ...
 - 网站统计中的数据收集原理及实现(share)
		
转载自:http://blog.codinglabs.org/articles/how-web-analytics-data-collection-system-work.html 网站数据统计分析工 ...
 - Vue依赖收集引发的问题
		
问题背景 在我们的项目中有一个可视化配置的模块,是通过go.js生成canvas来实现的.但是,我们发现这个模块在浏览器中经常会引起该tab页崩溃.开启chrome的任务管理器一看,进入该页面内存和c ...
 - PHP依赖注入原理与用法分析
		
https://www.jb51.net/article/146025.htm 本文实例讲述了PHP依赖注入原理与用法.分享给大家供大家参考,具体如下: 引言 依然是来自到喜啦的一道面试题,你知道什么 ...
 - 读Vue源码 (依赖收集与派发更新)
		
vue的依赖收集是定义在defineReactive方法中,通过Object.defineProperty来设置getter,红字部分主要做依赖收集,先判断了Dep.target如果有的情况会执行红字 ...
 
随机推荐
- 【逆向】x64dbg设置条件断点 比较内存字符串是否相等
			
前言 在OD中可以设置条件断点,通过表达式对字符串数据进行比较,比如在CreateFile打开某个特定文件的时候让调试器中断.但是在x32dbg.x64dbg中因为表达式只支持整数,不支持字符串和其它 ...
 - 解决CentOS 7.x虚拟机无法上网的问题
			
参考地址:https://blog.csdn.net/weixin_43317914/article/details/124770393 1.关闭虚拟机 2.打开cmd,查看本机dns 3.打开虚拟机 ...
 - 3月2号Android开发学习
			
(2)视图基础 1.设置视图的高度 视图宽度通过属性Android:layout_width表达,视图高度通过属性android:layout_heigth表达,宽高的取值主要有以下三种 1.matc ...
 - scrollToFirstError失效解决方法
			
ant design 使用 设置scrollToFirstError = true,表单验证失败后却没有滚动到第一个错误字段 解决方法: 在button按钮中加入 html-type = 'submi ...
 - vue中模块化后mapState的使用
			
代码如下: 相当于声明了一个变量name,然后以state入参取得其modules文件夹中user文件里的name属性.因为在模块(如user)中,在抛出时的export default中添加了一句: ...
 - python excel使用
			
python excel使用 https://blog.csdn.net/m0_59235508/article/details/122808875 pandas不覆盖写入 https://blog. ...
 - js数组原型方法
			
今天学习了一下js数组原型的操作方法,小结一下学习地址https://www.cnblogs.com/obel/p/7016414.html 1.join() join(separator): 将数组 ...
 - scanf()函数的详解以及使用时需要注意的一些细节-C语言基础
			
这篇文章要探讨的是"scanf()函数的详解以及使用时需要注意的一些细节".涉及scanf()函数的应用和需要注意的问题.属于C语言基础篇(持续更新). scanf()(函数原型: ...
 - flask-script的简单使用
			
1.Flask-Script介绍Flask-Script的作用是可以通过命令行的形式来操作Flask.Flask Script扩展提供向Flask插入外部脚本的功能,包括运行一个开发用的服务器,一个定 ...
 - find . -name "*.php" -execdir grep -nH --color=auto foo {} ';'
			
find . -name "*.php" -execdir grep -nH --color=auto foo {} ';'