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

发布时间 2023-10-08 19:41:58作者: 养肥胖虎

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

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

 

完整代码已开源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帮我盯着这个数据,
当数据发生改变,自动更新视图
实现"自动更新"
(关于vm._updata()的实现,Vue源码学习(五):<templete>渲染第四步,生成虚拟dom并将其转换为真实dom)
 
 

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 的响应式系统的重要环节之一,它保证了当响应式对象的属性被访问时能够建立起数据与观察者之间的关联关系,从而实现了数据的动态更新。