数据发生变化后,会重新对页面渲染,这就是Vue响应式
它们对应专业俗语分别是:
有两种办法可以侦测到变化:
使用Object.defineProperty
和ES6的Proxy
,这就是进行数据劫持或数据代理。
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
} //打印 set {x: 1000,y: 1000} 模拟视图渲染
data.name //打印 get 浪里行舟
上面这段代码的主要作用在于:
observe这个函数传入一个 obj(需要被追踪变化的对象)
,通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive
处理,给每个属性加上set
和get
方法,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。
那我们如何侦测Vue中data 中的数据,其实也很简单:
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。
但是我们发现一个问题,上面的代码无法检测到对象属性的添加或删除
(如data.location.a=1,增加一个a属性)。
这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改
,无法追踪新增属性和删除
属性。如果是删除属性,我们可以用vm.$delete
实现,那如果是新增属性,该怎么办呢?
Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写
Proxy 是 JavaScript 2015
的一个新特性。Proxy 的代理
是针对整个对象
的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代
理就可以监听同级结构
下的所有属性变化,当然对于深层结构
,递归还是需要进行的。此外Proxy支持代理数组
的变化。
function render() {
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 = '浪里行舟' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化
console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
proxy.arr.length-- // 无效
以上代码不仅精简,而且还是实现一套代码对对象和数组的侦测都适用。不过Proxy兼容性不太好!
我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如例子中,模板中使用了location 数据,当它发生变化时,要向使用了它的地方发送通知。
let globalData = {
text: '浪里行舟'
};
let test1 = new Vue({
template:
`
{{text}}
`,
data: globalData
});
let test2 = new Vue({
template:
`
{{text}}
`,
data: globalData
});
如果我们执行下面这条语句:
globalData.text = '前端工匠';
此时我们需要通知 test1
以及 test2
这两个Vue实例进行视图的更新
,我们只有通过收集依赖
才能知道哪些地方依赖我的数据,以及数据更新时派发更新
。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色-- 订阅者 Dep
和观察者 Watcher
,然后阐述收集依赖的如何实现的。
4.2 订阅者 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
对象中增加一个 Watcher
的订阅操作;
- 用 notify 方法通知目前
Dep
对象的 subs
中的所有 Watcher
对象触发更新操作。
所以当需要依赖收集
的时候调用 addSub,当需要派发更新
的时候调用 notify。
调用也很简单:
let dp = new Dep()
dp.addSub(() => {//依赖收集的时候
console.log('emit here')
})
dp.notify()//派发更新的时候
5 观察者 Watcher
5.1 为什么引入Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖
。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:
当属性
发生变化后,我们要通知用到数据
的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理
这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的是: 将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。
5.2 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中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
最后我们对 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;
observer(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通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
- 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
- 在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。
最后完整的响应式代码:
大概结构
//defineReactive是对Observer的抽离
const defineReactive = function(obj, key) {
// 以下代码省略
}
const Vue = function(options) {
console.log("Vue",this)
//打印1 Vue {
_data:{
text: "123"
get text: ƒ get()
set text: ƒ set(newVal)
},
mount: ƒ (),
render: ƒ ()
}
// 以下代码省略
}
const Watcher = function(vm, fn) {
console.log("Watcher",this)
//打印3 Watcher this是下面的Dep中subs的对象
// 以下代码省略
}
const Dep = function() {
console.log("Dep",this)
//打印2 Dep {
target: null,
subs: [
{ //是一个Watcher实例
subs: Array(1)
0: Watcher
vm: { //是一个Vue实例
_data:{
text: "123",//该属性有了get和set方法
get text: ƒ get(),
set text: ƒ set(newVal)
},
mount: ƒ (),
render: ƒ ()
},
addDep: ƒ (dep),
update: ƒ (),
value: undefined
}
],
depend: ƒ (),
addSub: ƒ (watcher),
notify: ƒ ()
}
// 以下代码省略
}
const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})
vue.mount();
vue._data.text = '123';
详细代码
const Observer = function(data) {
console.log(1) //开始4 new Vue的时候就会执行
// 循环修改为每个属性添加get set
for (let key in data) {
defineReactive(data, key);
}
}
const defineReactive = function(obj, key) {
console.log(2) //开始5 new Vue的时候就会执行
// 局部变量dep,用于get set内部调用
const dep = new Dep();
// 获取当前值
let val = obj[key];
Object.defineProperty(obj, key, {
// 设置当前描述属性为可被循环
enumerable: true,
// 设置当前描述属性可被修改
configurable: true,
get() {
console.log(3)//开始10 开始19
console.log('in get');
// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
dep.depend();
return val;
},
set(newVal) {
console.log(4)//开始15
if (newVal === val) {
return;
}
val = newVal;
// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
// 这里每个需要更新通过什么断定?dep.subs
dep.notify();
}
});
}
const observe = function(data) {
console.log(5) //开始3 new Vue的时候就会执行
return new Observer(data);
}
const Vue = function(options) {
console.log(6)//开始1 new Vue的时候就会执行
const self = this;
// 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
if (options && typeof options.data === 'function') {
console.log(7)//开始2 new Vue的时候就会执行
this._data = options.data.apply(this);
}
// 挂载函数
this.mount = function() {
console.log(8) //开始7 new Vue以后,执行vue.mount()
new Watcher(self, self.render);
}
// 渲染函数
this.render = function() {
console.log(9) //开始9 开始18 render函数执行后走到这里
with(self) {
_data.text; //这里取data值的时候,就会走get方法
}
}
// 监听this._data
observe(this._data); //new Vue的时候就会执行,这里执行完,就表示new Vue的过程执行完了
}
const Watcher = function(vm, fn) {
console.log(10) //开始8 执行vue.mount()以后会走到这里
const self = this;
this.vm = vm;
// 将当前Dep.target指向自己
Dep.target = this;
// 向Dep方法添加当前Wathcer
this.addDep = function(dep) {
console.log(11) //开始13
dep.addSub(self);
}
// 更新方法,用于触发vm._render
this.update = function() {
console.log(12)//开始17
console.log('in watcher update');
fn();
}
// 这里会首次调用vm._render,从而触发text的get
// 从而将当前的Wathcer与Dep关联起来
this.value = fn(); //开始9 fn是render函数,这里fn()就会赋值的时候执行
// 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
// 造成代码死循环
Dep.target = null;
}
const Dep = function() {
console.log(13) //开始6 new Vue的时候就会执行到new Dep,然后执行到这里
const self = this;
// 收集目标
this.target = null;
// 存储收集器中需要通知的Watcher
this.subs = [];
// 当有目标时,绑定Dep与Wathcer的关系
this.depend = function() {
console.log(14) //开始11 开始20 走了get获取属性后,就要进行依赖收集
if (Dep.target) {
console.log(15)//开始12
// 这里其实可以直接写self.addSub(Dep.target),
// 没有这么写因为想还原源码的过程。
Dep.target.addDep(self);
}
}
// 为当前收集器添加Watcher
this.addSub = function(watcher) {
console.log(16)//开始14
self.subs.push(watcher);
}
// 通知收集器中所的所有Wathcer,调用其update方法
this.notify = function() {
console.log(17) //开始16
for (let i = 0; i < self.subs.length; i += 1) {
self.subs[i].update();
}
}
}
const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})
vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get
解析:
-
- 一开始new Vue ,会走到46行执行Vue构造函数,打印6
-
- 然后46行Vue的入参options实际上是127行的入参{data(){}},是一个包含了data函数的对象,所以options.data是一个data函数,打印7。将vue中的data函数返回的数据赋值给_data。
-
- 然后走到67行的observe,会继续往上走到41行定义它的地方。
-
- 然后43行 new Observer 的时候会走到第一行
Observer(关键函数)
,打印1。我们发现Observer实际就是给data数据都添加上get和set
方法,只不过不添加的方法defineReactive给抽离出去了。
-
- 然后走到第9行,执行defineReactive,打印2,然后15行给每个
属性
加上get和set
方法。
-
- 然后走到12行,new Dep的时候,会走到95行执行Dep,打印13。Dep函数剩下的代码都只是定义函数,都不会执行,会跳出Dep函数。然后会到defineReactive函数第13行,defineReactive剩下的代码中的函数也不会执行,所以会回到Observer,再回到67行,即
new Vue的过程走完了
。
-
- 然后走到135行的vue.mount(),走到56行,打印8。
-
- 然后执行new Watcher走到70行,打印10。
-
- 72行到88行只是定义,没有执行。89行this.value = fn()中:fn实际是传进来的render函数(看57行),然后后面又加了()就会立即执行。然后走到60行的render函数,打印9。
Watcher就执行完了
,然后vue.mount()也执行完了
。
-
- 接着会走到136行,
vue._data.text = '123'
,注意:这里的vue._data
是获取,后面的=才是改变值,所以会先走get,然后再走set。所以会走到21行,打印10。
-
- 然后走到25行,执行dep.depend(),再走到104行,打印14。
-
- 这时候判断Dep.target,这时候是存在的,所以打印15。
-
- 然后走到110行,再跳到77行,打印11。
-
- 79行执行后会跳到114行,打印16。
-
- 然后就是赋值操作了,这时候会走到28行的set,打印4。
-
- 继续向下走,到36行,dep.notify(),然后走到119行,打印17。
-
- 然后会走到122行,触发update,走到82行,打印12。
-
- 然后执行fn(),即render函数,走到60行,打印9。
-
- 然后走到63行,取data值,会走get,走到21,打印3。
-
- 然后25行,会跳到104行,打印14。Dep.target为null,15不会打印
参考链接