前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些数据同样会出发setter导致重新渲染,所以vue在这里做了优化,通过收集依赖来判断哪些数据的变更需要触发视图更新。

前言

如果这篇文章有帮助到你,️关注+点赞️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

我们先来考虑两个问题:

  • 1.我们如何知道哪里用了data里面的数据?
  • 2.数据变更了,如何通知render更新视图?

在视图渲染过程中,被使用的数据需要被记录下来,并且只针对这些数据的变化触发视图更新

这就需要做依赖收集,需要为属性创建 dep 用来收集渲染 watcher

我们可以来看下官方介绍图,这里的collect as Dependency就是源码中的dep.depend()依赖收集,Notify就是源码中的dep.notify()通知订阅者

依赖收集中的各个类

Vue源码中负责依赖收集的类有三个:

  • Observer:可观测类,将数组/对象转成可观测数据,每个Observer的实例成员中都有一个Dep的实例(上一篇文章实现过这个类)

  • Dep:观察目标类,每一个数据都会有一个Dep类实例,它内部有个subs队列,subs就是subscribers的意思,保存着依赖本数据的观察者,当本数据变更时,调用dep.notify()通知观察者

  • Watcher:观察者类,进行观察者函数的包装处理。如render()函数,会被进行包装成一个Watcher实例

依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个watcher收集到Dep中。Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的watcher都通知一遍,这里我自己画了一张更清晰的图:

Observer类

这个类我们上一期已经实现过了,这一期我们主要增加的是defineReactive在劫持数据gētter时进行依赖收集,劫持数据setter时进行通知依赖更新,这里就是Vue收集依赖的入口

class Observer {
constructor(v){
// 每一个Observer实例身上都有一个Dep实例
this.dep = new Dep()
// 如果数据层次过多,需要递归去解析对象中的属性,依次增加set和get方法
def(v,'__ob__',this) //给数据挂上__ob__属性,表明已观测
if(Array.isArray(v)) {
// 把重写的数组方法重新挂在数组原型上
v.__proto__ = arrayMethods
// 如果数组里放的是对象,再进行监测
this.observerArray(v)
}else{
// 非数组就直接调用defineReactive将数据定义成响应式对象
this.walk(v)
} }
observerArray(value) {
for(let i=0; i<value.length;i++) {
observe(value[i])
}
}
walk(data) {
let keys = Object.keys(data); //获取对象key
keys.forEach(key => {
defineReactive(data,key,data[key]) // 定义响应式对象
})
}
} function defineReactive(data,key,value){
const dep = new Dep() //实例化dep,用于收集依赖,通知订阅者更新
observe(value) // 递归实现深度监测,注意性能
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
//获取值
// 如果现在处于依赖的手机阶段
if(Dep.target) {
dep.depend()
}
// 依赖收集
return value
},
set(newV) {
//设置值
if(newV === value) return
observe(newV) //继续劫持newV,用户有可能设置的新值还是一个对象
value = newV
console.log('值变化了:',value)
// 发布订阅模式,通知
dep.notify()
// cb() //订阅者收到消息回调
}
})
}

Observer类的实例挂在__ob__属性上,提供后期数据观察时使用,实例化Dep类实例,并且将对象/数组作为value属性保存下来 - 如果value是个对象,就执行walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理) - 如果value是个数组,就执行observeArray()过程,递归地对数组元素调用observe()

Dep类(订阅者)

Dep类的角色是一个订阅者,它主要作用是用来存放Watcher观察者对象,每一个数据都有一个Dep类实例,在一个项目中会有多个观察者,但由于JavaScript是单线程的,所以在同一时刻,只能有一个观察者在执行,此刻正在执行的那个观察者所对应的Watcher实例就会赋值给Dep.target这个变量,从而只要访问Dep.target就能知道当前的观察者是谁。

var uid = 0
export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // subscribes订阅者,存储订阅者,这里放的是Watcher的实例
} //收集观察者
addSub(watcher) {
this.subs.push(watcher)
}
// 添加依赖
depend() {
// 自己指定的全局位置,全局唯一
//自己指定的全局位置,全局唯一,实例化Watcher时会赋值Dep.target = Watcher实例
if(Dep.target) {
this.addSub(Dep.target)
}
}
//通知观察者去更新
notify() {
console.log('通知观察者更新~')
const subs = this.subs.slice() // 复制一份
subs.forEach(w=>w.update())
}
}

Dep实际上就是对Watcher的管理,Dep脱离Watcher单独存在是没有意义的。

  • Dep是一个发布者,可以订阅多个观察者,依赖收集之后Dep中会有一个subs存放一个或多个观察者,在数据变更的时候通知所有的watcher
  • DepObserver的关系就是Observer监听整个data,遍历data的每个属性给每个属性绑定defineReactive方法劫持gettersetter, 在getter的时候往Dep类里塞依赖(dep.depend),在setter的时候通知所有watcher进行update(dep.notify)

Watcher类(观察者)

Watcher类的角色是观察者,它关心的是数据,在数据变更之后获得通知,通过回调函数进行更新。

由上面的Dep可知,Watcher需要实现以下两个功能:

  • dep.depend()的时候往subs里面添加自己
  • dep.notify()的时候调用watcher.update(),进行更新视图

同时要注意的是,watcher有三种:render watcher、 computed watcher、user watcher(就是vue方法中的那个watch)

