1.响应式数据和副作用函数
//副作用函数
function effect(){
document.body.innerText = '我是王惊涛'
}
effect函数会修改body的文本内容,如果其他函数也会引用body.innerText这一属性,那么effect的执行就会影响其他函数的执行,这个时候就可以说effect函数产生了副作用。
//一个全局变量
let globalA = 10
//副作用函数
const fun1 = ()=>{
globalA = 100 //修改了全局变量
}
//引用这一变量的函数
const fun2 = ()=>{
return globalA+1
}
现在当globalA发生变化时,我们希望fun2可以重新执行,用最新的变量去计算,这就是响应式数据的需求所在,一个变量发生变化,引用该变量值的地方都应该触发更新。
2.响应式数据的基本实现
拦截一个属性的读取和修改操作,当这个属性被读取或修改的时候,可以实时的进行一些自定义的操作(写自己的函数),就是响应式数据所要完成的核心功能。
在js中,有两个api可以做到这一点:Object.defineProperty和proxy
名称 | api版本 | vue对应版本 | 特性 |
---|---|---|---|
Object.defineProperty | ES5 | vue2 | 有缺陷,对于数组和后续添加属性的读写无法直接监控到 |
proxy | ES6 | vue3 | 可以进行各种拦截监听,功能强大 |
const bucket = new Set() //一个用于存放副作用函数的桶
let data = {text:'我是王惊涛'}
const data_Proxy = new Proxy(data,{
get(target,key){ //拦截get方法,只要有人访问data_Proxy中的属性,就会触发这个函数,target代表对象整体,key代表被访问的属性名
console.log('触发了读取操作')
bucket.add(fun_rely) //这里就是解决之前提到的问题,谁用到这个变量了,就会将这个函数保存到存放桶里,修改的时候可以调用这个函数
return target[key] //返回该对象的被访问属性值
},
set(target,key,newVal){ //拦截set方法,只要有人修改data_Proxy中的属性,就会触发这个函数,newVal代表新的赋值
console.log('触发了set操作')
target[key] = newVal //赋值
bucket.forEach(fn=>fn()) //调用所有因为修改这个值而被影响的函数
return true //代表修改成功,得写
}
})
let fun_rely = ()=>{ //访问函数
console.log(data_Proxy.text,'fun_rely函数被调用')
}
fun_rely() //读
data_Proxy.text = '我还是王惊涛' //写
在nodejs环境中运行,结果如下:
3.设计一个完善的响应式系统
分析
在上面的代码中,存在一个很明显的缺陷,访问这个对象的任何属性,都会触发桶里的所有回调函数。因为我们没有针对具体某个属性的读写去区分,就好比你收到了A给你发的消息,结果你却给通讯录里的所有都一起回复了。某一属性被修改,应该只触发引用它的回调函数,而不是桶里的所有函数。
所以为了给被操作属性和副作用函数之间建立关联关系,我们得修改一下桶的数据结构。
桶是一个树形的数据结构,一个对象内可能会有多个响应式属性,而每一个属性可能也会有多个副作用函数跟其关联,最后形成了这样的一组数据
target是对象,key是具体的某一个属性,effectFn是关联的副作用函数
--obj1 (target)
-- key1 (key)
-- fun1 (effectFn)
-- fun2 (effectFn)
-- key2 (key)
-- fun3 (effectFn)
--obj2 (target)
-- key1 (key)
-- fun5 (effectFn)
-- fun6 (effectFn)
对obj1[key1]进行读取时,fun1和fun2都会添加到这个桶里面,并且和key1建立练习。当obj1[key1]被修改时,就会触发fun1和fun2这两个副作用函数,这样就达到了响应式的效果
很简答的总结:getter中收集依赖(副作用函数),setter中触发依赖
以下为一段自己模拟的源码和测试用例,请耐心观看…
代码
/*桶的结构
/.../ 表示数据的类型
bucket/weakMap结构/ :{
0:{
key/object结构/:{text:'我是王惊涛'}
value/Map结构/: {
key/属性名,字符串结构/:'text',
value/函数,副作用函数/: ()=>{console.log('副作用函数执行') return data.text}
}
}
}
这就是桶的结构,方便大家去看下面的代码
*/
const data = {text:'我是王惊涛'}
//完善响应式的代码(模拟源码)
let activeEffect = undefined //用一个全局变量存储副作用函数
const bucket = new WeakMap() //用来收集依赖的容器
function effect(fn){ //注册副作用函数
activeEffect = fn
fn() //假定这里的fn就是用户fun函数,通过调用想获取data.text,从而触发了getter操作,进行收集依赖
}
const data_Proxy = new Proxy(data,{
//读取
get(target,key){
if(!activeEffect) {
//副作用函数没有注册,如果注释掉上面的effect(fun),这里就会是一个普通的对象属性查询,只会return对应的值,然后结束函数的执行
return target[key]
}
//定义一个变量,从桶中获取当前访问对象所对应的值
let depsMap = bucket.get(target)
if(!depsMap){
//depsMap有效时,在桶中插入值,key为target对象,value是一个map容器
bucket.set(target,(depsMap = new Map()))
}
//定义deps,map结构中对应属性名称值
let deps = depsMap.get(key)
if(!deps){
//deps无效时,map容器插入值,key为键,值是一个Set,可用来存放多个副作用函数
depsMap.set(key,(deps = new Set()))
}
//将副作用函数插入set列表中,从而形成与target[key]的依赖
deps.add(activeEffect)
return target[key]
/*
名称 数据类型 具体含义
target 对象 data
key 属性名 text
bucket weakMap 最外层容器,key为target,value为depsMap
depsMap Map 第二层容器,key为key(属性值),value为deps
deps Set 最后一层容器,每个元素都是一个副作用函数
*/
},
//写入
set(target,key,newVal){
//赋上最新的值
target[key] = newVal
//根据对象,去查该对象在桶中所对应的值
const depsMap = bucket.get(target)
if(!depsMap){
//没有查找到,说明并没有进行响应式的注册
return
}
//查询key所对应的Set
const effects = depsMap.get(key)
//执行和该key有关的所有依赖函数,触发完成所有位置的更新
effects && effects.forEach(fn => fn())
}
})
//测试代码
const fun1 = ()=>{
console.log('副作用函数1执行')
return data_Proxy.text
}
const fun2 = ()=>{
console.log('副作用函数2执行')
return data_Proxy.text + ',哈哈哈'
}
effect(fun1) //注册fun1
effect(fun2) //注册fun2
console.log(data_Proxy.text,'读')
data_Proxy.text = '我还是王惊涛'
console.log(data_Proxy.text,'读')
console.log(bucket,'桶结构')
执行效果
依赖图形结构(官方)
weakMap和Map的使用区别
在上面的容器中使用了weakMap和Map,区别在于如果两个容器都插入一个以对象为key的键值对,Map是强引用类型,即时这个对象消失了,不会再调用了,垃圾回收机制也不会对其进行回收,会一直存在内存中;而weakMap是弱引用类型,如果这个对象不会再引用,垃圾机制就会将其回收,不会造成内存溢出。
无注释简练代码(增加track和trigger函数)
//创建track和trigger优化之前代码,没有功能上的新增
let activeEffect = undefined
const bucket = new WeakMap()
function effect(fn){
activeEffect = fn
fn()
}
const data = {text:'我是王惊涛'}
const data_Proxy = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
function track(target,key){
if(!activeEffect) {
return target[key]
}
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target,(depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,(deps = new Set()))
}
deps.add(activeEffect)
}
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap){
return
}
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
将get和set中的逻辑分裂到其他函数中,也是为了日后的方便维护,将逻辑拆分成功能单独的函数,可以有效降低重复逻辑的数量,极大提升灵活性。
4.分支切换与cleanup
有如下场景
const data = {ok:true,text:'我是王惊涛'}
const obj = new Proxy(data,{...})
effect(function effectFn(){
document.body.innerText = obj.ok?obj.text:'not'
})
如果obj.ok初始值为true,会触发obj.text的读取,副作用函数effectFn分别被data.ok和data.text所对应的依赖集合收集。当字段obj.ok修改为false,触发了副作用函数的调用,因为三元表达式的原因,obj.text不符合判断条件,就不会被读取,只会触发obj.ok的操作,所以,理想情况下,副作用函数effectFn不应该被obj.text所对应的依赖收集。
但是按照之前的代码来看,obj.ok修改为false,effectFn会执行。如果修改data.text,effectFn函数还是会执行,这个就不应该了。在obj.ok为false时,data.text修改不应该触发依赖函数。
解决思路:
所以,每次执行副作用函数时,可以先把他从所有与之关联的依赖集合中删除。
当副作用函数执行完毕后,会重新建立联系,但是在新的联系中不会包含遗留的副作用函数。而这个操作前提是要知道那些依赖集合中包含了这个副作用函数。
/**
* 场景
* const data = {ok:true,text:'我是王惊涛'}
const obj = new Proxy(data,{...})
effect(function effectFn(){
document.body.innerText = obj.ok?obj.text:'not'
})
*/
//增强
let activeEffect = undefined
const bucket = new WeakMap()
function effect(fn){
const effectFn = ()=>{
//清除原有的副作用函数
cleanup(effectFn)
activeEffect = effectFn
fn()
}
//添加一个用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
effectFn()
}
const data = {ok:true,text:'我是王惊涛'}
const data_Proxy = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
function track(target,key){
if(!activeEffect) {
return target[key]
}
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target,(depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,(deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps) //新增
}
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap){
return
}
const effects = depsMap.get(key)
//为了防止刚删除又创建,再删除再创建的无限死循环
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => effectFn())
// effects && effects.forEach(fn => fn())
}
//清除副作用函数的函数
function cleanup(effectFn){
for(let i = 0;i<effectFn.deps.length;i++){
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
//测试代码
const fun = ()=>{
return data_Proxy.ok?data_Proxy.text:'无文本'
}
effect(fun)
console.log(fun(),'第一次执行')
setTimeout(()=>{
data_Proxy.ok = false
console.log(fun(),'第二次执行')
},3000)
5.嵌套的effect与effect栈
effect函数是可以嵌套执行的,例如每个vue文件的内容都是一个render(),组件之间的嵌套关系,也会让effect去嵌套执行
如
//伪代码,effect嵌套
let data = {foo:'foo值',bar:'bar值'}
let effect = (fn)=>{
/** */
}
let temp1,temp2
effect (function effectFn1(){
console.log('effectFn1执行')
effect(function effectFn2(){
console.log('effectFn2执行')
temp2 = data.bar
})
temp1 = data.foo
})
理想状态下,副作用函数和对象属性之间的关系为
data
– foo
-- effectFn1
– bar
-- effectFn2
但是实际执行结果为
effectFn1执行
effectFn2执行
effectFn2执行
原因:用全局变量activeEffect来存储通过effect函数注册的副作用函数,这意味着同一时刻activeEffect所存储的副作用函数只能由一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖activeEffect的值,并且永远不会恢复到原来的值。如果这个时候再有响应式数据进行依赖收集,即时这个响应式数据是在外层副作用函数中读取的,他们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。
**解决思路:**创建一个函数栈effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后从栈中弹出,并始终让activeEffect指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况
let data = {foo:'foo值',bar:'bar值'}
let activeEffect
const effectStack = [] //effect栈
//注册响应式的函数
let effect = (fn)=>{
const effectFn = ()=>{
//将副作用函数赋值给activeEffect
activeEffect = effectFn
//调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
//执行副作用函数
fn()
//当副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length -1]
}
//用来存储与该副作用函数相关的依赖集合
effectFn.deps = []
effectFn()
}
let temp1,temp2
effect (function effectFn1(){
console.log('effectFn1执行')
effect(function effectFn2(){
console.log('effectFn2执行')
temp2 = data.bar
})
temp1 = data.foo
})
6.避免无限递归循环
场景:
const data = {foo:1}
const obj = new Proxy(data,{/**/})
effect(()=>obj.foo++)
原因:
obj.foo++可以理解为 obj.foo = obj.foo + 1 既进行了读取操作,也进行了修改操作。
先读取obj.foo的值,触发了track操作,将当前副作用函数收集到桶中,接着+1后再赋值给obj.foo,会触发trigger操作,把桶中的副作用函数取出并执行。问题在于该副作用函数还在执行中,还没有执行完毕,又要开始下一次的执行,就会无限调用自己,形成了栈溢出。
解决方法
设置和读取操作都在同一个副作用函数中进行,无论是track收集的还是trigger执行的,都是activeEffect。可以在trigger时增加守卫条件,如果trigger触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行。
function trigger (target,key){
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn=>{
if(effectFn !== activeEffect){ //如果trigger执行的副作用函数与当前执行的副作用函数相同,则不触发执行
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => effectFn())
}
7.调度执行
可调度性是响应式系统非常重要的特性。
其实就是通过事件循环,宏微任务的机制,可以自定义控制事件和函数的执行顺序
在vue中,多次修改响应式数据只会触发一次更新,就是用的这个机制
effect(()=>{
console.log(obj.foo)
},
//options
{
scheduler(fn){
setTimeout(fn)
}
}
)
function effect(fn,options){
const effectFn = ()=>{
/** */
}
//将options挂载到effectFn上
effectFn.options = options
effectFn.deps = []
effectFn()
}
function trigger(targrt,key){
const effectsToRun = new Set()
effectsToRun.forEach(effectFn=>{
//如果一个副作用函数存在调度器,则使用该调度器,并将副作用函数作为参数传递
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else{
effectFn()
}
})
}
8.计算属性computed和lazy
lazy
lazy懒执行,就是effect函数执行不会立刻传递给副作用函数
//立即执行
effect(()=>{
console.log('副作用函数')
})
//通过设置lazy字段为true,不立即执行
effect(()=>{
console.log('副作用函数')
},{lazy:true})
function effect(fn,options={}){
const effectFn =()=>{
/** */
}
effectFn.options = options
effectFn.deps = []
if(!options.lazy){ //立即执行
effectFn()
}
return effectFn
}
但如果仅仅是这样,意义也不大,因为我们后续还需要去执行副作用函数,所有就需要拿到副作用函数
function effect(fn,options = {}){
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
//将fn的执行结果存储到res中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
//将res做为effectFn的返回值
return res
}
effectFn.options = options
effectFn.deps = []
//未检出lazy:true,立即执行
if(!options.lazy){
effectFn()
}
return effectFn
}
传递给effect函数的参数fn才是真正的副作用函数,而effectFn是我们包装后的副作用函数。为了effectFn得到真正的副作用函数fn的执行结果,将res变量(副作用函数执行结果)作为返回值。
computed功能实现
下面的代码比较完整,有注释,可以直接引用到html中,或者在nodejs中运行
//计算属性函数
function computed(getter){
let value
let dirty = true
//把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter,{
lazy:true,
})
const obj = {
get value(){
return effectFn()
}
}
return obj
}
//定义一个全局变量,保存副作用函数
let activeEffect = undefined
//effect栈
const effectStack = []
//桶,用来保存对象=>属性=>依赖之间的连接关系
const bucket = new WeakMap()
//注册副作用函数
function effect(fn,options={}){
//effectFn是经过封装后的副作用函数,响应式数据的依赖其实就是这个经过封装的函数
const effectFn = ()=>{
//清除原有的副作用函数
cleanup(effectFn)
activeEffect = effectFn
//栈末尾压入一个副作用函数
effectStack.push(effectFn)
//执行这个函数
const res = fn()
//effects栈清除最后一个函数
effectStack.pop()
//动态的副作用函数赋值为栈中的最后一位
activeEffect = effectStack[effectStack.length - 1]
//res是副作用函数执行完毕的值,保存在res变量中,通过一层层返回,effect返回副作用函数effectFn,effectFn函数被调用返回res的值,最后作为obj中的value值,obj就是最后新生成的数据
return res
}
//保存副作用函数的配置
effectFn.options = options
//添加一个用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
//根据lazy属性判断,是否为立即执行副作用函数
if(!options.lazy){
effectFn()
}
//返回值就是依赖
return effectFn
}
//原始数据
const data = {foo:1,bar:2}
//使用元编程Proxy重写和自定义getter和setter
const data_Proxy = new Proxy(data,{
//重写getter,收集依赖,返回值
get(target,key){
track(target,key)
return target[key]
},
//重写setter,触发依赖,修改值
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
//追踪
function track(target,key){
//activeEffect无效,说明没有注册副作用函数,直接返回对象.属性值
if(!activeEffect) {
return target[key]
}
//depsMap:桶中第一层数据,键是对象,值是map
let depsMap = bucket.get(target)
if(!depsMap){
//在bucket,没有找到这个值,就新增一个
bucket.set(target,(depsMap = new Map()))
}
//获取桶中对象=>属性的依赖集合
let deps = depsMap.get(key)
if(!deps){
//不存在就给赋值一个,Set类型,第二层数据
depsMap.set(key,(deps = new Set()))
}
//将副作用函数加入deps容器中
deps.add(activeEffect)
//同时副作用函数的deps容器中加入刚才容器,类似于发布订阅模式,回调函数的相互存储和引用
activeEffect.deps.push(deps)
}
//触发
function trigger(target,key){
//获取桶中对应对象的值
const depsMap = bucket.get(target)
if(!depsMap){
return
}
//获取对应属性的值
const effects = depsMap.get(key)
//为了防止刚删除又创建,再删除再创建的无限死循环
const effectsToRun = new Set(effects)
effects && effects.forEach(effectFn=>{
if(effects !== activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
//自定义的调度
if(effectFn.options && effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else{
effectFn()
}
})
}
//清除副作用函数的函数
function cleanup(effectFn){
for(let i = 0;i<effectFn.deps.length;i++){
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
//一个需要其他变量计算的值,传参为一个有return值的函数
const sumRes = computed(()=>data_Proxy.foo + data_Proxy.bar)
console.log(sumRes.value,'计算获取的值:第一次')
data_Proxy.bar = 10
console.log(sumRes.value,'计算获取的值:第二次')
9.watch的实现原理
watch的本质就是观测一个响应式数据,当数据变化时通知并执行相应的回调函数。
watch本质上就是利用effect以及options.scheduler选项。
effect(()=>{
console.log(obj.foo)
},{
scheduler(){
//当obj.foo值变化时,会执行scheduler调度函数
}
})
当一个副作用函数中访问响应式数据obj.foo,会在副作用函数与响应式数据之间建立联系,当响应式数据变化时,会触发副作用函数重新执行。但是如果副作用函数存在scheduler选项,当响应式数据发生变化时,会触发scheduler调度函数执行,而不是直接触发副作用函数执行。从这个角度看,scheduler调度函数相当于一个回调函数,而watch的实现就是利用了这个特点特点。
简单版
可直接测试
//watch函数
function watch(source,callBack){
effect(
()=>traverse(source),
{
scheduler(){
callBack()
}
}
)
}
//递归回调函数
function traverse(value,seen = new Set()){
if(typeof value !== 'object' || value === null || seen.has(value)){
return
}
seen.add(value)
for(const k in value){
traverse(value[k],seen)
}
return value
}
//watch测试代码
//简单版本
watch(data_Proxy,()=>{
console.log('变化了',data_Proxy.foo)
})
data_Proxy.foo = 10
data_Proxy.foo = 20
函数版
除了常规的响应式数据,还可以接收一个getter函数
/**附带getter版本 start*/
function watch(source, callBack) {
let getter
if (typeof source === 'function') {
//如果source为函数类型
getter = source
} else {
getter = () => traverse(source)
}
//getter()执行完毕后返回值为被监测的对象
effect(() => getter(),
{
scheduler() {
callBack()
}
})
}
newValue和oldValue版本
通常,在数据监视时回去根据newValue和oldValue去做一些逻辑处理
//计算属性函数
/**监听对象属性 start*/
//watch函数
/**newValue和oldValue版本 start*/
function watch(source, callBack) {
let getter
if (typeof source === 'function') {
//如果source为函数类型
getter = source
} else {
//如果是初始对象类型
getter = () => traverse(source)
}
//定义新旧值变量
let oldValue , newValue
//getter()执行完毕后返回值为被监测的对象
const effectFn = effect(() => getter(),
{
lazy:true, //使用effect注册副作用函数时,开启lazt选项,并把返回值存储到effectFn中以便后续手动调用
scheduler() {
//执行effectFn,得到的是新值
newValue = effectFn()
callBack(newValue,oldValue)
//更新旧值,不然下一次得到的会是错误的旧值
oldValue = newValue
}
})
//手动调用,得到就是旧值
oldValue = effectFn()
}
/**newValue和oldValue版本 end*/
//递归回调函数
function traverse(value, seen = new Set()) {
//判断value的类型,正常情况下,value都是监视某一个对象,比如obj,如果递归传递回来的seen中有这个obj,也是不成立的,唯一性
if (typeof value !== 'object' || value === null || seen.has(value)) {
return
}
//set中添加这个对象作为元素
seen.add(value)
//遍历对象
for (const k in value) {
//递归回调,将对象的所有属性都遍历一遍
traverse(value[k], seen)
}
//返回的还是这个对象
return value
}
//定义一个全局变量,保存副作用函数
let activeEffect = undefined
//effect栈
const effectStack = []
//桶,用来保存对象=>属性=>依赖之间的连接关系
const bucket = new WeakMap()
//注册副作用函数
function effect(fn, options = {}) {
//effectFn是经过封装后的副作用函数,响应式数据的依赖其实就是这个经过封装的函数
const effectFn = () => {
//清除原有的副作用函数
cleanup(effectFn)
activeEffect = effectFn
//栈末尾压入一个副作用函数
effectStack.push(effectFn)
//执行这个函数
const res = fn()
//effects栈清除最后一个函数
effectStack.pop()
//动态的副作用函数赋值为栈中的最后一位
activeEffect = effectStack[effectStack.length - 1]
//res是副作用函数执行完毕的值,保存在res变量中,通过一层层返回,effect返回副作用函数effectFn,effectFn函数被调用返回res的值,最后作为obj中的value值,obj就是最后新生成的数据
return res
}
effectFn.options = options
//创建一个容器,effectFn
effectFn.deps = []
//根据lazy属性判断,是否为立即执行副作用函数
if (!options.lazy) {
effectFn()
}
//返回值就是依赖
return effectFn
}
//原始数据
const data = { foo: 1, bar: 2 }
//使用元编程Proxy重写和自定义getter和setter
const data_Proxy = new Proxy(data, {
//重写getter,收集依赖,返回值
get(target, key) {
track(target, key)
return target[key]
},
//重写setter,触发依赖,修改值
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
//追踪
function track(target, key) {
//activeEffect无效,说明没有注册副作用函数,直接返回对象.属性值
if (!activeEffect) {
return target[key]
}
//depsMap:桶中第一层数据,键是对象,值是map
let depsMap = bucket.get(target)
if (!depsMap) {
//在bucket,没有找到这个值,就新增一个
bucket.set(target, (depsMap = new Map()))
}
//获取桶中对象=>属性的依赖集合
let deps = depsMap.get(key)
if (!deps) {
//不存在就给赋值一个,Set类型,第二层数据
depsMap.set(key, (deps = new Set()))
}
//将副作用函数加入deps容器中
deps.add(activeEffect)
//同时副作用函数的deps容器中加入刚才容器,类似于发布订阅模式,回调函数的相互存储和引用
activeEffect.deps.push(deps)
}
//触发
function trigger(target, key) {
//获取桶中对应对象的值
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
//获取对应属性的值
const effects = depsMap.get(key)
//为了防止刚删除又创建,再删除再创建的无限死循环
const effectsToRun = new Set(effects)
effects && effects.forEach(effectFn => {
if (effects !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
//自定义的调度
if (effectFn.options && effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
//清除副作用函数的函数
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
//computed测试代码
//一个需要其他变量计算的值,传参为一个有return值的函数
// const sumRes = computed(()=>data_Proxy.foo + data_Proxy.bar)
// console.log(sumRes.value,'计算获取的值:第一次')
// data_Proxy.bar = 10
// console.log(sumRes.value,'计算获取的值:第二次')
// watch测试代码
watch(()=>data_Proxy.foo,(newVal,oldVal)=>{
console.log(newVal,oldVal,'数据监视')
})
data_Proxy.foo = 20 // 20 1 '数据监视'
data_Proxy.foo = 100 //100 20 '数据监视'
10.立即执行的watch与回调执行时机
在watch函数传参时,第三个参数:immediate设置为true,可以立即执行一次
function watch(source, callBack,options) {
let getter
if (typeof source === 'function') {
//如果source为函数类型
getter = source
} else {
//如果是初始对象类型
getter = () => traverse(source)
}
//定义新旧值变量
let oldValue , newValue
//提取scheduler调度函数为一个独立的job函数
const job = ()=>{
newValue = effectFn()
callBack(newValue,oldValue)
oldValue = newValue
}
const effectFn = effect(
()=>getter(),
{
lazy:true,
scheduler:job //使用job函数作为调度器函数
}
)
if(options.immediate){
job() //当immediate为true时立即执行,从而触发回调执行
}else{
oldValue = effectFn()
}
watch(()=>data_Proxy.foo,(newVal,oldVal)=>{
console.log(newVal,oldVal,'数据监视')
},{immediate:true})
data_Proxy.foo = 20
data_Proxy.foo = 100
//执行结果
//1 undefined '数据监视'
//20 1 '数据监视'
//100 20 '数据监视'