Vue响应式原理

148 阅读14分钟

defineReactive

为什么

为什么要定义一个defineReactive函数?原因是JS原生的defineProperty不好用。

如下代码所示:直接对obj.a赋值是没有意义的,因为getter一直返回的是7。

let obj = {}
Object.defineProperty(obj, 'a', {
    get() {
        console.log('你试图访问obj的a属性');
        return 7 //等于不管怎么样,得到的obj.a一直是7
    },
    set(n) {
        console.log('你试图改变obj的a属性', n);
    }
})

console.log(obj.a);
obj.a = 9 //赋值无效,因为getter一直返回的是7
console.log(obj.a);

为了让obj.a=9生效,需要一个临时变量周转getter和setter

let obj = {}
let temp
Object.defineProperty(obj, 'a', {
    get() {
        console.log('你试图访问obj的a属性');
        return temp //等于不管怎么样,得到的obj.a一直是7
    },
    set(n) {
        console.log('你试图改变obj的a属性', n);
        temp = n
    }
})
console.log(obj.a);
obj.a = 9 //赋值无效,因为getter一直返回的是7
console.log(obj.a);

为了使代码更加简洁清晰、模块化,将上述代码封装为一个闭包环境defineReactive,这样可以不用单独设置一个temp临时变量,闭包中的val变量可以起到同样的效果,因为闭包中访问到父级函数的变量不会被销毁。

关于闭包的知识需要参考此文:juejin.cn/post/739870…

let obj = {}

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可配置
        configurable: true,
        // getter
        get() {
            console.log('你试图访问obj的' + key + '属性', val);
            return val
        },
        // setter
        set(newValue) {
            console.log('你改变obj的' + key + '属性', newValue);
            if (val === newValue) return
            val = newValue
        }
    })
}

defineReactive(obj, 'a', 7)
obj.a = 9
console.log(obj.a);
defineReactive(obj, 'b', 11)
obj.b = 33
console.log(obj.b);

递归侦测对象全部属性

上一节写的defineReactive函数有缺陷,它没有识别.语法的能力,即不会遍历侦测对象属性

// 目前没有识别.语法的能力,即不会遍历侦测对象属性
defineRactive(obj, 'a.m.n', 5)

console.log(obj.a.m.n);

所以这一节,我们就需要解决这个问题

首先,先将defineReactive函数抽离为一个文件 defineReactive.js

export default function defineReactive(data, key, val) {
    if (arguments.length == 2) {
        val = data[key]
    }
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可配置
        configurable: true,
        // getter
        get() {
            console.log('你试图访问obj的' + key + '属性', val);
            return val
        },
        // setter
        set(newValue) {
            console.log('你改变obj的' + key + '属性', newValue);
            if (val === newValue) return
            val = newValue
        }
    })
}

main.js使用

import defineReactive from './defineReactive'
let obj = {}
// 目前没有识别.语法的能力,即不会遍历侦测对象属性
defineReactive(obj, 'a.m.n', 5)

console.log(obj.a.m.n);

同时新增一个observer类,这个类的作用是将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的object)

新建一个Observer.js文件

export default class Observer {
    constructor() {

    }
}

然后在main.js引入,并且创建一个新函数observe,注意函数的名字没有r,这个函数起到辅助判别的作用

import defineReactive from './defineReactive'
import Observer from './Observer';
let obj = {}

// 创建Observe函数,注意函数的名字没有r,这个函数起到辅助判别的作用
function observe(value) {
    // 如果value不是对象什么都不做,这个函数只为对象服务
    if (typeof value !== 'object') return
    // 定义ob
    let ob
    if (typeof value.__ob__ !== 'undefined') {
        // __ob__ 用来存储一个Observer实例
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob
}

目前我已经很混乱了,上面这些是什么鬼东西!!!为了帮助理解,总的来说,递归侦测对象的流程图是这个

image.png

按照这个流程图我们继续开发

Observer.js

import { def } from './utils.js'

export default class Observer {
    constructor(value) {
        console.log('我是Observer构造器', value);
        // 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
        // 加了__ob__属性,值是这次new的实例
        // __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
        def(value, '__ob__', this, false)
        console.log('我是Observer构造器', value);
    }
}

注意上面的代码:

  • __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__

为了方便定义__ob__属性,创建了一个def函数

utils.js

export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    })
}