var uid = 0
import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
constructor(vm,expr,cb,options){
this.vm = vm // 组件实例
this.expr = expr // 需要观察的表达式
this.cb = cb // 当被观察的表达式发生变化时的回调函数
this.id = uid++ // 观察者实例对象的唯一标识
this.options = options // 观察者选项
this.getter = parsePath(expr)
this.value = this.get()
} get(){
// 依赖收集,把全局的Dep.target设置为Watcher本身
Dep.target = this
const obj = this.vm
let val
// 只要能找就一直找
try{
val = this.getter(obj)
} finally{
// 依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。
Dep.target = null
}
return val }
// 当依赖发生变化时,触发更新
update() {
this.run()
}
run() {
this.getAndInvoke(this.cb)
}
getAndInvoke(cb) {
let val = this.get() if(val !== this.value || typeof val == 'object') {
const oldVal = this.value
this.value = val
cb.call(this.target,val, oldVal)
}
}
}

要注意的是,watcher中有个sync属性,绝大多数情况下,watcher并不是同步更新的,而是采用异步更新的方式,也就是调用queueWatcher(this)推送到观察者队列当中,待nextTick的时候进行调用。

这里的parsePath函数比较有意思,它是一个高阶函数,用于把表达式解析成getter,也就是取值,我们可以试着写写看:

export function parsePath (str) {
const segments = str.split('.') // 先将表达式以.切割成一个数据
// 它会返回一个函数
return obj = > {
for(let i=0; i< segments.length; i++) {
if(!obj) return
// 遍历表达式取出最终值
obj = obj[segments[i]]
}
return obj
}
}

Dep与Watcher的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者, dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

总结

依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集(这里就是我们常写的那个watch)
  3. render()时,触发 render watcher 依赖收集
  4. re-render 时,render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。
observe->walk->defineReactive->get->dep.depend()->
watcher.addDep(new Dep()) ->
watcher.newDeps.push(dep) ->
dep.addSub(new Watcher()) ->
dep.subs.push(watcher)

派发更新

  1. 组件中对响应的数据进行了修改,触发defineReactive中的 setter 的逻辑
  2. 然后调用 dep.notify()
  3. 最后遍历所有的 subs(Watcher 实例),调用每一个 watcherupdate 方法。
set ->
dep.notify() ->
subs[i].update() ->
watcher.run() || queueWatcher(this) ->
watcher.get() || watcher.cb ->
watcher.getter() ->
vm._update() ->
vm.__patch__()

推荐阅读

原文首发地址点这里,欢迎大家关注公众号 「前端南玖」

我是南玖,我们下一期见!!!

【Vue源码学习】依赖收集的更多相关文章

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

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

  2. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  3. Vue源码学习三 ———— Vue构造函数包装

    Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...

  4. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  5. 最新 Vue 源码学习笔记

    最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...

  6. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...

  7. Vue 源码学习(1)

    概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...

  8. VUE 源码学习01 源码入口

    VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...

  9. Vue源码学习(一):调试环境搭建

    最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...

随机推荐

  1. 【LeetCode】427. Construct Quad Tree 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  2. 【LeetCode】92. Reverse Linked List II 解题报告(Python&C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 迭代 递归 日期 题目地址:https://leet ...

  3. 【LeetCode】334. Increasing Triplet Subsequence 解题报告(Python)

    [LeetCode]334. Increasing Triplet Subsequence 解题报告(Python) 标签(空格分隔): LeetCode 题目地址:https://leetcode. ...

  4. Feature Distillation With Guided Adversarial Contrastive Learning

    目录 概 主要内容 reweight 拟合概率 实验的细节 疑问 Bai T., Chen J., Zhao J., Wen B., Jiang X., Kot A. Feature Distilla ...

  5. Java面向对象笔记 • 【第10章 Swing编程初级应用】

    全部章节   >>>> 本章目录 10.1 JFrame窗口容器 10.1.1 Swing介绍 10.1.2 JFrame窗口容器应用 JFrame常用方法 10.1.3 实践 ...

  6. 【】VMware vSphere中三种磁盘规格的解释说明

    在VMware vSphere中,不管是以前的5.1版本,或者是现在的6.5版本,创建虚拟机时,在创建磁盘时,都会让选择磁盘的置备类型,如下图所示,分为: 厚置备延迟置零 厚置备置零 Thin Pro ...

  7. STL(1)vector

    STL(1) 1.vector vector是vector直译为"向量",一般说成"变长数组",也就是长度根据需要而自动改变的数组,有些题目需要开很多数组,往往 ...

  8. 解决VirtualBox 运行时报内存不能written

    在VirtualBox 虚拟机中安装系统的时候,突然报"0x00000000指令,该内存不能written",只能强制停止,这个问题要怎么解决呢? 解决办法是恢复系统主题3个dll ...

  9. vue3 父菜单渲染出来了,但是子菜单渲染不出来的原因

    子菜单始终渲染不出来,控制台出现警告如下: 在查看框架源码时,发现在渲染时应用了递归.在这个博客找到答案,原因是升级的vue的版本没有升级@vue/compiler-sfc的版本,这两个版本应该保持一 ...

  10. 一篇文章带你搞懂DEX文件的结构

    *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 DEX文件就是Android Dalvik虚拟机运行的程序,关于DEX文件的结构的重要性我就不多说了.下面,开练! 建议:不要只看 ...