前言
响应式大家都应该很收悉吧,国内很火的vue框架就是采用的响应式。要想熟练的掌握vue原理,响应式是必须首先掌握的。大家可能对响应式之前是有过了解,但是不太清楚具体内部是怎实现的,这篇文章将一步一步的将响应式实现出来。
用到的主要知识点:
class类、Object.defineproperty、proxy、reflect、map、weakmap、set。
如果有不清楚这些知识点的可以先了解一下。
一、什么是响应式
简单来说,响应式就是当某一个变量发生改变的时候,会对应自动的执行一些代码。
下面我们来看一个例子:
const obj = {
name:'cj',
age:18
}
//obj对象改变需要执行的代码
function getName(){
console.log('发现obj对象的某个属性改变了,我要开始执行了')
return newName = obj.name
}
//对obj对象属性重新赋值
obj.name = 'wx'
getName()
当我们对obj对象属性重新赋值的时候,就会自动执行getName函数,这样的函数我们称为响应函数。
二、收集响应函数的封装
当变量发生改变的时候,怎么去识别哪些函数是需要执行的,哪些是不需要执行的呢?这个时候我们就需要将响应函数收集起来,等需要执行的时候再去执行。这里采用将需要执行的函数存放在数组中,数组统一保存。等到需要执行的时候再统一被执行。
let reactiveFns = [] //定义一个数组,用于储存需要执行的函数
function watchFn(func) { // 封装一个收集响应函数的函数
reactiveFns.push(func)
}
const obj = {
name: "cj",
age: 18
}
//对需要响应执行的函数进行收集
watchFn(function() {
console.log('发现obj对象的name属性改变了,我要开始执行了')
return newName = obj.name
})
//不需要响应执行的函数
function foo(){
console.log('我不需要被收集')
}
obj.name = "wx"
//统一执行被收集的函数
reactiveFns.forEach(fn => fn())
现在我们实现了对需要执行函数的收集、存放,与统一执行。但是这还明显不是我们想要的,甚至还存在很多问题。别急,我们先一步一步来。
三、依赖收集类的封装
对于上面的实现,我们想一下,只用数组存放函数好吗?是不好的,数组很难去管理需要响应函数,因为比如一个对象有多个属性,不同的属性对应不同响应函数。这样的话,就要通过对每个属性定义一个对应的数组,那又怎么去管理这些数组呐?。这里最好的就是用class去统一管理。
//定义一个class,用于收集保存的响应函数
class Depend {
constructor(){
this.reactiveFns = []
}
//储存响应函数
addDepend(func){
this.reactiveFns.push(func)
}
//统一响应函数
notify(){
this.reactiveFns.forEach(fn=>fn())
}
}
//分别对name与age定义一个depend实例
const dependName = new Depend()
const dependAge = new Depend()
//定义一个依赖收集函数
function watchFn(func,attri){
switch(attri){
case:'name'{
dependName.addDepend(func)
}
case:'age'{
dependAge.addDepend(func)
}
}
}
const obj = {
name:'cj',
age:18
}
//传入响应函数,watchFn来进行收集
watchFn(function(){
console.log('发现obj对象的name属性改变了,我要开始执行了')
return newName = obj.name
},'name')
watchFn(function(){
console.log('发现obj对象的age属性改变了,我要开始执行了')
return newAge = obj.age
},'age')
//不需要响应执行的函数
function foo(){
console.log('我不需要被收集')
}
//对obj对象属性重新赋值
obj.name = 'wx'
obj.age = 19
//执行notify
dependName.notify()
dependAge.notify()
这样看下来,一个属性对应一个depend,每个属性一一对应的关系是不是更清晰了。
四、实现自动监听对象的变化
上面的实现中,相信大家也发现了,对对象做出改变后,还是自己手动去执行notify,而且在手动执行的时候,我们还不知道哪个属性对应的哪个notify,直接一起全部执行。这明显不符合响应式啊,我们肯定要解决这个问题。但是要怎么去实现这个功能呐?对于这个问题的处理,vue2是采用ES5的Object.defineproperty去监听对象的属性变化,而vue3是采用ES6的proxy去监听对象属性变化(我会对这两个都进行实现,现在先以vue3的proxy为例进行实现,最后我会再用vue2的Object.defineproperty去替换proxy实现)。只要监听到属性的变化后,就直接在set捕获器中执行notify就行了。
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(func) {
this.reactiveFns.push(func)
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
//分别对name与age定义一个depend实例
const dependName = new Depend()
const dependAge = new Depend()
//定义一个依赖收集函数
function watchFn(func,attri){
switch(attri){
case:'name'{
dependName.addDepend(func)
}
case:'age'{
dependAge.addDepend(func)
}
}
}
// 监听对象的属性变量,里面还使用了Reflect协助实现
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
//执行notify
switch(key){
case:'name'{
dependName.notify()
}
case:'age'{
dependAge.notify()
}
}
}
})
//传入响应函数,watchFn来进行收集
watchFn(function(){
console.log('发现objProxy对象的name属性改变了,我要开始执行了')
return newName = objProxy.name
},'name')
watchFn(function(){
console.log('发现objProxy对象的age属性改变了,我要开始执行了')
return newAge = objProxy.age
},'age')
objProxy.name = "wx"
objProxy.age = 19
五、依赖收集管理的进一步处理
在第三节的时候后,我们对于存放响应函数的数组管理时通过给每一个对象属性创建一个depend进行管理。但是这样也不符合响应式,因为我们并不知道有哪些对象,而且不知道哪些响应函数对应哪个属性(之前是因为传递的固定属性名,但是真实的情况我们是不知道的),没法提前对每个对象属性创建对应的depend管理。
这里我们就需要用到weakmap、map这两种数据结构进行管理。实现的大致思路是,每个对象对应一个map,所有的map存放在一个weakmap。这里为啥用weakmap存放map,而不直接还是用map去存放所有map,是因为weakmap是一个弱引用数据结构,有利于响应式的性能。
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(func) {
this.reactiveFns.push(func)
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
//定义一个全局变量,这里用于收集属性的响应函数,
let dependFnc = null
//之前的watchFn函数是不对的,因为watchFn是不知道哪些响应函数是哪个对象的,哪个对象里的哪个属性,现在将响应函数加入数组的操作放在get捕获器里,
//因为捕获器是能精确的获取到是哪个对象的哪个属性在操作,所以就直接将创建depend与保存响应函数放入get捕获器里执行。
function watchFn(fn) {
//将收集的响应函数赋值给全局变量,这么做的作用是get将响应函数保存在数组中的时候是没办法直接拿到响应函数的。只有watchFn能获取到,每当watchFn收集
//到响应函数就直接赋值给全局变量,因为get是能获取到全局变量的。
dependFnc = fn
//这里拿到响应函数后,是需要执行的,因为响应函数是会获取对象属性的,一旦获取对应属性就会触发get捕获器,相当于是一个触发条件
fn()
//重新赋值null
dependFnc = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) { //没有的话,说明是第一次获取,还没有创建相关的map
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) { //第一次获取,创建对应的depend实例
depend = new Depend()
map.set(key, depend)
}
return depend //返回需要的depend
}
const obj = {
name: "cj",
age: 18
}
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
//每当获取对象属性时,就创建对应的depend
const depend = getDepend(target, key)
//创建depend后,将响应函数加入到对应的数组中保存。
depend.addDepend(dependFnc)
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 获取该对象对应属性的depend
const depend = getDepend(target, key)
//执行该属性的响应函数
depend.notify()
}
})
watchFn(function() {
console.log('发现objProxy对象的name属性改变了,我要开始执行了')
return newName = objProxy.name
})
watchFn(function() {
console.log('发现objProxy对象的age属性改变了,我要开始执行了')
return newAge = objProxy.age
})
objProxy.name = "wx"
objProxy.age = 19
现在已经可以,当对对象操作时,可以自动收集对应响应函数,并自动执行对应响应函数。离响应式实现越来越近了。
六、优化与重构
现在还存在的问题,当watchFn收集响应函数后,在去执行响应函数触发get捕获器时,万一响应函数内多次访问同一属性,就会多次触发get捕获器,也就会多次执行addDepend方法,多次添加一样的dependFnc全局变量。这显然是不对的,我们这里可以采用set数据结构去保存响应函数,而不是数组。set数据结构可以自动去除内部相同的数据。
//定义一个全局变量,这里用于收集属性的响应函数,
let dependFnc = null
class Depend {
constructor() {
//现在不用数组存放响应函数了,改为set数据结构
this.reactiveFns = new set
}
//优化添加响应函数步骤,直接在addDepend判断addDepend是否为空,不为空就直接添加
addDepend(){
if(dependFnc){
this.reactiveFns.add(dependFnc)
}
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
//之前的watchFn函数是不对的,因为watchFn是不知道哪些响应函数是哪个对象的,哪个对象里的哪个属性,现在将响应函数加入数组的操作放在get捕获器里,
//因为捕获器是能精确的获取到是哪个对象的哪个属性在操作,所以就直接将创建depend与保存响应函数放入get捕获器里执行。
function watchFn(fn) {
//将收集的响应函数赋值给全局变量,这么做的作用是get将响应函数保存在数组中的时候是没办法直接拿到响应函数的。只有watchFn能获取到,每当watchFn收集
//到响应函数就直接赋值给全局变量,因为get是能获取到全局变量的。
dependFnc = fn
//这里拿到响应函数后,是需要执行的,因为响应函数是会获取对象属性的,一旦获取对应属性就会触发get捕获器,相当于是一个触发条件
fn()
//重新赋值null
dependFnc = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) { //没有的话,说明是第一次获取,还没有创建相关的map
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) { //第一次获取,创建对应的depend实例
depend = new Depend()
map.set(key, depend)
}
return depend //返回需要的depend
}
const obj = {
name: "cj",
age: 18
}
const objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
//每当获取对象属性时,就创建对应的depend
const depend = getDepend(target, key)
//这里就不用管全局变量是啥了,直接调用addDepend方法就好了
depend.addDepend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// 获取该对象对应属性的depend
const depend = getDepend(target, key)
//执行该属性的响应函数
depend.notify()
}
})
watchFn(function() {
console.log('发现objProxy对象的name属性改变了,我要开始执行了')
return newName = objProxy.name
})
watchFn(function() {
console.log('发现objProxy对象的age属性改变了,我要开始执行了')
return newAge = objProxy.age
})
objProxy.name = "wx"
objProxy.age = 19
七、对象的响应式化封装(vue3)
最后大家还会有疑惑,上面的代码中是单独对obj对象做了处理,要是还有其它对象,那不是还要new proxy去创建一个代理,写入get、set捕获器,那代码量也太大了。而且我们咋知道有哪些对象。要解决这个问题,我们就的从创建对象的时候入手,直接在创建对象的时候就进行响应式化。把创建代理的步骤直接进行封装。
let dependFnc = null
class Depend {
constructor() {
this.reactiveFns = new set
}
addDepend(){
if(dependFnc){
this.reactiveFns.add(dependFnc)
}
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
function watchFn(fn) {
dependFnc = fn
fn()
dependFnc = null
}
const targetMap = new WeakMap()
function getDepend(target, key) {
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
let depend = map.get(key)
if (!depend) { //第一次获取,创建对应的depend实例
depend = new Depend()
map.set(key, depend)
}
return depend //返回需要的depend
}
//封装创建代理对象操作
function reactive(obj){
return new Proxy(obj, {
get: function(target, key, receiver) {
const depend = getDepend(target, key)
depend.addDepend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
const depend = getDepend(target, key)
depend.notify()
}
})
}
//调用reactive函数,直接传入要创建的字面量对象
const obj = reactive({
name:'cj',
age:18
})
const info = reactive({
name:'cx',
height:'162cm'
})
watchFn(function() {
console.log('发现obj对象的name属性改变了,我要开始执行了')
return newName = obj.name
})
watchFn(function() {
console.log('发现info对象的height属性改变了,我要开始执行了')
return newHeight = info.height
})
obj.name = "wx"
info.height = '180cm'
学过vue3的小伙伴,看见reactive是不是一下就想起来了vue3的reactiveAPI了,这不就是reactive的实现过程吗。到这里vue3的响应式就完成了。
八、对象的响应式化封装(vue2)
上面我们对vue3的实现已经完成了,这里就在用vue2的Object.defineproperty去替换vue3的proxy,实现vue2的响应式。
let dependFnc = null
class Depend {
constructor() {
this.reactiveFns = new set
}
addDepend(){
if(dependFnc){
this.reactiveFns.add(dependFnc)
}
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
function watchFn(fn) {
dependFnc = fn
fn()
dependFnc = null
}
const targetMap = new WeakMap()
function getDepend(target, key) {
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
let depend = map.get(key)
if (!depend) { //第一次获取,创建对应的depend实例
depend = new Depend()
map.set(key, depend)
}
return depend //返回需要的depend
}
//封装vue2响应式化操作
function reactive(obj) {
//直接使用Object.keys方法将对象转为以键名为元素的数组,再forEach进行遍历
Object.keys(obj).forEach(key => {
//获取对象属性值
let value = obj[key]
//监听对象属性,设置存取描述符
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
//返回要访问的属性值
return value
},
set: function(newValue) {
//将要设置的新值进行赋值
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
//再将obj对象返回
return obj
}
//调用reactive函数,直接传入要创建的字面量对象
const obj = reactive({
name:'cj',
age:18
})
const info = reactive({
name:'cx',
height:'162cm'
})
watchFn(function() {
console.log('发现obj对象的name属性改变了,我要开始执行了')
return newName = obj.name
})
watchFn(function() {
console.log('发现info对象的height属性改变了,我要开始执行了')
return newHeight = info.height
})
obj.name = "wx"
info.height = '180cm'
以上就是vue2的响应式实现
总结
这样一步一步去实现响应式,有利于更好的去理解响应式,掌握响应式每个点的作用。