完成了上述准备工作,开始整体完成递归侦测对象属性,总而言之,实现递归侦测对象属性的递归策略是:

  • observe(obj)将一个对象作为参数传入observe方法
  • 检测这个obj有没有__ob__属性,如果没有需要new Observer一个Observer实例,添加到这个obj的__ob__属性上.
  • 在Observer构造函数会执行这个Observer类的walk方法,在这个walk方法中遍历obj的属性,然后使用defineReactive函数将这些属性全部转化为响应式属性
  • 并且在defineReactive方法中也会observe每个属性,从而实现递归、深层次将对象的属性都转换为响应式的。

具体代码:

observe.js

import Observer from './Observer'
// 创建Observe函数,注意函数的名字没有r,这个函数起到辅助判别的作用
export default function observe(value) {
    // 如果value不是对象什么都不做,这个函数只为对象服务
    if (typeof value !== 'object') return
    // 定义ob
    let ob
    if (typeof value.__ob__ !== 'undefined') {
        // __ob__ 用来存储一个Observer实例
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob
}

utils.js

export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    })
}

Observer.js

import { def } from './utils.js'
import defineReactive from './defineReactive.js'

export default class Observer {
    constructor(value) {
        // 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
        // 加了__ob__属性,值是这次new的实例
        // __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
        def(value, '__ob__', this, false)
        console.log('我是Observer构造器', value);
        // 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
        this.walk(value)
    }
    // 遍历
    walk(value) {
        console.log('walk---value', value);
        for (let key in value) {
            defineReactive(value, key)
        }
    }
}

defineReactive.js

import observe from './observe'
export default function defineReactive(data, key, val) {
    console.log('我是defineReactive', key);
    if (arguments.length == 2) {
        val = data[key]
    }

    // 子元素要进行observe,至此形成了递归,这个递归不是函数自己调用自己,而是多个函数、类循环调用
    let childOb = observe(val)
    console.log('childOb', childOb);
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可配置
        configurable: true,
        // getter
        get() {
            console.log('你试图访问' + key + '属性', val);
            return val
        },
        // setter
        set(newValue) {
            console.log('你改变' + key + '属性', newValue);
            if (val === newValue) return
            val = newValue
            // 当设置了新值,这个新值也需要转换为响应式的
            childOb = observe(newValue)
        }
    })
}

main.js

import observe from './observe';
let obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10,
    c: {
        d: {
            e: {
                f: 22
            }
        }
    }
}
observe(obj)

数组响应式原理

目前的代码依然不完善,当obj有一个属性是数组时,你会发现defineReactiveget可以触发,但是set不会触发。

为什么不完善

对于数组,直接使用 Object.defineProperty 是不够的,原因如下:

  • 数组的索引和 length 属性:数组的索引和 length 属性是特殊的,不能通过 Object.defineProperty 来劫持。例如,直接修改 arr[0] 或 arr.length 不会触发 getter 和 setter。
  • 数组方法的原生行为:数组的原生方法(如 pushpop 等)在修改数组时不会触发依赖更新。这是因为这些方法内部并没有调用属性的 getter 和 setter。

如何实现数组响应式原理

改写数组的七个方法

要想改写这七个方法,首先要对JS原型有特别清醒的认知。

首先,这七个方法都挂载在Array.prototype上

大致操作流程图如下:

image.png

把数组的原型指向arrayMethods

array.js

import { def } from './utils'

//得到Array.prototype
const arrayPrototype = Array.prototype

// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype)


const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

methodsNeedChange.forEach(methodName => {
    // 备份原来的方法,因为push等7个函数的功能不能被剥夺
    const original = arrayPrototype[methodName]
    // 定义新的方法
    def(arrayMethods, methodName, function () {
        // 恢复原来的功能
        original.apply(this, arguments)
    }, false)

})

然后observer构造函数增加对value值是否是数组的判断,是的话就将value,也就是数组的原型指向我们自己造的arrayMethods

// 检查它是数组还是对象
        if (Array.isArray(value)) {
            // 如果是数组,要非常强行的将这个数组的原型指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods)
        } else {
            this.walk(value)
        }

完整代码: Observer.js

