头像

zjian

Just Code!

帖子照片墙关于

Vue2响应式原理

2023-11-03 14:39

介绍

首先,Vue2 的核心原理实际上就是使用 Object.defineProperty 这个 API 实现的。基本的原理大概是,使用 Object.defineProperty 劫持数据的 get 和 set 方法,在 get 方法上收集依赖,在 set 方法上做响应式处理。

那么,知道了这个,那么 Vue 的实现是怎么样的呢?

首先,Vue 会分为几个类来处理响应式的:Observer、Watcher、Depend

Observer

Observer 主要是用来监听数据变动的,Vue 在 initdata 的时候,对于 data 对象,是调用 new Observer(data) 实现的

然后 Observer 的构造函数,会去遍历 data 的所有属性,然后调用 defineReactive()(函数实现就是用 Object.defineProperty)给每一个属性都做 get 和 set 的劫持

Watcher

Watcher 其实是一个中间的类,用于连接数据变动和组件的关系类。是依赖收集要收集的实例,当我们修改数据后,也是通过 Watcher 去通知组件刷新的。

Depend

Depend 类主要存放依赖,即存放 Watcher 的实例。

有什么作用呢? 想一下,我们劫持了 data 的 get 和 set,当修改 data 的值的时候,会自动触发 set 方法,是不是应该去通知 Watcher 去做更新操作,那通知那几个 watcher 呢?这就是 Depend 主要做的。

关系

初始化一个 data 对象,会去构造成一个 Observer 实例, 然后 data 对象的每个属性,都会通过 defineReactive() 去劫持 get 和 set,然后,每个劫持到的 set,都能访问到“依赖”,即 Depend,以便在 set 的时候能通知依赖列表去做 update。所以 Depend 实例应该在 defineReactive 函数闭包里。

// init data
function initData(vm) {
    let data = vm.$options.data;
    ...

    new Observer(value);
}

// 给data对象的属性都做劫持
function defineReactive(obj, key) {
    // 每个属性都有它的依赖列表,存在该函数闭包里。
    const dep = new Dep()
    ...

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            // 依赖收集
            ...
        },
        set: function reactiveSetter (newVal) {
            ...
            // 通知依赖更新
            dep.notify()
        }
    })
}

class Observer {
    value: any;
    dep: Dep;
    constructor (value: any) {
        ...

        const keys = Object.keys(obj)
        // 所有属性都添加劫持
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
  }
}

依赖收集

理清楚上述后,我们继续,怎么收集这个依赖列表呢?

我们很容易想到,当用到这个数据的时候(触发了 get 函数的时候),自然就是这个属性的依赖了,所以我们可以在 get 的时候收集依赖。

那么我需要给依赖记录什么内容,上面我们说过,Watcher 是中间桥梁的作用,数据变动更新 Watcher 更新,所以很显然,我们记录依赖,记录 Watcher 就可以了。

因为用到的时候去收集依赖,那很显然第一个问题就是什么时候数据第一次用到?

很容易想到,页面渲染上去的时候是第一次用到数据的,因为要拿初始数据展示出来,然后我们能找到这样一段代码:

// 这是组件执行挂载的函数
function mountComponent () {
    // 调用beforeMount 生命周期函数
    callHook(vm, 'beforeMount')
    ...
    let updateComponent;
    ...
    updateComponent = () => {
        // 渲染vnode操作。
        vm._update(vm._render(), hydrating)
    }
    // 没有直接执行挂载,而是调用new Watcher执行,
    new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */)
    // 调用mounted 生命周期函数
    callHook(vm, 'mounted')
}

class Watcher {
    vm: Component;
    getter: Function;
    value: any;
    ...
    // 构造函数,注意第二个参数
    constructor(
        vm: Component,
        expOrFn: string | Function,
        ...
    ){
        this.vm = vm;
        ...
        if (typeof expOrFn === "function") {
            // 把函数赋值给getter。
            this.getter = expOrFn;
        }
        this.value = this.get();
    }
    get() {
        // 让 Depend.target = this (重要步骤!!)
        pushTarget(this);
        let value;
        const vm = this.vm;
        try {
            // 执行一次getter函数,这里的getter,实际就是上面组件挂载的时候的updateComponent函数
            value = this.getter.call(vm, vm);
        } catch (e) {
            ...
        }
        return value;
    }
}

// 给data对象的属性都做劫持
function defineReactive(obj, key) {
    // 每个属性都有它的依赖列表,存在该函数闭包里。
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            // 依赖收集
            if (Dep.target) {
                dep.depend()  // 实际就是dep.subs.push(Depend.target)
            }
            ...
            // 返回值
            return value
        },
        set: function reactiveSetter (newVal) {
            ...
            // 通知依赖更新
            dep.notify()
        }
    })
}

这里,挂载组件的时候,Vue 没有直接调用 updateComponent 函数。而是 new 一个 Watcher 实例,然后 Watcher 的构造函数里面会去调用实例的 get 方法。

然后 get 方法第一步是把 Depend.target 赋值为 this, 这里的 Depend.target 相当于一个全局变量,主要是借助这个全局变量把这个 watcher 实例传给依赖收集器(属性劫持的 get 方法)。

设置好全局变量后,执行一次 getter 函数,我们知道 getter 函数实际就是 updateComponent 函数,这个函数会根据模板生成 vnode 并渲染上页面上。

所以,执行 updateComponent 的时候,自然会触发到 data 对象属性的 get 方法,所以 get 方法把 Depend.target 传来的 Watcher 实例添加到依赖上去,这样依赖就收集完了。

总结

总的来说,围绕 Object.defineProperty 可以分为依赖收集,和依赖的响应,跨越 Watcher、Depend、Observer 类;

依赖收集:

组件挂载 → new Watcher → 把 Watcher 实例放到 Depend.target 上 → 执行一次渲染函数 → 自动触发 data 的属性劫持 get 函数 → 把 Depend.target 存到闭包的 Depend 实例上 → 完成收集

依赖响应:

我们执行 this.xxx = xxx → 触发 data 的属性劫持 set 函数 → 调用收集到的依赖(watcher 实例)的 update 函数 → update 函数会调用 run 方法,run 方法会执行一次 updateComponent 函数 → 完成更新

当然,展示的代码删除掉很多干扰的,实际上,不止在渲染页面用到了 Watcher,比如还有我们写的 watch、computed 属性,不过原理差不多,就不多介绍了。

还有就是实际看源码会发现,实际 Watcher 里面也有依赖 deps,也有 newDeps,newDepIds,实际的作用主要是防止重复依赖,和自动的清理没用的依赖。