前提摘要:
紧接上文,我们知道Vue2的实现原理核心之一就是Object.defineProperty函数,检测数据的变化,他的缺点是get无法捕捉到set的及时变化,所以引入中间全局变量tep,又不希望污染全局环境,我们封装了definReactive函数最终实现对数据变化的监测,那么Vue3是如何做的呢?
实际回答:Vue3有Ref和Reactive两种实现数据响应式得方法,对于简单数据也就是Ref定义得数据依旧是通过对setter和getter得数据劫持来实现得,没有太大区别,然后对于对象类型Reactive实现对象类型的响应式Vue3主要是依靠了proxy,proxy负责数据的劫持/代理,其实设计之初就选择proxy,但是由于proxy是ES6新特性对版本要求较高,考虑到兼容性问题,用Object.defineProperty代替,proxy与Object.defineProperty不同的点就是,proxy不是直接操作源数据,而是先生成源数据的代理数据,然后再通过getter和setter对源数据进行操作(代码如1.1),但是proxy只能代理对象类型的,这是一个需要注意的点,由于代理数据并非直接修改源数据这样就避免了一个问题,就是set修改数据get无法及时捕获,proxy中target就是源数据,get方法中通过return返回访问源数据,set方法中通过target[key]修改源数据,然后返回true提示修改完成,proxy通过传入源数据进行代理,在get和set中监视数据变化并且修改源数据,这就是Vue2和Vue3注意的不同点,其他实现结构和Vue2大同小异,Vue2
function reactive(data) {
//Proxy只能代理对象类型的数据,不是对象类型return回去
if (!isObject(data)) return
return new Proxy(data,{
get(target,key){
//在get操作时,收集依赖
track(target,key)
return target[key]
},
set(target,key,value){
target[key] = value
//在set操作时,触发副作用重新执行
trigger(target,key)
return true
}
})
}
代码片段1.1
前提摘要:
紧接上文,Vue2的时候已经解释过为什么需要收集依赖那么具体实现是如何进行的呢?
1.在get处通过track函数收集依赖,依赖的具体收集实现是设置一个全局变量,将副作用函数赋值给全局函数,然后执行函数,副作用函数里面会用到很多变量,例如:state.name,这个时候就访问了state.name数据这个时候就触发了他的get方法,执行完后还需要将其赋值为null,方便下一个函数的时候,同时也避免重复执行,因为触发数据的get方法的时候就会执行收集依赖,如果不清空,可能会将他加入无关依赖里面;然后就是track依赖收集函数,触发依赖就会执行,如果第一次进入就会创建弱映射关系,target是一个对象,例如state->map[name:Set(fn,fn),age:Set(fn,fn)]那么target就是这个对象,如果对应的key值也没有创建就会创建强映射的关系,比如name:Set(fn,fn),收集完的依赖都会放在一个map中以键值对的形式存储,然后触发的时候就可以通知到对应的依赖了,这样的映射关系可以做到精细化管理,如果不这样做的话,可能修改对象中一个值其他的值都会触发get方法,但是这样就可以精准到每个属性了,而且只触发对应属性get
function effect(fn){
if (typeof fn !== 'function') return
//记录正在执行的副作用函数
activeEffect = fn
//调用副作用函数
fn()
//重置全局变量
activeEffect = null
}
//收集依赖
function track(target,key) {
//当activeEffect不为null的时候才去添加到桶里面
if (!activeEffect) return
let depMap = bucket.get(target)
if(!depMap){
depMap = new Map()
bucket.set(target,depMap)
}
//设置一个中转
let depSet = depMap.get(key)
if(!depSet){
depSet = new Set()
depMap.set(key,depSet)
}
depSet.add(activeEffect)
}
2.在set函数的地方触发依赖,当数据发生变化的时候,循环在get处收集的依赖,遍历执行内部的方法,就能修改与变化数据相关的依赖
//触发依赖
function trigger(target,key){
let depMap = bucket.get(target)
if (!depMap) return
// 从副作用函数桶中依次取出每一个元素(副作用函数)执行
let depSet = depMap.get(key)
if (depSet) {
depSet.forEach((fn)=>fn())
}
}
3.其实原理并不难理解,除了实现思路的巧妙之处,还有一个重要的难点就是依赖桶的设计,通过代码片段3.3发现有有几次改动,第一次将收集依赖桶设计成数组,会遇到重复添加依赖的情况,比如同一个副作用函数出现多次,那么由于数组的设计本身不去重,那么就会触发多个同样的函数,修改数据成集合模式,集合优点就是自带去重的功能,但是这样设计缺陷就是如果只修改一个属性,他就会通知一个对象内的所有属性,显然这样会有不必要的开销,于是修改数据类型为map,这样就可以设置属性名了,修改name的时候不会影响到age等其他属性,但是当修改拥有两个对象相同的属性名,例如state:{name:'zs',age:'18'},state1:{name:'ls',age:'20'}这样访问name属性两个对象都会触发,于是又引入了弱映射关系,WeakMap()结构就增加对象作为属性名,这样又避免一个问题,其实用map也能用对象作为属性名,但是map会导致内存泄漏,WeakMap能够解决这个问题,WeakMap和Map区别在最底部有具体说明
// 定义一个副作用函数桶,存放所有的副作用函数,每一个元素都是一个副作用函数
// const bucket = []
// const bucket = new Set() //修改数据为集合模式 集合模式自带去重
// const bucket = new Map() //修改结构为[name:Set(fn,fn),age:Set(fn,fn)]
const bucket = new WeakMap() //修改结构为[state -> Map[name:Set(fn,fn),age:Set(fn,fn)],state1 -> Map[],]
// 定义一个全局变量,保存当前正在执行的副作用函数
代码片段3.3
Document
xiaopang
// 定义一个副作用函数桶,存放所有的副作用函数,每一个元素都是一个副作用函数
// const bucket = []
// const bucket = new Set() //修改数据为集合模式 集合模式自带去重
// const bucket = new Map() //修改结构为[name:Set(fn,fn),age:Set(fn,fn)]
const bucket = new WeakMap() //修改结构为[state -> Map[name:Set(fn,fn),age:Set(fn,fn)],state1 -> Map[],]
// 定义一个全局变量,保存当前正在执行的副作用函数
let activeEffect = null
function isObject(value){
return typeof(value) ==='object' && value !== null
}
function effect(fn){
if (typeof fn !== 'function') return
//记录正在执行的副作用函数
activeEffect = fn
//调用副作用函数
fn()
//重置全局变量
activeEffect = null
}
//收集依赖
function track(target,key) {
//当activeEffect不为null的时候才去添加到桶里面
if (!activeEffect) return
let depMap = bucket.get(target)
if(!depMap){
depMap = new Map()
bucket.set(target,depMap)
}
//设置一个中转
let depSet = depMap.get(key)
if(!depSet){
depSet = new Set()
depMap.set(key,depSet)
}
depSet.add(activeEffect)
}
//触发依赖
function trigger(target,key){
let depMap = bucket.get(target)
if (!depMap) return
// 从副作用函数桶中依次取出每一个元素(副作用函数)执行
let depSet = depMap.get(key)
if (depSet) {
depSet.forEach((fn)=>fn())
}
}
/**
* 创建响应式数据
* @param[object]:普通对象
* @return[proxy]:代理对象
*/
function reactive(data) {
//Proxy只能代理对象类型的数据,不是对象类型return回去
if (!isObject(data)) return
return new Proxy(data,{
get(target,key){
//在get操作时,收集依赖
track(target,key)
return target[key]
},
set(target,key,value){
target[key] = value
//在set操作时,触发副作用重新执行
trigger(target,key)
return true
}
})
}
WeakMap、Map、对象、数组区别:
数组也是一种键值对结构(key->value)------key只能是数字
数组的访问是通过arr[2]
对象也是一种键值对结构(key->value)------key只能是字符串
对象访问是通过obj['name']字符的访问,obj.name其实本质就是obj['name'],他是一种简化写法
有一种骚操作如图,但是依旧无法通过obj.0获取数据,还是需要obj['0']访问
Map:映射表.(key->value) key可以是任意类型的数据,字符串、数字、对象等等都可以
Map可以被看作一个高级版的对象
const map = new Map()
const foo = {foo:1}
map.set(foo,1) // {foo:1} => 1
console.log(map)
WeakMap: 弱映射表 (key只能是一个对象)
const weakmap = new WeakMap()
const bar = {bar:2}
weakmap.set(bar,1)
console.log(weakmap)
//weakmap.set(2,3)
重要区别之一:(map定义的会导致内存)
{
const map = new Map()
;(function(){
const foo = {foo:1} //在IIFE(自执行函数)定义的变量foo,执行完成后会被释放
map.set(foo,1) //由于将foo作为map的key,导致foo占用的空间没有被释放
})()
console.log(map.keys()) //通过调用map.keys方法可以证实说法,依旧能够找到foo这个对象
}
{
const weakmap = new WeakMap()
;(function(){
const foo = {foo:1}
weakmap.set(foo,1) //weakmap建立的是弱引用关系,不会保留foo的空间
})()
//当IIFE执行完后,foo的空间被释放
console.log(weakmap) //查看可知weakmap方法中没有map.keys这样的方法,方法较少
}
环境中命名的全局变量会占用内存空间,如果数据一旦不需要使用了,需要及时销毁,比如通过delete data.array
与 data.array = null
等删除data.array数组,当然JavaScript也有自己的垃圾回收机制可以通过标记删除、引用计数法和手动置空的方式销毁变量,当然闭包是一种很好解决变量使用的方法,但是如果变量没有及时销毁,这个变量就无法给下一个需要用到的功能使用
通俗理解就是比如一个工厂,有白班和夜班两班人,都可以公用车间的工作岗位,上班打卡使用机器,下班打卡注销使用,给夜班的人用,但是如果白班的人工作完,下班忘记打卡了,那么虽然人走了不会用到工位上的设备,但是设备使用权还是他的,夜班的人无法正常打卡使用机器,说白了内存泄漏就像你占着茅坑不拉屎或者说活干完了"人"不走。
对上述例子做一个补充,阅读后发现有不严谨的地方,全局变量的定义除了污染环境以外,还有一个非常重要的点就是安全性的问题,如果一个功能变量定义为全局变量,其他部分代码也可以访问和修改这个变量,这样很明显会带来安全问题,导致原有功能不能正常使用等bug,这个是不希望看到的,总结(有但不限于以下两点):①功能方法执行变量安全性得不到保证,可能会被其他代码修改或访问②功能执行完成后,变量未及时销毁,会导致占用内存
参考文章及图片:
【杰哥课堂】Vue3.2源码设计与实现-响应式原理