Vue源码学习(九):响应式前置:实现对象的依赖收集(dep和watcher)

vue,源码,学习,响应,前置,实现,对象,依赖,收集,dep,watcher · 浏览次数 : 5

小编点评

## Vue响应式系统中的依赖收集 **1. Vue响应式系统的核心** 响应式系统通过利用 Watcher 对象来建立数据与视图之间的依赖关系。当数据发生变化时,会触发相应的 Watcher 进行更新操作,从而实现视图的重新渲染。 **2. Dep依赖收集** Dep 是一个用来管理数据依赖关系的数组。当我们访问一个响应式对象的属性时,会触发属性的 getter 方法。在这个方法中,会通过 Dep.target 去判断当前正在进行依赖收集的 Watcher。如果存在 Dep.target,说明当前正在进行依赖收集的操作,那么就将这个 Watcher 添加到相关的依赖列表中。当依赖列表中收集完所有的 Watcher 后,清空 Dep.target,将其置为 null,表示依赖收集完成。 **3.依赖管理** Dep 实例通过 subs 数组维护了一组观察者(Watcher)的引用。当数据发生变化时,可以快速找到需要更新的观察者,从而实现数据与视图的同步更新。 **4.收集依赖** 通过收集依赖,我们可以确保当数据被访问时,能够准确地找到需要更新的观察者,从而实现数据与视图的同步更新。收集依赖的过程是自动进行的,无需手动调用,由 Vue 内部的响应式系统自动管理。 **5.示例** ```js // 获取属性的 getter方法 function getAttribute(data) { return data.dep.target; } // 获取依赖的 Watcher function getWatcher() { return this.dep.target; } // 数据发生变化时,触发依赖收集 data.dep.target = getAttribute(data); // 更新视图的 watcher this.getWatcher(); ``` **6.总结** 依赖收集是实现 Vue 的响应式系统的重要环节之一,它保证了当响应式对象的属性被访问时能够建立起数据与观察者之间的关联关系,从而实现了数据的动态更新。

正文

好家伙,这是目前为止最绕的一章,也是十分抽象的一章

由于实在太过抽象,我只能用一个不那么抽象的实例去说服我自己

 

完整代码已开源https://github.com/Fattiger4399/analytic-vue.git

1.我们要做什么?

来看这个例子,

index.html

setTimeout(() => {
            vm.msg = "张三"
            vm._updata(vm._render())
        },2000)

 

在这个例子中,我们能够看到hello在msg的值变化后,渲染的值也发生了改变

 

现在,我们要做的事情是

setTimeout(() => {
            vm.msg = "张三"
            // vm._updata(vm._render())
        },2000)
vm._updata(vm._render())将这行代码去掉,也能实现相同的效果
原本我要手动更新,现在我引入一个监视者watcher帮我盯着这个数据,
当数据发生改变,自动更新视图
实现"自动更新"
 
 

2.代码实现

代码已开源https://github.com/Fattiger4399/analytic-vue.git

项目:

 (红框为本篇涉及到的文件)

 

2.1.dep.js

class Dep{
    constructor(){
        this.subs = []
    }
    //收集watcher
    depend(){
        this.subs.push(Dep.target)
    }
    //更新watcher
    notify(){
        this.subs.forEach(watcher =>{
            watcher.updata()
        })
    }
}

//添加watcher
Dep.target = null
export function pushTarget(watcher){
    Dep.target = watcher
}
export function popTarget(){
    Dep.target = null
}
export default Dep

Dep 类中有 subs 数组,用于存放依赖(即 Watcher 对象)。

depend() 方法用于收集依赖,将当前的 Watcher 对象添加到 subs 数组中。

notify() 方法用于更新依赖,遍历 subs 数组,调用各个 Watcher 对象的 update() 方法

 

2.2.index.js中数据劫持部分

