此文档是个人根据vue源码以及网上的资料和个人理解,由一个Demo由浅入深来对vue的响应式原理一探究竟,达到深刻理解vue响应式原理的目的,若有不对的地方望大神指正。
套用一句经典名言:任何伟大都源于一个简单的开始!我们就从一个简单demo开始:
假设现在有2个变量:
let price = 10
let count = 2
然后我们需要得到一个总数:
let total = price * count
问题:price或者count变了,需要实现total跟着变化
思考:total跟着变化,也就是total的求值过程需要重新再执行一遍,只要将整个求值过程封装成一个方法再执行一遍就好,再加上观察者的设计模式,代码是这样的:
let price = 10
let count = 2
let total = 0
let target = null
let subscribers = []
function record() {
subscribers.push(target)
}
function notify() {
subscribers.forEach((target) => target())
}
target = () => {
total = price * count
}
record() // 存储
target() // 计算求值
console.log('total:', total) // => 20
price = 20 //修改
console.log('total:', total) // => 20
notify() // 通知,重新计算求值
console.log('total:', total) // => 40
上面的依赖搜集(record方法)和通知更新(notify方法)比较零散,如果有多个变量需要搜集依赖,不方便扩展,所以需要专门封装一个类来集中管理依赖关系,Dep(Dependency)出现了:
// 封装一个Dep(Dependency)类用于专门处理依赖搜集和触发更新
class Dep {
constructor() {
this.subscribers = []
}
depend() {
if(target && !this.subscribers.includes(target)){
this.subscribers.push(target)
}
}
notify(){
this.subscribers.forEach(target => target())
}
}
let dep = new Dep()
let price = 10
let count = 2
let total = 0
let target = () => {
total = price * count
}
dep.depend() //搜集依赖
target() // 计算求值
console.log('total:', total) // => 20
price = 20 //修改
console.log('total:', total) // => 20
dep.notify() // 通知,重新计算求值
console.log('total:', total) // => 40
接着上面的例子,如果有多个变量,计算求值对应多个方法,那么定义一个target方法显示是不够的,我们接着封装一个Watcher 类,用于包装计算求值这个观察者函数:
// 封装一个Dep(Dependency)类用于专门处理依赖搜集和触发更新
class Dep {
constructor() {
this.subscribers = []
}
depend() {
if (target && !this.subscribers.includes(target)) {
this.subscribers.push(target)
}
}
notify() {
this.subscribers.forEach(target => target())
}
}
let target = null
// 封装一个Watcher类,用来包装观察者函数
class Watcher {
constructor(func) {
target = func
dep.depend() //搜集依赖
target() // 计算求值
target = null
}
}
let price = 10
let count = 2
let total = 0
let dep = new Dep()
let watcher = new Watcher(() => {
total = price * count
})
console.log('total:', total) // => 20
price = 20 //修改
console.log('total:', total) // => 20
dep.notify() // 通知,重新计算求值
console.log('total:', total) // => 40
这里有个疑问是为什么要把target设置为全局变量,为什么不直接作为参数传入depend方法,我们接着优化,答案就在后面。
到目前为止,我们并没有实现真正意义上的响应式,在变量发生变化后,自动更新依赖,上面我们需要主动调用notify()方法通知更新。那么在JavaScript中我们如果侦测一个对象的变化?目前主要有2种方式实现:1、ES5中使用Object.definePropertyAPI,将对象的属性转为==getter/setter== 2、ES6的Proxys
鉴于vue2是用Object.defineProperty实现,我们先用Object.defineProperty来接着优化,添加一个observer方法,用来专门将需要观察的数据对象转换成getter/setter:
// 封装一个Dep(Dependency)类用于专门处理依赖搜集和触发更新
class Dep {
constructor() {
this.subscribers = []
}
depend() {
if (target && !this.subscribers.includes(target)) {
this.subscribers.push(target)
}
}
notify() {
this.subscribers.forEach(target => target())
}
}
let target = null
// 封装一个Watcher类,用来包装观察者函数
class Watcher {
constructor(func) {
target = func
target() // 计算求值,触发getter,从而搜集依赖
target = null
}
}
function observer(data) {
Object.keys(data).forEach((key) => {
let value = data[key]
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend()
return value
},
set(newVal) {
value = newVal
dep.notify()
}
})
})
}
let total = 0
let data = {
price: 10,
count: 2,
}
observer(data) // 将data转换为getter/setter
// 传入包装函数生成一个Watcher实例
let watcher = new Watcher(() => {
total = data.price * data.count
})
console.log('total:', total) // => 20
data.price = 20 // 触发setter,从而调用notify方法重置技术求值
console.log('total:', total) // => 40
这里的代码和上面的代码的区别是:
1、将dep实例由全局挪入observer()方法,这样每一个需要观察的数据对象都对应一个dep实例,将dep.depend()方法的调用由Watcher构造函数挪入get()方法里,在getter中搜集依赖。
2、在setter中调用dep.notify()方法,从而触发更新。
这里也间接回答了上面target的问题,dep.depend()方法调用挪入getter里,Dep和Watcher完全解耦,我们需要一个全局变量来保存依赖所对应的包装函数。
上面的代码有一个问题,就是只侦测了一个数据对象(data),如果有多个数据对象,并且还要侦测所有子属性,很显然一个方法是不合理的,这时需要封装一个Observer类,并且封装成类在后面的数组的响应式还有大用处,这里先卖个关子,封装后的代码:
function isObject(obj){
return obj !== null && typeof obj === 'object'
}
class Dep {
// static target = null
constructor() {
this.subscribers = []
}
addSub(sub) {
this.subscribers.push(sub)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
this.subscribers.forEach(sub => {
sub.update()
})
}
}
Dep.target = null //静态属性
class Watcher {
constructor(func) {
this.getter = func
this.value = this.get()
}
get() {
Dep.target = this;
this.getter() // 触发getter并且添加依赖,因为target已存在
Dep.target = null
}
addDep(dep) {
dep.addSub(this)
}
update() {
this.getter() // 模拟视图更新
}
}
/**
* Observer类会附件到每一个被侦测的object上。
* 一旦被附件,Observer会将所有的属性转换为getter/setters
* 来搜集依赖和触发更新
* --来自官方源码的注释
*/
class Observer {
constructor(value){
this.value = value
if(!Array.isArray(value)){
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
});
}
}
function observe(value) {
if (!isObject(value)) return
new Observer(value)
}
function defineReactive(obj, key, value) {
observe(value) // 递归,对象的值也有可能是对象
let dep = new Dep()
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: function () {
if (Dep.target) {
dep.depend() // 搜集依赖
}
return value
},
set: function (newval) {
if (newval !== value) {
observe(newval) // 新设置的值有可能是对象
value = newval
dep.notify() // 触发更新
}
}
})
}
class Vue {
constructor(data) {
new Observer(data)
new Watcher(render)
}
}
let data = {
price: 10,
count: 2,
}
function render() {
let total = data.price * data.count // 触发getter
console.log('total:', total)
}
new Vue(data) //入口
data.price = 20 // => 40 // 触发更新
data.count = 3 // => 60 // 触发更新
上面的代码加入了Observer类和Vue类,Vue类只是为了模拟vue应用响应式系统的全过程,通过创建一个Vue实例开始,传入一个数据对象(data),将data对象的所有属性添加到响应式系统中,当属性的值发生变化时,视图将会更新(重新调用render方法)。由于这里着重讲响应式,所以render所对应的虚拟DOM、模板渲染等这里暂不涉及,只是简单的打印数据。
上面代码的重点是加入了Observer类,很多小伙伴也许开始会跟我一样,觉得Observer类和observe方法并没有什么区别嘛,observe方法也就是生成Observer类的实例而已啊,感觉这是脱掉裤子放屁--多此一举嘛,其实不然,存在即合理,我们接着往下看:
上面的代码只实现了对象的响应式,对于数组并没有添加侦测,那要怎样实现呢?
按照上面的套路,响应式主要分为依赖搜集和数据侦测,在getter中搜集依赖,在setter中触发更新。那么问题来了,数组getter中搜集依赖,那怎么触发更新?
思考一下,数组的变化主要是通过各种数组的操作(会改变原数组的操作)来实现,只要拦截这些方法,加上触发更新就OK了,具体实现为劫持数组的原型,在原型链上进行增强操作(更新依赖),对于一些插入操作,还需要对插入的值添加到响应式系统,最后对于需要观察的数组,将原型设置为劫持的新的原型对象。
嗯,一切都很完美,准备撸代码了!不过等等,对象的依赖搜集和触发更新是在同一个函数里面(defineReactive),可以操作同一个Dep实例,数组的依赖搜集和触发更新在不同的地方,触发更新是在拦截器里面,那又怎么解决呢?
再次思考一下,getter里和拦截器里我们都能获取到当前操作的值,getter里将搜集的依赖dep绑定到值上面,在拦截器里面再获取然后触发更新就解决了,代码如下:
// 工具函数:判断是否是对象
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
class Dep {
// static target = null
constructor() {
this.subscribers = []
}
addSub(sub) {
this.subscribers.push(sub)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
this.subscribers.forEach(sub => {
sub.update()
})
}
}
Dep.target = null //静态属性
class Watcher {
constructor(func) {
this.getter = func
this.value = this.get()
}
get() {
Dep.target = this;
this.getter() // 触发getter并且添加依赖,因为target已存在
Dep.target = null
}
addDep(dep) {
dep.addSub(this)
}
update() {
this.getter() // 模拟视图更新
}
}
//处理数组的变化侦测
let arrayProto = Array.prototype
let arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => {
const original = arrayProto[method]
// 官方用的是def工具函数去定义
arrayMethods[method] = function (...args) {
const result = original.apply(this, args) // 调用原先的方法
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2)
break;
}
if (inserted) observeArray(args) // 新增的数据也要侦测每一项
this.__dep__.notify() // 触发更新
return result
}
})
/**
* Observer类会附件到每一个被侦测的object上。
* 一旦被附件,Observer会将所有的属性转换为getter/setters
* 来搜集依赖和触发更新
* --来自官方源码的注释
*/
class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
// Object.setPrototypeOf(value, arrayMethods) // ES6 API
observeArray(value)
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
});
}
}
// 侦测数组的每一项
function observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
function observe(value) {
if (!isObject(value)) return
new Observer(value)
}
function defineReactive(obj, key, value) {
observe(value) // 递归,对象的值也有可能是对象,返回一个子对象所对应的Observer实例
let dep = new Dep()
value.__dep__ = dep // 将dep保存起来,用于数组更新方法里面触发更新
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: function () {
if (Dep.target) {
dep.depend() // 搜集依赖
}
return value
},
set: function (newval) {
if (newval !== value) {
observe(newval) // 新设置的值有可能是对象
value = newval
dep.notify() // 触发更新
}
}
})
}
class Vue {
constructor(data) {
new Observer(data)
// 一个组件可能会有多个watcher实例
new Watcher(render) // 组件渲染过程中将数据记录为依赖
new Watcher(sum) //这里模拟计算属性,computed(计算属性)或者 watch 也会将数据记录为依赖
}
}
let data = {
price: 10,
count: 2,
list: []
}
function render() {
let total = data.price * data.count // 触发getter
console.log('total:', total)
}
function sum() {
let sum = 0
data.list.forEach(item => sum += item) // 触发getter
console.log('sum:', sum)
}
new Vue(data) // 模拟Vue实例化,入口
data.price = 20 // 触发更新
data.list.push(1, 2, 3) // 触发更新
上面的代码有3处重点:
1、创建数组拦截器,劫持数组原型,并进行增强操作
let arrayMethods = Object.create(arrayProto);
.
.
.
this.__dep__.notify() // 触发更新
2、设置原形
value.__proto__ = arrayMethods
将当前响应式数据的原形设置为上面的拦截器arrayMethods
这里要多嘴提一句,我们知道__proto__是非标准属性,虽然大部分浏览器都默默支持了,但是也有一部分浏览器是不支持的,对于不支持__proto__设置原形的要怎么处理呢?vue2源码里面是调用了copyAugment方法,其实就是暴力的将拦截器里面拦截的方法挂载到当前值value上,巧妙的运用了js的特性:访问对象的属性时,会先从当前的对象去找,如果能找到就直接调用当前对象的方法,找不到时才去原形上找。
3、绑定依赖
value.__dep__ = dep
在当前值添加__dep__属性,将依赖dep绑定,用于在拦截器中使用
以上是我个人的实现,但Vue2源码里面==绑定依赖==这块实现的更灵活更优雅,我们看看源码里面是怎么实现的:
// 工具函数:判断是否是对象
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
// 工具函数:通过Object.defineProperty给对象设置属性
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
// 工具函数:对象是是否有某个非继承的key
function hasOwnKey(target, key) {
return target.hasOwnProperty(key)
}
class Dep {
// static target = null
constructor() {
this.subscribers = []
}
addSub(sub) {
this.subscribers.push(sub)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
this.subscribers.forEach(sub => {
sub.update()
})
}
}
Dep.target = null //静态属性
class Watcher {
constructor(func) {
this.getter = func
this.value = this.get()
}
get() {
Dep.target = this;
this.getter() // 触发getter并且添加依赖,因为target已存在
Dep.target = null
}
addDep(dep) {
dep.addSub(this)
}
update() {
this.getter() // 模拟视图更新
}
}
//处理数组的响应
let arrayProto = Array.prototype
let arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => {
const original = arrayProto[method]
// 官方用的是def工具函数去定义
arrayMethods[method] = function (...args) {
const result = original.apply(this, args) // 调用原先的方法
let inserted
const ob = this.__ob__ // 在Observer类中定义
switch (method) {
case 'push':
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2)
break;
}
if (inserted) ob.observeArray(args) // 新增的数据也要侦测每一项
ob.dep.notify() // 触发更新
return result
}
})
/**
* Observer类会附件到每一个被侦测的object上。
* 一旦被附件,Observer会将所有的属性转换为getter/setters
* 来搜集依赖和触发更新
* --来自官方源码的注释
*/
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
// 将__ob__属性添加到当前实例上,用于判断当前数据是否已转换为响应式数据和数组触发更新
def(value, '__ob__', this)
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
// Object.setPrototypeOf(value, arrayMethods) es6API
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
});
}
// 侦测数组的每一项
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function observe(value) {
if (!isObject(value)) return
let ob
if (hasOwnKey(value, '__ob__')) { // 如果已经是响应式数据
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
function defineReactive(obj, key, value) {
let childOb = observe(value) // 递归,对象的值也有可能是对象,返回一个子对象所对应的Observer实例
let dep = new Dep()
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: function () {
if (Dep.target) {
dep.depend() // 搜集依赖
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set: function (newval) {
if (newval !== value) {
observe(newval) // 新设置的值有可能是对象
value = newval
dep.notify() // 触发更新
}
}
})
}
class Vue {
constructor(data) {
new Observer(data)
// 一个组件可能会有多个watcher实例
new Watcher(render) // 组件渲染过程中将数据记录为依赖
new Watcher(sum) //这里模拟计算属性,computed(计算属性)或者 watch 也会将数据记录为依赖
}
}
let data = {
price: 10,
count: 2,
list: []
}
function render() {
let total = data.price * data.count // 触发getter
console.log('total:', total)
}
function sum() {
let sum = 0
data.list.forEach(item => sum += item) // 触发getter
console.log('sum:', sum)
}
new Vue(data) // 模拟Vue实例化,入口
data.price = 20 // 触发更新
data.list.push(1, 2, 3) // 触发更新
上面的代码也有3处重点:
1、将依赖dep绑定到Observer实例属性上,并将当前实例绑定到value值的__ob__属性上
class Observer {
constructor(value) {
this.dep = new Dep()
// 将__ob__属性添加到当前实例上,用于判断当前数据是否已转换为响应式数据和数组触发更新
def(value, '__ob__', this)
}
}
我们上面的做法是直接将dep绑定到vulue上,这里的做法是将dep绑定当前Observer实例上,而把实例又绑定到vulue上,这样的做法好处多多,首先我们可以共享Observer实例方法,比如observeArray方法,在拦截器里面可以直接调用,而不用将此方法定义为全局方法;其次,通过__ob__属性,我们可以判断当前value是否已经转换为响应式。所以这种做法更灵活,也更容易扩展。
2、在依赖搜集时,将上面value值所对应的dep也搜集为依赖
function defineReactive(obj, key, value) {
let childOb = observe(value) // 递归,对象的值也有可能是对象,返回一个子对象所对应的Observer实例
...
Object.defineProperty(obj, key, {
...
get: function () {
if (Dep.target) {
dep.depend() // 搜集依赖
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set: function (newval) {
...
}
})
}
function observe(value) {
if (!isObject(value)) return
let ob
if (hasOwnKey(value, '__ob__')) { // 如果已经是响应式数据
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
上面我们在实例上添加了dep依赖,在getter里收集这个依赖,然后在拦截器里面去触发更新时才会生效,childOb.dep.depend()就是为数组而生的!
3、触发更新时,通过当前值value值的__ob__属性找到所对应的Observer实例,并找到实例属性dep来更新
arrayMethods[method] = function (...args) {
...
const ob = this.__ob__ // 在Observer类中定义
ob.dep.notify() // 触发更新
...
}
嗯,上面做了那么多铺垫,就为了这一刻,终于大功告成!
上面我们有提到为什么一定要定义Observer类,这里也一目了然,我们将Dep类实例与Observer类实例紧密结合,灵活运用,让代码更易扩展,也更优雅。
到目前为止,一个较为完整响应式系统已经完成了,当然这里只是较为简单的实现,真实的vue.js源码里面远比这复杂,考虑的问题更多,代码更健壮,但是万变不离其宗,我们主要还是学习这个框架响应式的思路。
在此顺便附上vue官网的响应式架构图,对应Vue的响应式系统更加一目了然:
目前为止虽然功能实现了,但是总有些不完美:1、只能侦测对象属性的改变,对于添加/删除属性无法监测,数组length改变或某一项值的改变也无法侦测;2、在将对象属性转换为getter/setter时进行了遍历和递归,如果嵌套的很深,则会影响性能。
对于问题1,Vue添加了set和delete方法,专门用来处理这些情况的影响式:
function set(target, key, val) {
// ...
// 前面会进行一系列的检查判断
// ...
const ob = target.__ob__ // __ob__属性对应的是数据对象的Observer实例对象,
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val) // 将传入的对象转换为getter/setter从而添加到响应式系统中
ob.dep.notify() // 触发更新
return val
}
function del(target, key) {
// ...
// 前面会进行一系列的检查判断
// ...
const ob = target.__ob__ // __ob__属性对应的是数据对象的Observer实例对象,
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
对于问题2,ES5似乎没有完美的解决办法,而使用 ES6的Proxy则完美解决了以上2个问题,Proxy代理是针对整个对象,而不是对象的某个属性,只要代理了对象,就可以监听同级结构下的所有属性的变化,包括添加/删除新属性,当然对于深层结构,递归在所难免,另外Proxy也完美支持代理数组,甚至函数,我们先拿Proxy来小试牛刀:
let handler = {
get(target, key, receiver){
console.log('触发了getter:', key);
if(typeof target[key] === 'object' && target[key] !== null){
return new Proxy(target[key], handler)
}
return Reflect.get(target, key, receiver);
},
set(target, key, value){
console.log('触发了setter:',key+ ' - '+value);
return Reflect.set(target, key, value)
}
}
let data = {
list: []
}
let proxy = new Proxy(data, handler)
proxy.list[0] = 1 // 设置数组某一项会触发setter
proxy.list.push(2) // push()会触发setter
proxy.info = {name:'test'} // 新增一个info属性,也会触发setter
可以看到不管是数组操作,还是新增属性等,无需特殊处理,全部都能监听到,下面我们就来看看Vue3中的基于Proxy的响应式系统
vue2中,响应式系统主要分为依赖收集和数据侦测,vue3也是一样。研究Vue3的响应式之前,我们先了解一下Vue3创建响应式数据的方法,为了兼容Vue2,Vue3保留了data选项的用法,不过我们主要来研究新增的reactive API和effact API,Vue3的响应式架构如下图:
Vue3中通过调用reactive方法将数据转换成一个可观察的Proxy代理对象,通过effact方法实现依赖收集,在数据发生变化后调用传入回调函数,用法大致如下:
import {reactive, effect} from 'vue'
const state = reactive({
name: 'hello'
})
effect(() => {
console.log(state.name); // => 打印出hello
})
state.name = 'world' // => 再次打印出hello
可以看到核心就是reactive和effect这两个方法,一个用来数据侦测,一个用来依赖收集和响应,下面我们就来看看Vue3响应式具体怎样实现的,先从reactive数据侦测说起,上面我们已经见识了Proxy的威力,现在就来继续优化:
var toProxy = new WeakMap()
var toRaw = new WeakMap()
function isObject(obj){
return obj !== null && typeof obj === 'object'
}
function hasOwnKey(target, key){
return target.hasOwnProperty(key)
}
function reactive(target){
return createReactiveObject(target)
}
function createReactiveObject(target){
if(!isObject(target)) return
if(toProxy.has(target)) return toProxy.get(target)
if(toRaw.has(target)) return target
let baseHandler = {
get(target, key, receiver){
console.log(key,'获取');
let result = Reflect.get(target, key, receiver)
if(isObject(result)){
return reactive(result)
}
else{
return result
}
},
set(target, key, value, receiver){
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if(!hasOwnKey(target, key)){
console.log(key,'新增设置');
}
else if(oldValue !== value){
console.log(key,'更改设置');
}
return result
},
deleteProperty(target, key){
return Reflect.deleteProperty(target, key)
}
}
let observed = new Proxy(target, baseHandler)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
var data = {
name: 'hello',
list:[1,2],
info: {
age: 0
}
}
var state = reactive(data)
// state = reactive(data) // => 代码正常运行,因为有toProxy的判断
// state = reactive(state) // => 代码正常运行,因为有toRaw的判断
state.name = 'world'
state.list.push(3)
state.info.age = 18
上面的代码主要做了这几件事情:
1、创建一个代理对象。
2、get时判断value是否是对象,如果是需要递归。上面例子中,如果get中没有递归判断,那么list.push和info.age都无法正常运行,因为我们只代理了一级,后面的操作无法检测!这里有人也许会有疑问,上面不是说Vue2的缺点是递归么,那你Vue3也有递归啊?要说明的是,Vue2的递归,是程序一进入就开始递归,不管你数据有没有用上,如果数据复杂层级结构深会对性能有一定影响,而Vue3的递归是在用的时候(get)才会对当前的对象递归,性能更优化!
3、set时需要处理数组push方法产生set多次的情况(一次是值个改变,一次是length的改变),需要明确的是,只有数据发生变化我们才做响应,如果是新增key那肯定是发生了变化;如果key已存在则判断新值和旧值是否相等。
4、 判断数据对象被重复代理,和代理被重复代理的情况。这里用到了WeakMap,他的特点是对象可以作为key,并且对象是弱引用的,也就是一旦对象被删除,那么这里的引用不会影响垃圾回收机制,用WeakMap完美的实现了我们的需求!
实现了reactive,我们接着来看看effect,effect接受一个函数参数fn,默认会执行一次fn,并且在数据发生变化后再次自动执行,我们来看看怎么实现:
var toProxy = new WeakMap()
var toRaw = new WeakMap()
var activeEffectStacks = []
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
function hasOwnKey(target, key) {
return target.hasOwnProperty(key)
}
function reactive(target) {
return createReactiveObject(target)
}
function createReactiveObject(target) {
if (!isObject(target)) return
if (toProxy.has(target)) return toProxy.get(target)
if (toRaw.has(target)) return target
let baseHandler = {
get(target, key, receiver) {
console.log(key, '获取');
track(target, key) // 收集依赖
let result = Reflect.get(target, key, receiver)
if (isObject(result)) {
return reactive(result)
}
else {
return result
}
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (!hasOwnKey(target, key)) {
console.log(key, '新增设置');
trigger(target, key) // 触发更新
}
else if (oldValue !== value) {
console.log(key, '更改设置');
trigger(target, key) // 触发更新
}
return result
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key)
}
}
let observed = new Proxy(target, baseHandler)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
// 跟踪依赖,创建如下数据格式用来保存依赖
// {
// target: {
// key: [fn]
// }
// }
var targetsMap = new WeakMap()
function track(target, key) {
let effect = activeEffectStacks[activeEffectStacks.length - 1]
if (!effect) return
let depsMap = targetsMap.get(target)
if (!depsMap) {
targetsMap.set(target, depsMap = new Map())
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, deps = new Set())
}
if (!deps.has(effect)) {
deps.add(effect)
}
}
// 触发依赖
function trigger(target, key) {
let depsMap = targetsMap.get(target) // 取出target所对应的Map: {key:[fn,fn]}
if (depsMap) {
let deps = depsMap.get(key) // 取出 key所对应的set:[fn,fn]
if (deps) {
deps.forEach(effect => { // 遍历set取出fn并执行
effect()
})
}
}
}
// 执行fn并将fn存入栈,在数据使用时(get)将数据和fn对应起来
function effect(fn) {
const effect = createReactiveEffect(fn)
effect()
}
function createReactiveEffect(fn) {
const effect = function () {
run(effect, fn)
}
return effect
}
function run(effect, fn) {
if (!activeEffectStacks.includes(effect)) {
try {
activeEffectStacks.push(effect)
return fn()
} finally {
activeEffectStacks.pop()
}
}
}
let data = {
name: '111',
age: 12,
list: [1, 2]
}
let state = reactive(data)
effect(() => {
console.log('name:', state.name);
})
effect(() => {
console.log('age:', state.age);
})
state.name = '222'
//obj.list.push(3)
这里的代码和上面比起来,多了effect、createReactiveEffect、run、track、trigger这几个函数,前3个函数其实主要就做了一件事:执行fn并将fn存起来,后面的track函数通过多层Map和set将数据和fn对应起来,trigger则是反向操作取出数据对应的fn遍历执行。
可以看到Vue3比起Vue2,代码更直观明了,Vue2用了3个类来实现,而Vue3则用起ES6的Proxy、Map、Set等API轻松实现,而且天生解决了Vue2中那些令人苦恼的问题,不得不说这些API简直好用到飞起啊。。。
到此为止,Vue2和Vue3的响应式就理完了,会不会有一种豁然开朗的感觉?整个过程下来,让我对Vue也有了更深的认识,它的设计**和很多技巧让我受益匪浅,用框架就要去读懂它,既可以让我们在开发时少犯错误,又可以提升自己,何乐而不为?
写在最后:
1、上面的代码都已放入GitHub对应的js文件里:https://github.com/littleyan-xu/vue-reactive
2、强烈建议安装VsCode插件Code Runner来直接运行上面的代码