import { def } from './utils.js'
import defineReactive from './defineReactive.js'
import { arrayMethods } from './array.js'
console.log('arrayMethods', arrayMethods);
export default class Observer {
    constructor(value) {
        // 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
        // 加了__ob__属性,值是这次new的实例
        // __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
        def(value, '__ob__', this, false)
        console.log('我是Observer构造器', value);
        // 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
        // 检查它是数组还是对象
        if (Array.isArray(value)) {
            // 如果是数组,要非常强行的将这个数组的原型指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods)
        } else {
            this.walk(value)
        }
    }
    // 遍历
    walk(value) {
        for (let key in value) {
            defineReactive(value, key)
        }
    }
}

让数组变得observe(可侦测的)

接下来我们要想办法让这个数组变得observe,首先我们需要对这个数组进行特殊的遍历

import { def } from './utils.js'
import defineReactive from './defineReactive.js'
import { arrayMethods } from './array.js'
import observe from './observe.js'

export default class Observer {
    constructor(value) 
        def(value, '__ob__', this, false)
        if (Array.isArray(value)) {
            Object.setPrototypeOf(value, arrayMethods)
            // 让这个数组变得observe
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }
    // 遍历
    walk(value) {
        for (let key in value) {
            defineReactive(value, key)
        }
    }
    // 数组的特殊遍历
    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            // 逐项进行observe
            observe(arr[i])
        }
    }
}

因为splice、push、unshift这三个方法比较特殊,会往数组内部插入项,但是新插入的项也得让它observe。为此我们需要在array.js方法也使用我们新增的observeArray方法

import { def } from './utils'

//得到Array.prototype
const arrayPrototype = Array.prototype

// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype)


const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

methodsNeedChange.forEach(methodName => {
    // 备份原来的方法,因为push等7个函数的功能不能被剥夺
    const original = arrayPrototype[methodName]
    // 定义新的方法
    def(arrayMethods, methodName, function () {
        // 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么被添加了?因为数组肯定不是最高层,第一次遍历obj对象的第一层的时候,已经给这个数组属性添加了__ob__属性
        const ob = this.__ob__
        // 把类数组对象变为数组
        const args = [...arguments]
        // 有三种方法push/unshift/splice能够插入新项,现在要把插入的新项也要变为observe的
        let inserted = []
        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                // splice格式是splice(下标,数量,插入的新项)
                inserted = args.slice(2)
                break
        }
        // 判断有没有要插入的新项,让新项也变为响应的
        if (inserted) {
            ob.observeArray(inserted)
        }
        // 恢复原来的功能
        const result = original.apply(this, arguments)
        return result
    }, false)

})

这样数组响应式就基本完成了,但是目前还不完善,是个低配版,只能通过调用那7个数组方法才能变为响应式。通过数组下标array[2]=1这样修改还不行

依赖收集

什么是依赖

用到数据的地方就是依赖,比如vue2组件的data属性就是依赖。

在getter中收集依赖,在setter中触发依赖

Dep、Watcher、Observer、数据属性之间的关系

响应式原理示意图如下:

image.png

结合代码辅以理解Watcher和Dep是什么:

<template>
    <div>
      <p>{{ message }}</p>
      <p>{{ computedMessage }}</p>
    </div>
  </template>
  
  <script>
  export default {
    data() {
      return {
        message: 'Hello, Vue!'
      };
    },
    computed: {
      computedMessage() {
        return this.message + ' (Computed)';
      }
    }
  };
  </script>

在这个例子中:

  1. message 属性:

    • message 属性有一个唯一的 Dep 实例与之关联。
  2. Watcher 实例:

    • 模板中的 {{ message }} 会创建一个 Watcher 实例来监听 message 属性的变化。
    • 计算属性 computedMessage 也会创建一个 Watcher 实例来监听 message 属性的变化。
    • 因此,message 属性的 Dep 实例会包含两个 Watcher 实例:一个用于模板中的 {{ message }},另一个用于计算属性 computedMessage

总结

  • 每个数据属性有一个 Dep 实例:负责收集所有依赖于该数据属性的 Watcher 实例。
  • 每个依赖关系有一个 Watcher 实例:负责监听数据属性的变化并执行相应的更新逻辑。
  • 一个数据属性可以有多个 Watcher 实例:因为多个组件或计算属性可能依赖于同一个数据属性。

这种设计使得 Vue 的响应式系统能够高效地管理和更新视图,确保数据变化时能够准确地触发相关的更新操作。