//对对象中的属性进行劫持
function defineReactive(data, key, value) {
    observer(value) //深度代理
    let dep = new Dep() //给每一个对象添加dep
    Object.defineProperty(data, key, {
        get() {
            // console.log('获取')
            if(Dep.target){
                dep.depend()
            }
            console.log(dep)
            return value
        },
        set(newValue) {
            // console.log('设置')
            if (newValue == value) {
                return;
            }
            observer(newValue)
            value = newValue
            dep.notify()
        }
    })

}

 

2.3.watcher.js

//(1) 通过这个类watcher 实现更新
import { pushTarget,popTarget } from "./dep"
let id = 0
class watcher {
    //cb表示回调函数,options表示标识
    constructor(vm, updataComponent, cb, options) {
        //(1)将
        this.vm = vm
        this.exprOrfn = updataComponent
        this.cb = cb
        this.options = options
        this.id = id++
        //判断
        if (typeof updataComponent === 'function') {
            this.getter = updataComponent
        }
        //更新视图
        this.get()
    }
    //初次渲染
    get() {
        console.log(this,'||this is this')
        pushTarget(this) //给dep 添加  watcher
        this.getter()
        popTarget() //给dep 去除 watcher
    }
    //更新
    updata() {
        this.getter()
    }
}

export default watcher

//收集依赖 vue dep watcher data:{name,msg}
//dep:dep 和 data 中的属性是一一对应
//watcher:监视的数据有多少个,就对应有多少个watcher
//dep与watcher: 一对多 dep.name = [w1,w2]


//实现对象的收集依赖

初次渲染时,调用 get() 方法,会先调用 pushTarget() 方法将当前 Watcher 添加到 Dep 中,

然后调用 getter() 方法进行渲染,最后调用 popTarget() 方法去除该 Watcher。

而在更新时,直接调用 update() 方法,也会调用 getter() 方法进行更新。

 

2.4.lifecycle.js

 

import {
    patch
} from "./vnode/patch"

import watcher from "./observe/watcher"

export function mounetComponent(vm, el) {
    //源码
    callHook(vm, "beforeMounted")
    //(1)vm._render() 将 render函数变成vnode
    //(2)vm.updata()将vnode变成真实dom
    let updataComponent = () => {
        vm._updata(vm._render())
    }
    new watcher(vm, updataComponent,()=>{},true)
    callHook(vm, "mounted")
}

export function lifecycleMixin(Vue) {
    Vue.prototype._updata = function (vnode) {
        // console.log(vnode)
        let vm = this
        //两个参数 ()
        vm.$el = patch(vm.$el, vnode)
        // console.log(vm.$el, "||this is vm.$el")
    }
}

//(1) render()函数 =>vnode =>真实dom 

//生命周期调用
export function callHook(vm, hook) {
    // console.log(vm.options,"||this is vm.options")
    // console.log(hook,"||this is hook")
    // console.log(vm.$options,"||this is vm.$options")
    const handlers = vm.$options[hook]
    if (handlers) {
        for (let i = 0; i < handlers.length; i++) {
            handlers[i].call(this) //改变生命周期中的指向 
        }
    }
}

在挂载方法中新建watcher实例

 

 

于是,到这里,我们已经实现了与上述等去掉vm._updata(vm._render())前的效果

 

 

接下来是个人的一些思考

3.一些思考

3.1.vue的响应式原理和dep依赖收集的关系?

Vue 的响应式原理是通过利用 Object.defineProperty() 方法拦截对象的访问和修改,从而实现对数据的观测。

Vue 在初始化时会遍历 data 对象的属性,为每个属性创建一个 Dep(依赖)对象。

Dep 对象主要负责管理依赖收集和派发更新。

当读取数据时,会触发该属性的 getter 函数,收集依赖。

Dep 对象会存储当前正在执行的 Watcher(观察者)对象,将其添加到自身的 subs(订阅者)数组中。

在修改数据时,会触发该属性的 setter 函数,通知 Dep 对象进行依赖的派发。

Dep 对象会遍历 subs 数组,调用每个 Watcher 对象的 update() 方法,进而触发 Watcher 的更新操作。

