vue响应式原理解析
原创原文链接 .
什么是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
处理,增加每个属性 get
和 set
方法实现的检测对象的变化。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实现,如果一个新属性添加呢?
- 可以使用Vue.set(location, a, 1)方法将响应属性添加到嵌套对象
- 你也可以为这个对象分配一个新值,如: 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
在这一点上,我们需要通知 test1
及 test2
这两个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更新视图。
版权声明
所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除