vue响应式原理解析

原创
小哥 2年前 (2023-05-13) 阅读数 43 #大杂烩

原文链接 .

什么是vue响应式

数据更改后,会重新对页面渲染,这就是Vue响应式。

我们需要做什么来完成这个过程吗

  • 检测数据的变化
  • 什么数据集合视图依赖
  • 自动“通知”时需要更新的视图部分数据修改和更新
    相应的专业术语:
  • 数据劫持/数据代理
  • 依赖收集
  • 发布订阅模式

如何检测数据的变化

有两种方法可以检测数据的变化:
只用 Object.defineProperty 和 ES6 的 Proxy 这称为数据劫持或数据代理。

Object.defineProperty实现

Vue 通过设置对象的 setter/getter 方法通过监测数据变化 getter 集合行为依赖,而每个 setter 该方法是一个 观察者 ,在 数据变更 通知的时候 订阅者 更新视图。

的代码如下:

function render () {
    // set去的时候在这里呈现
    console.log(模拟试图呈现)
}

let data = {
    name: 大漠孤烟,
    location: { x: 100, y: 100 }
}

observe(data)

定义核心功能:

function observe (obj) { // 我们用它来让对象可见
    // 类型判断
    if (!obj || typeof obj !== object) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
    function defineReactive (obj, key, value) {
        // 递归子属性
        observe(value)
        Object.defineProperty(obj, key, {
            enumerable: true, // 可列举的(遍历)
            configurable: true, // 可配置(如果删除)
            get: function reactiveGetter () {
                console.log(get, value)
                return value
            },
            set: function reactiveSetter (newVal) {
                observe(newVal) // 如果赋值是一个对象,还递归子属性
                if (newVal !== value) {
                    console.log(set, newVal) // 监听
                    render()
                    value = newVal
                }
            }
        })
    }
}

改变data属性,会触发set,然后获得data将触发的属性get

data.location = {
    x: 1000,
    y: 1000
} // 打印 { x: 1000, y: 1000 } 模拟试图呈现
data.name // 打印get 大漠孤烟

上面的代码的主要目的是:
observe函数接收一个对象 obj(需要跟踪的对象的变化) , 通过遍历所有的属性,每个属性的对象 defineReactive 处理,增加每个属性 getset 方法实现的检测对象的变化。observe递归调用。

那么,我们如何倾听Vue中data数据实际上是非常简单的:

class Vue {
    // Vue构造类
    constructor (options) {
        this._data = options.data
        observe(this._data)
    }
}

所以我们只需要new一个Vue对象,它将data跟踪更改的数据。

但是我们发现一个问题,上面的代码无法检测对象属性 添加或删除 (如data.location.a=1 增加一个a属性)

这是因为Vue通过Object.defineProperty将一个对象的key转化成get/set追踪变化,但是get/set只有一个可以追踪数据 它已经被修改 ,无法跟踪 添加和删除 属性。vm.$delete实现,如果一个新属性添加呢?

  1. 可以使用Vue.set(location, a, 1)方法将响应属性添加到嵌套对象
  2. 你也可以为这个对象分配一个新值,如: data.location = {…data.location, a:1}

Object.defineProperty无法监听数组的变化,需要重写数组的方法

Proxy实现

Proxy是 ES6(ES2015) 的一个特征。Proxy代理是特定于整个对象,而不是一个特定对象的属性,所以它是不同的 Object.defineProperty 必须遍历每一个属性的对象,Proxy您只需要创建一个层代理侦听所有属性在相同的水平结构的变化,但对于深层结构,递归仍然需要做。Proxy支持代理 数组 的变化。

function render () {
    // set去的时候在这里呈现
    console.log(模拟试图更新)
}

let obj = {
    name: 大漠孤烟,
    age: { age: 100 },
    arr: [1, 2, 3]
}

let handler = {
    get (target, key) {
        // 如果该值是一个对象,然后对该对象执行数据劫持
        if (typeof target[key] === object && target[key] !== null) {
            return new Proxy(target[key], handler)
        }
        return Reflect.get(target, key)
    },
    set (target, key, value) {
        // key 为length时,它表明最后属性被遍历
        if (key === length) return true
        render()
        return Reflect.set(target, key, value)
    }
}

let proxy = new Proxy(obj, handler)
proxy.age.name = zhangsan // 支持添加新的属性
console.log(proxy.age.name) // 模拟试图更新 zhangsan
proxy.arr[0] = lisa // 支持改变数组的内容
console.log(proxy.arr) //  ["lisi", 2, 3]

上面的代码不仅简化了,而且实现了一套适用于对象和数组检测的代码。Proxy的兼容性不是很好。

收集依赖

为什么收集依赖关系

为什么我们想要观察数据通知这些地方用当其属性改变的数据。localtion当数据发生变化时,通知应发送到的地方使用它。

let globalData = {
    text: 大漠孤烟
}

let test1 = new Vue({
    template:
    `
{{text}}
`, data: globalData }) let test2 = new Vue({ template: `
{{text}}
`, data: globalData })

如果我们想要执行下列语句

globalData.text = zhangsan

在这一点上,我们需要通知 test1test2 这两个Vue更新视图的一个实例,我们只能使用 依赖收集 只有这样,我才能知道从哪里时依靠我的数据和更新 派发更新 收集。依赖吗? 事件订阅发布模式 。接下来,我们将介绍两个重要的角色 订阅者Dep观察者Watcher 然后解释收集依赖关系是如何实现的。

