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

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

Vue2依赖收集原理的更多相关文章

  1. Vue 依赖收集原理分析

    此文已由作者吴维伟授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Vue实例在初始化时,可以接受以下几类数据: 模板 初始化数据 传递给组件的属性值 computed wat ...

  2. 深入浅出Vue基于“依赖收集”的响应式原理(转)

    add by zhj: 文章写的很通俗易懂,明白了Object.defineProperty的用法 原文:https://zhuanlan.zhihu.com/p/29318017 每当问到VueJS ...

  3. 【Vue源码学习】依赖收集

    前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...

  4. 使用 Proxy + Promise 实现 依赖收集

    (深入浅出Vue基于“依赖收集”的响应式原理) ,这篇文章讲的是通过一个通俗易懂例子,介绍了 如何用Object.defineProperty 实现的“依赖收集”的原理.Object.definePr ...

  5. Vue.js依赖收集

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...

  6. 三、vue依赖收集

    Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集,这一节我们来详细分析这个过程 Dep Dep 是整个 getter 依赖收集的核心,它的定义在 src/core ...

  7. 网站统计中的数据收集原理及实现(share)

    转载自:http://blog.codinglabs.org/articles/how-web-analytics-data-collection-system-work.html 网站数据统计分析工 ...

  8. Vue依赖收集引发的问题

    问题背景 在我们的项目中有一个可视化配置的模块,是通过go.js生成canvas来实现的.但是,我们发现这个模块在浏览器中经常会引起该tab页崩溃.开启chrome的任务管理器一看,进入该页面内存和c ...

  9. PHP依赖注入原理与用法分析

    https://www.jb51.net/article/146025.htm 本文实例讲述了PHP依赖注入原理与用法.分享给大家供大家参考,具体如下: 引言 依然是来自到喜啦的一道面试题,你知道什么 ...

  10. 读Vue源码 (依赖收集与派发更新)

    vue的依赖收集是定义在defineReactive方法中,通过Object.defineProperty来设置getter,红字部分主要做依赖收集,先判断了Dep.target如果有的情况会执行红字 ...

随机推荐

  1. js 将多层json对象 转化为一层json

    const parse = data => { const uid = `uid_${Date.now()}`; const process = (input, prefix = '', jso ...

  2. 服务器DMZ理解

    转别人的 您的公司有一堆电脑,但可以归为两大类:客户机.服务器.所谓客户机就是主动发起连接请求的机器,所谓服务器就是被动响应提供某些服务的机器.服务器又可以分仅供企业内网使用和为外网提供服务两种.   ...

  3. 循环神经网络(Recurrent Neural Networks)(第一部分)

    参考 https://www.cnblogs.com/royhoo/p/Recurrent-Neural-Networks-1.html

  4. hadoopzookeeper

    一.zookeeper是什么(概括)? Zookeeper是一个分布式协调服务的开源框架,为分布式程序提供协调服务,同时用来解决分布式集群中应用系统的数据一致性问题. zookeeper在本质上是一个 ...

  5. 循环文件夹汇总所有发票开具Excel文件数据

    'xlsx cnADO.Open "provider=Microsoft.ACE.OLEDB.12.0;extended properties='excel 8.0;hdr=no;imex= ...

  6. ASP.NET WEBAPI oken验证

    看了下网上关于.net webAPI 的案例全是坑 验证成功了不被微信服务器接收 微信客服有找不到,提问也没人回 自己测试好几个小时 终于发现返回结果只要个string 双引号都不用加 public ...

  7. Flink学习系列——简介

    Flink起源 德国柏林 Flink的目标 低延迟 高吞吐 较高的准确性(乱序数据的处理) 良好的容错性(容错性差的表现:一个节点挂了,全部回滚重新做计算,这对实时性要求高的场景非常致命)

  8. windows server 2012 AD域服务器的搭建安装 子域的创建加入 客户机加入域环境(Active Directory域)

    1,安装Active Directory域前的准备工作 2,安装Active Directory域 3,加入子域(可选) 4,加入客户机 ******************************* ...

  9. for in循环的坑

    num本来数个数组,但是for in把数组原型上的也遍历(偶尔会)记录一下坑,数组还是for循环,for in还是用在对象上好

  10. docker 镜像rabbitmq安装

    docker 镜像rabbitmq安装 1.拉取镜像 带有"mangement"的版本(包含web管理页面): docker pull rabbitmq:3.7.7-managem ...