Watcher 是 Vue 响应式系统的核心,它负责建立响应式数据与视图之间的关系。

在渲染过程中,Vue 会将模板中涉及到的数据对应的 Watcher 实例化并添加到 Dep 的 subs 数组中。

当数据发生变化,会触发相应的 Watcher 进行更新操作,从而实现视图的重新渲染。(所谓的订阅发布)

 

3.2.Dep作用:

 

  1. 收集依赖:Dep 实例内部有一个 subs 数组,用于存储依赖(Watcher)的引用。当一个观察者(Watcher)初始化时,会通过调用对应数据的 getter 方法,触发依赖收集的过程。在这个过程中,Dep.target(当前正在访问的观察者)会被添加到 Dep 实例的 subs 数组中。这样就建立了数据与观察者之间的依赖关系。

  2. 触发更新:当被观察的数据发生变化时,它的 setter 方法会通知 Dep 实例。Dep 实例会遍历 subs 数组,依次调用每个依赖(Watcher)的 update 方法,通知观察者进行更新操作。

  3. 依赖管理:Dep 实例通过 subs 数组维护了一组观察者(Watcher)的引用。当数据发生变化时,可以快速找到需要更新的观察者,避免了不必要的触发和更新。

  4. 多重依赖管理:在 Vue 中,一个观察者(Watcher)可以依赖多个数据,一个数据也可以被多个观察者依赖。通过 Dep 实例和 subs 数组的组合,可以实现对多个数据和观察者的管理和通知。

Dep 依赖收集的作用就是在 getter 函数中将 Watcher 对象添加到自己的 subs 数组中,而 Watcher 通过 update() 方法来触发视图的更新,从而保持数据与视图的同步。

 

3.3.为什么要收集依赖?

收集依赖的目的是为了建立起数据与视图之间的关联关系,

当数据发生变化时,能够准确地知道需要更新哪些视图。

在 Vue 中,当数据对象的某个属性被访问时,会触发该属性的 getter 函数,并在这个时候收集依赖。

这个依赖就是 Watcher 对象,它会被添加到 Dep 对象的 subs 数组中。这样在数据发生变化时,就可以遍历 subs 数组,依次调用每个 Watcher 对象的 update() 方法来更新对应的视图。

收集依赖的好处有以下几点:

  1. 减少不必要的更新:只有当数据被访问时,才会触发依赖收集的过程。如果数据没有被使用,那么就不会收集对应的依赖。这样可以避免不必要的视图更新,提升性能。

  2. 精确追踪依赖关系:通过依赖收集,可以准确地确定哪些数据被使用,以及被使用的地方。当某个数据发生变化时,只需要更新和这个数据相关联的视图,而不是所有的视图。

  3. 动态的更新依赖关系:如果在数据使用过程中有新的 Watcher 注册进来,依赖收集可以动态地将这个新的 Watcher 添加到对应的依赖中。这样可以保证当数据变化时,新的 Watcher 也能够得到通知。

总之,通过收集依赖,可以确保数据与视图之间的同步更新,提高程序的效率和可靠性。

 

3.4.什么是收集依赖

收集依赖是一个在观察者模式中常见的概念,用于建立起观察者和被观察者之间的关联关系

在 Vue.js 中的响应式系统中,当我们访问一个响应式对象的属性时,会触发属性的 getter 方法。而在 getter 方法中,会有一个收集依赖的过程。

具体来说,收集依赖的过程如下:

  1. 创建一个 Watcher 对象,用于表示当前正在进行数据观察的实例。

  2. 在创建 Watcher 对象之前,会将这个 Watcher 对象先设置为“活动”的状态,即将其赋值给一个特定的变量 Dep.target。

  3. 在 getter 方法中,会通过 Dep.target 去判断当前正在进行依赖收集的 Watcher。如果存在 Dep.target,说明当前正在进行依赖收集的操作,那么就将这个 Watcher 添加到相关的依赖列表中。

  4. 当依赖列表中收集完所有的 Watcher 后,清空 Dep.target,将其置为 null,表示依赖收集完成。

     

