【Vue源码学习】依赖收集
前面我们学习了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。
- Dep和- Observer的关系就是- Observer监听整个data,遍历data的每个属性给每个属性绑定- defineReactive方法劫持- getter和- setter, 在- 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 更新。
总结
依赖收集
- initState时,对- computed属性初始化时,触发- computed watcher依赖收集
- initState时,对侦听属性初始化时,触发- user watcher依赖收集(这里就是我们常写的那个watch)
- render()时,触发- render watcher依赖收集
- 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)
派发更新
- 组件中对响应的数据进行了修改,触发defineReactive中的setter的逻辑
- 然后调用 dep.notify()
- 最后遍历所有的 subs(Watcher 实例),调用每一个watcher的update方法。
set ->
dep.notify() ->
subs[i].update() ->
watcher.run() || queueWatcher(this) ->
watcher.get() || watcher.cb ->
watcher.getter() ->
vm._update() ->
vm.__patch__()
推荐阅读
原文首发地址点这里,欢迎大家关注公众号 「前端南玖」。
我是南玖,我们下一期见!!!
【Vue源码学习】依赖收集的更多相关文章
- 读Vue源码 (依赖收集与派发更新)
		vue的依赖收集是定义在defineReactive方法中,通过Object.defineProperty来设置getter,红字部分主要做依赖收集,先判断了Dep.target如果有的情况会执行红字 ... 
- Vue源码学习1——Vue构造函数
		Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ... 
- Vue源码学习三 ———— Vue构造函数包装
		Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ... 
- Vue源码学习二 ———— Vue原型对象包装
		Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ... 
- 最新 Vue 源码学习笔记
		最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ... 
- 【Vue源码学习】响应式原理探秘
		最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ... 
- Vue 源码学习(1)
		概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ... 
- VUE 源码学习01 源码入口
		VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ... 
- Vue源码学习(一):调试环境搭建
		最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ... 
随机推荐
- 【LeetCode】522. Longest Uncommon Subsequence II 解题报告(Python)
			[LeetCode]522. Longest Uncommon Subsequence II 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemin ... 
- E. Number With The Given Amount Of Divisors
			E. Number With The Given Amount Of Divisors time limit per test 2 seconds memory limit per test 256 ... 
- 海康威视摄像机Java SDK拉流(二)开启关闭实时预览
			本篇介绍海康威视摄像机通过SDK开启关闭实时预览接口 下篇介绍实时预览的回调函数及解码库 测试环境: 系统:Centos 7 SDK:设备网络SDK Linux64 实时预览模块流程: 图中虚线框部分 ... 
- 离线版centos8环境部署迁移监控操作笔记
			嗨咯,前两天总结记录了离线版centos8下docker的部署笔记,今天正好是2021年的最后一天,今天正好坐在本次出差回家的列车上,车上没有上面事做,索性不如把本次离线版centos8环境安装的其他 ... 
- Java初学者作业——定义英雄类(Hero),英雄类中的属性包括:姓名、攻击力、防御力、生命值和魔法值;方法包括:攻击、介绍。
			返回本章节 返回作业目录 需求说明: 定义英雄类(Hero),英雄类中的属性包括:姓名.攻击力.防御力.生命值和魔法值:方法包括:攻击.介绍. 实现思路: 分析类的属性及其变量类型. 分析类的方法及其 ... 
- Java程序设计基础笔记 • 【目录】
			持续更新中- 我的大学笔记>>> 章节 内容 实践练习 Java程序设计基础作业目录(作业笔记) 第1章 Java程序设计基础笔记 • [第1章 初识Java] 第2章 Java程序 ... 
- PowerShell 教程
			随笔分类 - 教程 转载自:https://www.cnblogs.com/XiaoCY/category/1065141.html PowerShell 管道符之Where-Object的使用方法 ... 
- 单篇长文TestNG从入门到精通
			简介 TestNG是Test Next Generation的缩写,它的灵感来自于JUnit和NUnit,在它们基础上增加了很多很牛的功能,比如说: 注解. 多线程,比如所有方法都在各自线程中,一个测 ... 
- Web开发之Servlet
			当一个请求到达服务端,服务器怎么处理? 当一个请求到达服务端时,由服务端的引擎来进行分析.它根据工程名找到工程, 然后拿到URL的资源地址和web.XML文件的所有的进行对比,和哪一个对比上就找到了具 ... 
- GLPK下载安装
			GLPK下载安装 下载 wget http://ftp.gnu.org/gnu/glpk/glpk-4.65.tar.gz tar -zxvf glpk-4.65.tar.gz 安装 如果你有管理员权 ... 