订阅者Dep

为什么介绍Dep:
收集依赖需要找个地方来存储依赖关系,所以我们已经创建了Dep,用它来 收集依赖删除依赖 以及 发送消息到依赖关系 等。
所以我们首先实现 订阅者Dep 类,用于 解耦 更具体地说,依赖收集和分发更新操作的属性主要是用于存储Watcher观察者对象。Watcher理解作为一个 中介 的作用,数据变更时向它发出通知,然后通知其他地方。

Dep简单的实现:

class Dep {
    constructor () {
        // 用来存放Watcher对象的数组
        this.subs = []
    }
    // 在subs添加一个Watcher对象
    addSub (sub) {
        this.subs.push(sub)
    }
    // 通知所有Watcher对象更新视图
    notify () {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

上面的代码主要做两件事:

  • addSub 该方法可以进一步改善 Dep对象 添加一个在 Watch 订阅操作
  • notify 方法通知当前 Dep对象 的subs中所有Watcher对象触发更新操作。 依赖收集 当调用 addSub ,当 派发更新 是时候打电话 notify

电话也很简单:

let dp = new Dep()
dp.addSub(() => { // 当收集的依赖性
    console.log(emit here)
})
dp.notify() // 当分发更新

观察者Watcher

为什么介绍 Watcher :
Vue定义一个 Watcher类 来表示 观察订阅依赖性 至于为什么它被引入Watcher, 深入理解和易于理解vue.js》 提供一个很好的解释:
当属性改变时,我们需要使用通知数据的地方,还有很多地方使用这些数据,和类型也不同,这可能是 模板 它也可能是由用户编写的 watch 这需要提取的类可以集中处理这些情况。

依赖集的目的是:
将观察者Watcher订阅者的对象存储在当前的闭包Dep的subs中。

Watcher简单的实现:

class Watcher {
    constructor (obj, key, cb) {
        // 将Dep.target指向自己
        // 然后进行属性getter 添加监听
        // 最后将 Dep.target置空
        Dep.target = this
        this.cb = cb
        this.obj = obj
        this.key = key
        this.value = obj[key]
        Dep.target = null
    }
    update () {
        // 获得新值
        this.value = this.obj[this.key]
        // 我们定义了一个 cb 函数,这个函数是用来模拟视图更新,称其代表更新视图
        this.cb(this.value)
    }
}

以上就是Watcher设置一个简单的实现 Dep.target 指向自己,因此收集相应的Watcher当分发更新,检索相应的Watcher然后执行update函数。

依赖的本质:

其实所谓的依赖Watcher。
至于如何收集依赖关系,它可以概括为一句话:
getter 收集的依赖关系 setter 触发的依赖。 收集起来 ,然后等 数据更改 当收集以前收集的依赖关系 循环触发 只做一次。

具体来说,当外面的世界通过Watcher在读取数据时,它改变了回触发getter从而将Watcher添加到依赖,哪一个Watcher触发了getter哪一个将watcher收集到Dep中。当数据更改时,会循环依赖列表,把所有的Wacher通知每个人。

最后,我们有defineReactive 变换函数通过添加依赖收集和调度更新相关代码自定义函数,实现一个简单的数据响应的方法。

function observe (obj) {
    // 类型判断
    if (!obj || typeof obj !== object) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
    function defineReactive (obj, key, value) {
        observe(value) // 递归子属性
        let dp = new Dep() // 新增
        Object.defineProperty(obj, key, {
            enumerable: true, // 可列举的(遍历)
            configurable: true, // 可配置(删除等)。
            get: function reactiveGetter () {
                console.log(get, value) // 监听
                // 将Watcher添加到订阅
                if (Dep.target) {
                    dp.addSub(Dep.target)
                }
                return value
            },
            set: function reactiveSetter (newVal) {
                observe(newVal) // 如果赋值是一个对象,还递归子属性
                if (newVal !== value) {
                    console.log(set, newVal)
                    render()
                    value = newVal
                    // 执行watcher的update方法
                    dp.notify()
                }
            }
        })
    }
}

class Vue {
    constructor (options) {
        this._data = options.data
        observe(this._data)
        // 新建一个Watcher观察者对象,在这一点上Dep.target将指向这Watcher对象
        new Watcher()
        console.log(模拟视图呈现)
    }
}

render function 呈现的时候,阅读所需的对象的值将触发 reactiveGetter 将当前函数Watcher对象(存储在Dep.target)收集到Dep类中去。 reactiveSetter 方法,通知Dep类调用 notify 触发所有Watcher的 update 方法更新相应的视图。

  • 在new Vue()后,Vue会调用_init初始化函数,即init过程,在这个过程中Data通过Observe转换成了getter/setter跟踪数据的形式的变化,并执行它的时候读一组对象getter当分配函数,该函数执行setter函数。

  • 当外面的世界通过Watcher当读取数据时,它将触发getter从而将Watcher添加到依赖。

  • 当修改对象值,相应的setter,setter依赖于收集数据之前通知Dep每一个在Watcher告诉他们,他们的价值观已经改变,他们需要重新绘制视图。Watcher会调用update更新视图。

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除

热门