所以我们得出了结论,也就是数据属性、watcher、observer、dep这四者之间的关系:

  • Observer 使得数据对象中的每个属性都能被监听,当这些属性被访问或修改时,可以触发相应的行为。
  • Dep 收集所有依赖于特定属性的 Watcher,这样当属性发生变化时,Dep 可以通知所有相关的 Watcher
  • Watcher 监听数据的变化,并在变化时执行相应的更新逻辑,比如更新 DOM 或重新渲染组件。

注意事项:

  • 依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
  • Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有Watcher都通知一遍
  • 代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中。

按照上文的依赖收集逻辑,先实例化Dep

新增Dep.js

export default class Dep {
    constructor() {
        console.log('我是Dep类的构造器');
    }
    notify() {
        console.log('我是notify方法');
    }
}

新增Watcher.js

export default class Watcher {
    constructor() {
        console.log('我是Watcher类的构造器');
    }
}

给Observer类的构造函数添加Dep属性

image.png

defineReactive.js

image.png

setter中触发依赖

image.png

为什么Observer与defineReactive都实例化了一个dep

  1. Observer 类的作用:

    • Observer 类的主要职责是将一个对象的所有属性变成响应式的。它会遍历对象的所有属性,并调用 defineReactive 方法来定义这些属性的 getter 和 setter。
    • Observer 类中的 dep 实例通常用于管理整个对象的依赖,但它并不是直接用来管理具体属性的依赖。
  2. defineReactive 的作用:

    • defineReactive 方法用于将对象的单个属性变成响应式的。它会在每个属性上定义 getter 和 setter,并为每个属性创建一个独立的 Dep 实例来管理该属性的依赖。
    • defineReactive 确保每个属性都有自己的依赖收集和通知机制,从而实现细粒度的依赖管理。

比如在这个例子中

import observe from './observe';
let obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10,
    c: {
        d: {
            e: {
                f: 22
            }
        }
    },
    g: [1, 2, 3, 4, 5]
}
observe(obj)

obj.a.m.n = 88
obj.g.push(100)

console.log('obj', obj);
  • obj.a.m:

    • Observer 会为 obj.a.m 创建一个 Dep 实例,管理 m 对象的依赖。
    • defineReactive 会为 m 对象的 n 属性创建一个独立的 Dep 实例,管理 n 属性的依赖。
  • obj.a.m.n:

    • defineReactive 会为 n 属性创建一个独立的 Dep 实例,管理 n 属性的依赖。

Watcher类和Dep类的具体代码

继续来处理Watcher和Dep的具体代码,目前要做的就是什么时候把Watcher放到Dep当中。

具体代码,我就不太注重研究了,因为面试的时候不可能问得那么细

Dep.js

let uid = 0
export default class Dep {
    constructor() {
        console.log('我是Dep类的构造器');
        this.id = uid++
        // 用数组存储自己的订阅者
        // 这个数组里面放的是Watcher的实例
        this.subs = []
    }
    // 添加订阅
    addSub(sub) {
        this.subs.push(sub)
    }
    // 添加依赖
    depend() {
        // Dep.target就是当前正在被watcher观察的watcher对象
        if (Dep.target) {
            // 把watcher观察者对象push到dep中
            this.addSub(Dep.target)
        }
    }
    // 通知更新
    notify() {
        console.log('我是notify方法');
        //浅克隆一份
        const subs = this.subs.slice()
        // 遍历
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

watcher.js

import Dep from './Dep'

let uid = 0
export default class Watcher {
    constructor(target, experssion, callback) {
        console.log('我是Watcher类的构造器');
        this.id = uid++
        this.target = target
        this.getter = parsePath(experssion)
        this.callback = callback
        this.value = this.get()
    }
    update() {
        this.run()
    }
    get() {
        // 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
        Dep.target = this
        const obj = this.target
        let value
        // 只要能找,就一直找
        try {
            value = this.getter(obj)
        } finally {
            Dep.target = null
        }
        return value
    }
    run() {
        this.getAndInvoke(this.callback)
    }
    getAndInvoke(cb) {
        const value = this.get()

        if (value !== this.value || typeof value == 'object') {
            const oldValue = this.value
            this.value = value
            cb.call(this.target, value, oldValue)
        }
    }
}
function parsePath(str) {
    let segments = str.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return
            obj = obj[segments[i]]
        }
        return obj
    }
}

defineReactive.js

image.png

main.js使用Watcher image.png

结语

目前我春招复习的Vue响应式原理就告一段落了,本文的知识积累足以解决面试。

具体源码:github.com/LanQing0817…