通过收集依赖,我们可以在数据发生变化时,快速找到需要更新的观察者,从而实现数据与视图的同步更新。收集依赖的过程是自动进行的,无需手动调用,由 Vue 内部的响应式系统自动管理。

收集依赖是实现 Vue 的响应式系统的重要环节之一,它保证了当响应式对象的属性被访问时能够建立起数据与观察者之间的关联关系,从而实现了数据的动态更新。

 

与Vue源码学习(九):响应式前置:实现对象的依赖收集(dep和watcher)相似的内容:

Vue源码学习(九):响应式前置:实现对象的依赖收集(dep和watcher)

好家伙,这是目前为止最绕的一章,也是十分抽象的一章 由于实在太过抽象,我只能用一个不那么抽象的实例去说服我自己 完整代码已开源https://github.com/Fattiger4399/analytic-vue.git 1.我们要做什么? 来看这个例子, index.html setTimeou

Vue源码学习(十一):计算属性computed初步学习

好家伙, 1.Computed实现原理 if (opts.computed) { initComputed(vm,opts.computed); } function initComputed(vm, computed) { // 存放计算属性的watcher const watchers = vm

Vue源码学习(一):数据劫持(对象类型)

好家伙,了解一下Vue如何实现数据劫持 1.Vue中data的使用 首先,我得搞清楚这玩意的概念,我们先从vue的使用开始吧 想想看,我们平时是如何使用vue的data部分的? 无非是这两种情况 (你可千万不要带着惊讶的表情说"啊!原来有两种写法的吗") //函数写法 data() { return

Vue源码学习(二):渲染第一步,模板解析

好家伙, 1.去哪了 在正式内容之前,我们来思考一个问题, 当我们使用vue开发页面时,中的内容是如何变成我们网页中的内容的? 它会经历四步: 解析模板:Vue会解析中的内容,识别出其中的指令、插值表达式({{}}),以及其他元素和属性。

Vue源码学习(三):渲染第二步,创建ast语法树

好家伙,书接上回 在上一篇Vue源码学习(二):渲染第一步,模板解析中,我们完成了模板解析 现在我们继续,将模板解析的转换为ast语法树 1.前情提要 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我

Vue源码学习(四):渲染第三步,将ast语法树转换为渲染函数

好家伙, Vue源码学习(三):渲染第二步,创建ast语法树, 在上一篇,我们已经成功将 我们的模板 转换为ast语法树 接下来我们继续进行操作 1.方法封装 由于代码太多,为了增加代码的可阅读性 我们先将代码进行封装 index.js import { generate } f

Vue源码学习(六):(支线)渲染函数中with(),call()的使用以及一些思考

好家伙, 昨天,在学习vue源码的过程中,看到了这个玩意 嘶,看不太懂,研究一下 1.上下文 这段出现vue模板编译的虚拟node部分 export function renderMixin(Vue) { Vue.prototype._c = function () { //创建标签 return

Vue源码学习(五):渲染第四步,生成虚拟dom并将其转换为真实dom

好家伙, 前情提要: 在上一篇我们已经成功将ast语法树转换为渲染函数 现在我们继续 1.项目目录 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我解释给你听一万遍 新增文件:vnode/index.js vnode/p

Vue源码学习(七):合并生命周期(混入Vue.Mixin)

好家伙, 1.使用场景 现在来,来想一下,作为一个使用Vue的开发者,假设现在我们要使用created(),我们会如何使用 1.1. .vue文件中使用 {{ message }}

Vue源码学习(十):关于dep和watcher使用的一些思考

好家伙, 前面想了好久,都没想明白为什么要dep和watcher打配合才能实现数据-视图同步 为什么要多一个依赖管理这样的东西 给每个数据绑个watcher(xxfunction),然后,数据变了,调set,然后调xxfunction,不就行了, 然后今天突然想明白了,不是为什么要这么干,而是必须这