VUE3响应式设计原理(对象和数组响应方案)
/* *原理:当触发数据读取操作时,执行副作用函数并存储到桶中 *当设置数据操作时,再将副作用函数从桶中取出并执行 */ //用一个全局变量activeEffect存储被注册过的副作用函数 let activeEffect //const buket=new Set() /* *weakMap为弱引用,不影响垃圾回收机制工作,当用户代码对一个 *对象没有引用关系时,垃圾会收器会回收该对象,避免引起栈堆的溢出 */ const bucket=new WeakMap() const effectStack=[] //定义一个宏任务队列 const jobQueue=new Set() //定义一个Promose,将一个任务添加到微任务队列 const p=Promise.resolve() //是否正在刷新队列 let isFlushing=false //Symbol唯一,可以作为对象属性标识符使用 const ITERATE_KEY=Symbol() /* const data={ foo:1,bar:2, get tep(){ return this.foo } } */ /* const obj={} const proto={bar:1} const child=reactive(obj) const parent=reactive(proto) //使用parent作为child的原型 Object.setPrototypeOf(child,parent) //此处打印true,因为代理对象可以通过raw属性读取原始数据 console.dir(child.raw===obj) console.dir(parent.raw===proto) effect(()=>{ //会执行两次 console.log(child.bar) }) */ let arr=[1,2,3] //const obj=readonly(arr) const obj=reactive(arr) //重新建立副作用函数 effect( ()=>{ for(const key of obj){ console.log(key) document.getElementById("test").innerHTML=key } } ) setTimeout(()=>obj[3]=10,1000) //封装代理对象 //isShallow浅响应,isReadonly,浅只读 function createReactive(obj,isShallow=false,isReadonly=false){ return new Proxy(obj,{ //对原始数据的代理 //拦截读取操作 get(target,key,receiver){ //代理对象可以通过raw属性访问原始数据 if(key==="raw"){ return target } /* *因为数组的for of会读取symbol.iterator属性 *为避免错误和性能开销,要避免副作用函数与之建立响应 *如果key的类型是symbol则不进行追踪 */ if(!isReadonly && typeof key!=="symbol"){ track(target,key) } //返回属性值 //如果对象自身不存在该属性,会从对象原型寻找对应属性, //并调用原型get方法得到最终结果 const res=Reflect.get(target,key,receiver) if(isShallow){ return res } //深响应 /* *对于obj.foo.bar来说,当修改obj.foo.bar的值时,并不能触发响应 *为了解决这个问题,需要递归地调用reactive函数,直到能返回深响应地数据 */ if(typeof res==="object" && res!==null){ /* *实现深只读 *如果是只读对象,则调用readyonly对数据,返回只读对象 */ return isReadonly?readonly(res):reactive(res) } return res }, //拦击in操作符读取属性值 has(target,key){ track(target,key) return Reflect.has(target,key) }, //拦截for in 循环读取属性值 ownKeys(target){ //将副作用函数和ITERATE_KEY关联 //如果操作的是数组,则用length作为key并建立响应 track(target,Array.isArray(target)?"length":ITERATE_KEY) return Reflect.ownKeys(target) }, //拦截设置操作 set(target,key,newvalue,receiver){ //数据是只读的,则打印错误并返回 if(isReadonly){ console.warn(`属性${key}是只读的`) return true } //获取旧值 const oldval=target[key] //如果属性不存在,说明是添加新属性,否则是设置已有属性 //Object.prototype.hasOwnProperty检查当前操作的属性是否已经存在对象身上 /* *如果代理目标是数组,检测被设置的索引是否小于数组长度 *如果是,为SET操作,否则ADD操作 */ const type=Array.isArray(target) ?Number(key){ if(fn!==activeEffect){ effectsToRun.add(fn) } }) } /* *当直接设置了数组的length属性时,只需要对大于新length的元组进行操作即可 *如果操作的是数组的length属性 *那么取出大于新length的所有元素对应的副作用函数执行 */ if(Array.isArray(target) && key==="length"){ depsMap.forEach((effects,key)=>{ if(key>=newValue){ effects.forEach(fn=>{ if(fn!==activeEffect){ effectsToRun.add(fn) } }) } }) } //取得与INTERATE_KEY相关的副作用函数 const interateEffects=depsMap.get(ITERATE_KEY) //避免自增导致无限循环 //ECMA规范:再调用foreach遍历set集合时,如果一个值已经被访问过 //但这个值被删除并重新添加到集合,如果遍历没有结束,那么这个值 //又会重新被访问,解决办法是建立一个新的Set来遍历 effects && effects.forEach(f=>{ if(f!=effectsToRun){ effectsToRun.add(f) } }) //将ITERATE_KEY相关联的副作用函数6添加到effectsToRun //删除属性会导致ITERATE_KEY减少,所以需要重新触发 if(type==="ADD" || type==="DELETE"){ interateEffects && interateEffects.forEach(effect=>{ if(effect!==activeEffect){ effectsToRun.add(effect) } }) } effectsToRun.forEach(fn=>{ //如果副作用函数存在调度函数,那么执行调度函数,否则执行原函数 if(fn.options.scheduler){ fn.options.scheduler(fn) }else{ fn() } }) } //通过修改第二个参数来实现只读深浅,此处浅只读 function readonly(obj){ return createReactive(obj,false,true) } //此处深只读 function shallowReadonly(obg){ return createReactive(obj,true,true) } //深响应 function reactive(obj){ return createReactive(obj) } //实现浅响应 function shallowReactive(obj){ return createReactive(obj,true) } //options对象动态调度副作用函数的执行时机 function effect(fn,options={}){ const effectFn=()=>{ //例如effet(function effectFn(){document.body.inntext=obj.ok?obj.text:"not"}) //清除工作 cleanup(effectFn) //存储被注册过的副作用函数 activeEffect=effectFn //嵌套的副作用函数 //在调用副作用函数前将其压入栈中,首先压入的内层副作用函数 effectStack.push(effectFn) let res=fn() //调用完之后,将其弹出栈,弹出内层的副作用函数 effectStack.pop() activeEffect=effectStack[effectStack.length-1] //返回fn的结果 return res } //存储与该副作用相关的依赖集合 effectFn.deps=[] //将options挂在到副作用函数 effectFn.options=options if(!options.lazy) effectFn() return effectFn } function cleanup(effectFn){ //遍历副作用函数的deps数组 for(let i=0;i{console.log(obj.foo)}, scheduler(fn){ //执行调度时,将其添加到微任务队列 jobQueue.add(fn) //刷新队列 flushJob() } ) obj.foo++ obj.foo++ *最终输出 1 3 *微任务队列最终执行的只有一次,而此时obj.foo的值已经是3. */ function flushJob(){ //如果正在刷新任务队列,什么都不做,否则isFlushing=true if(isFlushing) return isFlushing=true //将任务添加到微任务队列 p.then(()=>{ jobQueue.forEach(job=>job()) }).finally(()=>{isFlushing=false}) } /* *计算属性与懒执行 */ function computed(getter){ let value //是否需要重新计算值,true代表需要计算 let dirty=true //只有调用value的时候才会执行 const effectFn=effect(getter,{ //不执行 lazy:true, //当值发生变化时,在跳读器中重新设置diarty。 scheduler(){ if(!dirty){ dirty=true //当计算属性依赖的响应数据发生变化时,手动调用函数触发响应 trigger(obj, "value") } } }) const obj={ get value(){ if(dirty){ //执行副作用函数 value=effectFn() //设置为false,下次访问时,直接使用原来的值 dirty=false } //当读取value时,手动调用track函数进行追踪 track(obj, "value") //返回值为fn的值 return value } } return obj } /* *wach的实现原理 *当数据发生变化时,执行回调 */ function watch(source,cb,options={}){ let getter //如果source是函数,则执行函数,否则调用traverse函数递归地读取属性 if(typeof source==="function"){ getter=source }else{ getter=()=>traverse(source) } //旧值与新值 let oldValue,newValue let cleanup function onInvalidate(fn){ cleanup=fn } //对scheduler函数的封装 const job=()=>{ newValue=effectFn() if(cleanup){ cleanup() } //返回旧值,新值,已经回调给用户使用 cb(newValue,oldValue,onInvalidate) //已经触发了回调函数,所以这里重新赋值 oldValue=newValue } //出发操作,建立回调 const effectFn=effect( //调用函数递归地读取数据 ()=>getter() ,{ lazy:true, //调度函数 scheduler:()=>{ //创建微任务队列,再DOM加载完成后再执行 if(options.flush==="post"){ const p=Promise.resolve() p.then(job) }else{ job() } } }) if(options.immediate){ job() }else{ //调用副作用函数,拿到旧值 oldValue=effectFn() } } 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(()=>obj.foo,(newValue,oldValue)=>alert(oldValue+":"+newValue)) setTimeout(()=>obj.foo++,1000) const sum=computed(()=>{ document.getElementById("test").innerHTML=obj.tep }) //重新建立副作用函数 effect(function effectFn(){ sum.value }) */
我的苦命姐姐秋日生活打卡季踩在这件事儿的风口浪尖上,无数次我姐被人堵在家门口,打工路上要钱,谩骂。无数个漫漫长夜里姐姐孤独哭泣。苦熬在那个小镇上,就想证明自己的清白。说话是上下嘴唇碰撞,一点都
死生亦大矣(2)一个多月的疫情封控,把人们都快折磨疯了,最近几天总是能听到有跳楼自杀的人。我想,虽修短随化,终期于尽但死生亦大矣!纵观古今,面对生死,人间百态,不一而足。在此谈谈个人看法,也算一解
2022进博会新款特斯拉ModelSPlaid首发太平洋汽车新车频道11月5日,在第五届中国国际进口博览会上,新款特斯拉ModelS(询底价查参配)Plaid迎来了国内首秀。新车外观部分细节进行调整,采用了全新的内饰设计,配备了独
国漫再出力作提起国漫,你会想到什么?精致的画面?酣畅淋漓的打戏?还是热血的少年主角?毫无疑问,在济公之降龙降世里,这些元素你统统都能找到。相信不少关心国漫的观众,也会对这部影片充满期待。这部电
哈登恩比德缺阵!马克西317,76人104106遭尼克斯逆转北京时间11月5日,NBA常规赛继续进行,费城76人主场迎战纽约尼克斯。本场比赛76人双核哈登恩比德均未出战。比赛开始,76人借主场之利以一波92开局,随后尼克斯逐渐调整状态将分差
进博会汽车展区纯电氢能争妍斗艳,跨国车企开启绿色转型新思考第五届中国国际进口博览会(下称进博会)的汽车展区,汽车首发首展产品力度达到五届进博会以来的最高水平。在第六届进博会招展处,宝马通用现代捷豹路虎特斯拉沃尔沃现代摩比斯曼胡默尔等产业链
晚间公告11月8日这些公告有看头品大事舒泰神STSA1002注射液和STSA1005注射液联用完成首例受试者给药舒泰神公告,近日,STSA1002注射液和STSA1005注射液联用在国内医院完成了针对治疗重型危重
科学直播一周年张朝阳的物理课出书了张朝阳的物理课出书了!11月6日,搜狐创始人董事局主席兼首席执行官张朝阳在线下物理课上,官宣自己首部著作张朝阳的物理课正式发布。在搜狐视频打造知识直播平台的背景下,张朝阳的物理课于
全锦赛爆冷门!国乒3大种子队首轮出局,世界冠军无缘团体奖牌北京时间2022年11月5日,全国乒乓球锦标赛已经结束前两日争夺,男团八强和女团八强全部出炉。在淘汰赛首轮比赛中,世界冠军陈梦王楚钦林高远都吃到了败仗,还有3支种子队伍在首轮出局,
斯诺克冠中冠今日赛况11月5日,一个新纪录摆在笵争一面前截止11月5日凌晨,冠中冠半决赛贾德特鲁姆普63战胜马克塞尔比。11月6日凌晨3点看笵争一再战奥沙利文,晚23点看夺冠之争。如果笵争一夺冠,将创造一举干掉排名前123的新记录,缘分
世界冠军爆冷遭遇一轮游,孙颖莎遗憾无缘再战陈梦,球迷表示失望世界冠军爆冷遭遇一轮游,孙颖莎遗憾无缘再战陈梦,球迷表示失望!继续来关注一下全锦赛的最新消息,经过前面两个比赛日的精彩争夺,本届女团比赛目前已经进行到了四强赛的阶段,四组对决分别是