vue响应式原理解析 (进阶必备知识)

480 阅读5分钟

这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战

TIP 👉 沉舟侧畔千帆过,病树前头万木春。——《酬乐天扬州》

前言

理解 Vue 响应式原理,我个人比较推荐的方法是结合源码来看。Vue响应式原理,你所需要知道的 首先,各位再熟悉不过的,一定是 Vue 官方提供的这张示意图了:

image.png

我们以这张图为基础,先帮助大家重新捋一遍响应式的机制。在这个基础上,再去做更进一步的分析。注意我们图中有三个关键角色:Watcher、Data、和 Render。

Vue 会对传入的 data 做处理:为每一个属性添加 getter 和 setter。在这个过程中,涉及到了 Object.defineProperty 这个方法。

同时每一个 Vue 组件实例,都对应着一个 watcher 实例;这个 watcher 实例仿佛一个跟踪狂,它的目光永远跟随着 data: 由于 render 函数的执行依赖于数据的读取,因此渲染时必定会读取 data 属性进而触发其对应的 getter 方法。getter 方法被调用后,会通知到 watcher,watcher 就会把这些 getter 方法被触发的属性记录为“依赖”—— 这一过程,就是大家常常听到的“依赖收集”过程。

如果 data 发生了更新,也就是说被“写”了,此时对应属性的 setter 方法就会被触发。setter 也会去通知 watcher, 告诉它“我改变了”。watcher  拿到消息后,立刻跑去告诉  render:“data变了,你也给我跟着变!”。由此去触发一个 re-render 的过程、与数据更新相关的组件会重新渲染。

源码分析

Object.defineProperty

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。它的调用形式如下:

Object.defineProperty(obj, prop, descriptor)

其中第一个入参,是我们操作的目标对象;第二个入参,是我们需要修改的属性的名称;第三个入参,是一个描述符,用来描述你到底要对这个目标属性做什么。

我们在 Vue 响应式原理中涉及到的“描述符”,就是 getter/setter 方法:

getter方法: 一个给属性提供 getter 的方法,实际方法名为“get”;如果没有 getter 则为 undefined 。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this 对象(由于继承关系,这里的this 并不一定是定义该属性的对象)。默认为 undefined

setter方法: 一个给属性提供 setter 的方法,实际方法名为“set”;如果没有 setter 则为 undefined 。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined

ObserverDepWatcher 的关系

在源码层次,大家需要把握好这三个角色:

Observer:处理 data 的家伙。它会给 data 安装 getter 和 setter,这些安装上的逻辑会联动 Dep 去完成依赖收集和更新的派发;

Dep:实际通知 Watcher 的人。在 getter 和 setter 逻辑中,正是通过调度 Dep 来完成信息的收集、以及和Watcher 间的通信;

Watcher:Watcher 被通知之后,就会通知 render、进而触发重渲染了。

Observer

Observer 的作用是遍历所有的属性,给它们安装上 getter/setter 方法:

class Observer { 
    constructor() {
        // 具体逻辑在 observe 函数里
        observe(this.data);
    }
}
function observe (data) {
    // 取出所有的 key
    const keys = Object.keys(data);
    // 遍历所有属性
    for (let i = 0; i < keys.length; i++) {
        // 绑定 getter/setter 方法
        defineReactive(obj, keys[i]);
    }
}

这里我们看到,具体的绑定操作是在 defineReactive 里做的:

function defineReactive (obj, key, val) {
    // 定义一个 Dep 对象,它的作用正如我们上文所说const dep = new Dep();
    Object.defineProperty(obj, key, 
       { enumerable: true, 
       configurable: true,
        get() {
            // 收集依赖、关联到 watcher dep.depend();
            return val;
        },
        set(newVal) {
            if (newVal === val) return;
            // 感知更新、通知 watcher 
            dep.notify();
        }
    });
}

在 defineReactive 里面,每一个 getter/setter 里面都出现了 Dep 实例。正如我们前面所介绍的一样,实际收集信息和通知 watcher 的工作是 Dep 来做的。每一个属性都对应一个单独的 Dep 实例。

在 getter 方法里面,调用了 dep 的 depend 方法,这个方法有什么玄机呢?我们来看看 Dep 的结构:

Dep

Dep 的角色,宛如一个“工具人”,它是 Watcher 和 Observer 之间的纽带,是“通信兵”:

class Dep { 
    constructor () {
        // 存储 Watcher 实例的数组
        this.subs = []
    }
    // 将 watcher 实例添加到 subs 中(这个方法在 Watcher 类的实现里会用到)
    addSub (sub: Watcher) { 
        this.subs.push(sub)
    }
    // 收集依赖
    depend() {
        // Dep.target 实际上就是当前 Dep 对应的 watcher,我们下文会提及
        if (Dep.target) {
            // 把当前的 dep 实例关联到组件对应的 watcher 上去
            Dep.target.addDep(this)
        }
    }
    // 通知 watcher 对象发生更新
    notify () {
        const subs = this.subs.slice()
        // 这里 subs 的元素是 watcher 实例,逐个调用 watcher 实例的 update 方法
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

在 Dep 内部,会维护一个 watcher 队列。

depend 方法在每次 getter 触发时都会把 watcher 实例和 dep 实例做一次关联。

在 setter 触发时,dep 实例便会逐个通知每一个和自己有关联的 watcher:我对应的属性发生了更新!进而调度watcher 实例的 update 方法,实现视图更新。

Watcher

class Watcher {
    constructor() {
        ...
        // Dep 的 target 属性是有赋值过程的^_^,它是组件对应的 watcher 对象
        Dep.target = this
        ...
    }
    addDep (dep: Dep) {
        ...
        // 把当前的 watcher 推入 dep 实例的 watcher 队列(subs)里去
        dep.addSub(this)
        ...
    }
    update() {
        // 更新视图
    }
}

这里需要大家注意一点:宏观上看,咱们说“收集依赖”,是指 watcher 去收集自己所依赖的数据属性;不过从实现上来看,实际上是把 watcher 对象推入了 dep 实例的队列里,更像是 dep 在“收集” watcher。

其实,不管是谁来维护队列、谁“收集”谁,其本质目的都是建立起 dep 和 watcher 间的关联,达到 dep 发生变化后可以立刻通知到 watcher 的目的。