大家好,今天给各位分享响应式文章网站源码分享的一些知识,其中也会对响应式网站用什么开发的进行解释,文章篇幅可能偏长,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在就马上开始吧!
作者:hkc52前端巅峰
转发链接:https://mp.weixin.qq.com/s/A6WgCjQj3KsaKC6kSLy-1A
原文作者:KC
原文链接:https://hkc452.github.io/slamdunk-the-vue3/
前言
上一篇1.1万字深入细品Vue3.0源码响应式系统笔记「上」讲解了effect是响应式系统的核心,而响应式系统又是vue3中的核心。接下来我们继续讲解collectionHandlers
collectionHandlers主要是对set、map、weakSet、weakMap四种类型的对象进行劫持。主要有下面三种类型的handler,当然照旧,我们拿其中的mutableCollectionHandlers进行讲解。剩余两种结合理解。
exportconstmutableCollectionHandlers:ProxyHandler<CollectionTypes>={\nget:createInstrumentationGetter(false,false)\n}\n\nexportconstshallowCollectionHandlers:ProxyHandler<CollectionTypes>={\nget:createInstrumentationGetter(false,false)(false,true)\n}\n\nexportconstreadonlyCollectionHandlers:ProxyHandler<CollectionTypes>={\nget:createInstrumentationGetter(true,false)\n}
mutableCollectionHandlers主要是对collection的方法进行劫持,所以主要是对get方法进行代理,接下来对createInstrumentationGetter(false,false)进行研究。instrumentations是代理get访问的handler,当然如果我们访问的key是ReactiveFlags,直接返回存储的值,否则如果访问的key在instrumentations上,在由instrumentations进行处理。
functioncreateInstrumentationGetter(isReadonly:boolean,shallow:boolean){\nconstinstrumentations=shallow\n?shallowInstrumentations\n:isReadonly\n?readonlyInstrumentations\n:mutableInstrumentations\n\nreturn(\ntarget:CollectionTypes,\nkey:string|symbol,\nreceiver:CollectionTypes\n)=>{\nif(key===ReactiveFlags.isReactive){\nreturn!isReadonly\n}elseif(key===ReactiveFlags.isReadonly){\nreturnisReadonly\n}elseif(key===ReactiveFlags.raw){\nreturntarget\n}\n\nreturnReflect.get(\nhasOwn(instrumentations,key)&&keyintarget\n?instrumentations\n:target,\nkey,\nreceiver\n)\n}\n}
接下来看看mutableInstrumentations,可以看到mutableInstrumentations对常见集合的增删改查以及迭代方法进行了代理,我们就顺着上面的key怎么进行拦截的。注意this:MapTypes是ts上对this类型进行标注
constmutableInstrumentations:Record<string,Function>={\nget(this:MapTypes,key:unknown){\nreturnget(this,key,toReactive)\n},\ngetsize(){\nreturnsize((thisasunknown)asIterableCollections)\n},\nhas,\nadd,\nset,\ndelete:deleteEntry,\nclear,\nforEach:createForEach(false,false)\n}\nconstiteratorMethods=[&39;,&39;,&39;,Symbol.iterator]\niteratorMethods.forEach(method=>{\nmutableInstrumentations[methodasstring]=createIterableMethod(\nmethod,\nfalse,\nfalse\n)\nreadonlyInstrumentations[methodasstring]=createIterableMethod(\nmethod,\ntrue,\nfalse\n)\nshallowInstrumentations[methodasstring]=createIterableMethod(\nmethod,\ntrue,\ntrue\n)\n})
get方法首先获取target,对target进行toRaw,这个会被createInstrumentationGetter中的proxy拦截返回原始的target,然后对key也进行一次toRaw,如果两者不一样,说明key也是reative的,对key和rawkey都进行track,然后调用target原型上面的has方法,如果key为true,调用get获取值,同时对值进行wrap,对于mutableInstrumentations而言,就是toReactive。
functionget(\ntarget:MapTypes,\nkey:unknown,\nwrap:typeoftoReactive|typeoftoReadonly|typeoftoShallow\n){\ntarget=toRaw(target)\nconstrawKey=toRaw(key)\nif(key!==rawKey){\ntrack(target,TrackOpTypes.GET,key)\n}\ntrack(target,TrackOpTypes.GET,rawKey)\nconst{has,get}=getProto(target)\nif(has.call(target,key)){\nreturnwrap(get.call(target,key))\n}elseif(has.call(target,rawKey)){\nreturnwrap(get.call(target,rawKey))\n}\n}
has方法跟get方法差不多,也是对key和rawkey进行track。
functionhas(this:CollectionTypes,key:unknown):boolean{\nconsttarget=toRaw(this)\nconstrawKey=toRaw(key)\nif(key!==rawKey){\ntrack(target,TrackOpTypes.HAS,key)\n}\ntrack(target,TrackOpTypes.HAS,rawKey)\nconsthas=getProto(target).has\nreturnhas.call(target,key)||has.call(target,rawKey)\n}
size和add方法size最要是返回集合的大小,调用原型上的size方法,同时触发ITERATE类型的track,而add方法添加进去之前要判断原本是否已经存在了,如果存在,则不会触发ADD类型的trigger。
functionsize(target:IterableCollections){\ntarget=toRaw(target)\ntrack(target,TrackOpTypes.ITERATE,ITERATE_KEY)\nreturnReflect.get(getProto(target),&39;,target)\n}
functionadd(this:SetTypes,value:unknown){\nvalue=toRaw(value)\nconsttarget=toRaw(this)\nconstproto=getProto(target)\nconsthadKey=proto.has.call(target,value)\nconstresult=proto.add.call(target,value)\nif(!hadKey){\ntrigger(target,TriggerOpTypes.ADD,value,value)\n}\nreturnresult\n}
set方法
set方法是针对map类型的,从this的类型我们就可以看出来了,同样这里我们也会对key做两个校验,第一,是看看现在map上面有没有存在同名的key,来决定是触发SET还是ADD的trigger,第二,对于开发环境,会进行checkIdentityKeys检查
functionset(this:MapTypes,key:unknown,value:unknown){\nvalue=toRaw(value)\nconsttarget=toRaw(this)\nconst{has,get,set}=getProto(target)\n\nlethadKey=has.call(target,key)\nif(!hadKey){\nkey=toRaw(key)\nhadKey=has.call(target,key)\n}elseif(__DEV__){\ncheckIdentityKeys(target,has,key)\n}\n\nconstoldValue=get.call(target,key)\nconstresult=set.call(target,key,value)\nif(!hadKey){\ntrigger(target,TriggerOpTypes.ADD,key,value)\n}elseif(hasChanged(value,oldValue)){\ntrigger(target,TriggerOpTypes.SET,key,value,oldValue)\n}\nreturnresult\n}
checkIdentityKeys就是为了检查目标对象上面,是不是同时存在rawkey和key,因为这样可能会数据不一致。
functioncheckIdentityKeys(\ntarget:CollectionTypes,\nhas:(key:unknown)=>boolean,\nkey:unknown\n){\nconstrawKey=toRaw(key)\nif(rawKey!==key&&has.call(target,rawKey)){\nconsttype=toRawType(target)\nconsole.warn(\n`Reactive${type}containsboththerawandreactive`+\n`versionsofthesameobject${type===`Map`?`askeys`:“},`+\n`whichcanleadtoinconsistencies.`+\n`Avoiddifferentiatingbetweentherawandreactiveversions`+\n`ofanobjectandonlyusethereactiveversionifpossible.`\n)\n}\n}
deleteEntry和clear方法deleteEntry主要是为了触发DELETEtrigger,流程跟上面set方法差不多,而clear方法主要是触发CLEARtrack,但是里面做了一个防御性的操作,就是如果集合的长度已经为0,则调用clear方法不会触发trigger。
functiondeleteEntry(this:CollectionTypes,key:unknown){\nconsttarget=toRaw(this)\nconst{has,get,delete:del}=getProto(target)\nlethadKey=has.call(target,key)\nif(!hadKey){\nkey=toRaw(key)\nhadKey=has.call(target,key)\n}elseif(__DEV__){\ncheckIdentityKeys(target,has,key)\n}\n\nconstoldValue=get?get.call(target,key):undefined\n//forwardtheoperationbeforequeueingreactions\nconstresult=del.call(target,key)\nif(hadKey){\ntrigger(target,TriggerOpTypes.DELETE,key,undefined,oldValue)\n}\nreturnresult\n}\n\nfunctionclear(this:IterableCollections){\nconsttarget=toRaw(this)\nconsthadItems=target.size!==0\nconstoldTarget=__DEV__\n?targetinstanceofMap\n?newMap(target)\n:newSet(target)\n:undefined\n//forwardtheoperationbeforequeueingreactions\nconstresult=getProto(target).clear.call(target)\nif(hadItems){\ntrigger(target,TriggerOpTypes.CLEAR,undefined,undefined,oldTarget)\n}\nreturnresult\n}
forEach方法在调用froEach方法的时候会触发ITERATE类型的track,需要注意Size方法也会同样类型的track,毕竟集合整体的变化会导致整个两个方法的输出不一样。顺带提一句,还记得我们的effect时候的trigger吗,对于SET|ADD|DELETE等类似的操作,因为会导致集合值的变化,所以也会触发ITERATE_KEY或则MAP_KEY_ITERATE_KEY的effect重新收集依赖。在调用原型上的forEach进行循环的时候,会对key和value都进行一层wrap,对于我们来说,就是reactive。
functioncreateForEach(isReadonly:boolean,shallow:boolean){\nreturnfunctionforEach(\nthis:IterableCollections,\ncallback:Function,\nthisArg?:unknown\n){\nconstobserved=this\nconsttarget=toRaw(observed)\nconstwrap=isReadonly?toReadonly:shallow?toShallow:toReactive\n!isReadonly&&track(target,TrackOpTypes.ITERATE,ITERATE_KEY)\n//important:createsurethecallbackis\n//1.invokedwiththereactivemapas`this`and3rdarg\n//2.thevaluereceivedshouldbeacorrespondingreactive/readonly.\nfunctionwrappedCallback(value:unknown,key:unknown){\nreturncallback.call(thisArg,wrap(value),wrap(key),observed)\n}\nreturngetProto(target).forEach.call(target,wrappedCallback)\n}\n}
createIterableMethod方法主要是对集合中的迭代进行代理,[&39;,&39;,&39;,Symbol.iterator]主要是这四个方法。
constiteratorMethods=[&39;,&39;,&39;,Symbol.iterator]\niteratorMethods.forEach(method=>{\nmutableInstrumentations[methodasstring]=createIterableMethod(\nmethod,\nfalse,\nfalse\n)\nreadonlyInstrumentations[methodasstring]=createIterableMethod(\nmethod,\ntrue,\nfalse\n)\nshallowInstrumentations[methodasstring]=createIterableMethod(\nmethod,\ntrue,\ntrue\n)\n})
可以看到,这个方法也会触发TrackOpTypes.ITERATE类型的track,同样也会在遍历的时候对值进行wrap,需要主要的是,这个方法主要是iteratorprotocol进行一个polyfill,所以需要实现同样的接口方便外部进行迭代。
functioncreateIterableMethod(\nmethod:string|symbol,\nisReadonly:boolean,\nshallow:boolean\n){\nreturnfunction(this:IterableCollections,…args:unknown[]){\nconsttarget=toRaw(this)\nconstisMap=targetinstanceofMap\nconstisPair=method===&39;||(method===Symbol.iterator&&isMap)\nconstisKeyOnly=method===&39;&&isMap\nconstinnerIterator=getProto(target)[method].apply(target,args)\nconstwrap=isReadonly?toReadonly:shallow?toShallow:toReactive\n!isReadonly&&\ntrack(\ntarget,\nTrackOpTypes.ITERATE,\nisKeyOnly?MAP_KEY_ITERATE_KEY:ITERATE_KEY\n)\n//returnawrappediteratorwhichreturnsobservedversionsofthe\n//valuesemittedfromtherealiterator\nreturn{\n//iteratorprotocol\nnext(){\nconst{value,done}=innerIterator.next()\nreturndone\n?{value,done}\n:{\nvalue:isPair?[wrap(value[0]),wrap(value[1])]:wrap(value),\ndone\n}\n},\n//iterableprotocol\n[Symbol.iterator](){\nreturnthis\n}\n}\n}\n}
总的来说对集合的代理,就是对集合方法的代理,在集合方法的执行的时候,进行不同类型的key的track或者trigger。
ref其实就是reactive包了一层,读取值要要通过ref.value进行读取,同时进行track,而设置值的时候,也会先判断相对于旧值是否有变化,有变化才进行设置,以及trigger。话不多说,下面就进行ref的分析。
通过createRef创建ref,如果传入的rawValue本身就是一个ref的话,直接返回。而如果shallow为false,直接让ref.value等于value,否则对rawValue进行convert转化成reactive。可以看到__v_isRef标识一个对象是否是ref,读取value触发track,设置value而且newVal的toRaw跟原先的rawValue不一致,则进行设置,同样对于非shallow也进行convert。
exportfunctionref(value?:unknown){\nreturncreateRef(value)\n}\nconstconvert=<Textendsunknown>(val:T):T=>\nisObject(val)?reactive(val):val\nfunctioncreateRef(rawValue:unknown,shallow=false){\nif(isRef(rawValue)){\nreturnrawValue\n}\nletvalue=shallow?rawValue:convert(rawValue)\nconstr={\n__v_isRef:true,\ngetvalue(){\ntrack(r,TrackOpTypes.GET,&39;)\nreturnvalue\n},\nsetvalue(newVal){\nif(hasChanged(toRaw(newVal),rawValue)){\nrawValue=newVal\nvalue=shallow?newVal:convert(newVal)\ntrigger(\nr,\nTriggerOpTypes.SET,\n&39;,\n__DEV__?{newValue:newVal}:void0\n)\n}\n}\n}\nreturnr\n}
triggerRef手动触发trigger,对shallowRef可以由调用者手动触发。unref则是反向操作,取出ref中的value值。
exportfunctiontriggerRef(ref:Ref){\ntrigger(\nref,\nTriggerOpTypes.SET,\n&39;,\n__DEV__?{newValue:ref.value}:void0\n)\n}\n\nexportfunctionunref<T>(ref:T):TextendsRef<inferV>?V:T{\nreturnisRef(ref)?(ref.valueasany):ref\n}
toRefs是将一个reactive对象或者readonly转化成一个个refs对象,这个可以从toRef方法可以看出。
exportfunctiontoRefs<Textendsobject>(object:T):ToRefs<T>{\nif(__DEV__&&!isProxy(object)){\nconsole.warn(`toRefs()expectsareactiveobjectbutreceivedaplainone.`)\n}\nconstret:any={}\nfor(constkeyinobject){\nret[key]=toRef(object,key)\n}\nreturnret\n}\n\nexportfunctiontoRef<Textendsobject,KextendskeyofT>(\nobject:T,\nkey:K\n):Ref<T[K]>{\nreturn{\n__v_isRef:true,\ngetvalue():any{\nreturnobject[key]\n},\nsetvalue(newVal){\nobject[key]=newVal\n}\n}asany\n}
需要提到baseHandlers一点的是,对于非shallow模式中,对于target不是数组,会直接拿ref.value的值,而不是ref。
if(isRef(res)){\nif(targetIsArray){\n!isReadonly&&track(target,TrackOpTypes.GET,key)\nreturnres\n}else{\n//refunwrapping,onlyforObjects,notforArrays.\nreturnres.value\n}\n}
而set中,如果对于target是对象,oldValue是ref,value不是ref,直接把vlaue设置给oldValue.value
if(!shallow){\nvalue=toRaw(value)\nif(!isArray(target)&&isRef(oldValue)&&!isRef(value)){\noldValue.value=value\nreturntrue\n}\n}
需要注意的是,ref还支持自定义ref,就是由调用者手动去触发track或者trigger,就是通过工厂模式生成我们的ref的get和set
exporttypeCustomRefFactory<T>=(\ntrack:()=>void,\ntrigger:()=>void\n)=>{\nget:()=>T\nset:(value:T)=>void\n}\n\nexportfunctioncustomRef<T>(factory:CustomRefFactory<T>):Ref<T>{\nconst{get,set}=factory(\n()=>track(r,TrackOpTypes.GET,&39;),\n()=>trigger(r,TriggerOpTypes.SET,&39;)\n)\nconstr={\n__v_isRef:true,\ngetvalue(){\nreturnget()\n},\nsetvalue(v){\nset(v)\n}\n}\nreturnrasany\n}
这个用法,我们可以在测试用例找到,
constcustom=customRef((track,trigger)=>({\nget(){\ntrack()\nreturnvalue\n},\nset(newValue:number){\nvalue=newValue\n_trigger=trigger\n}\n}))
computed就是计算属性,可能会依赖其他reactive的值,同时会延迟和缓存计算值,具体怎么操作。showthecode。需要注意的是,computed不一定有set操作,因为可能是只读computed。
首先我们会对传入的getterOrOptions进行解析,如果是方法,说明是只读computed,否则从getterOrOptions解析出get和set方法。紧接着,利用getter创建runnereffect,需要注意的effect的三个参数,第一是lazy,表明内部创建effect之后,不会立即执行。第二是coumputed,表明computed上游依赖改变的时候,会优先triggerrunnereffect,而runner也不会在这时被执行的,原因看第三。第三,我们知道,effect传入scheduler的时候,effect会trigger的时候会调用scheduler而不是直接调用effect。而在computed中,我们可以看到trigger(computed,TriggerOpTypes.SET,&39;)触发依赖computed的effect被重新收集依赖。同时因为computed是缓存和延迟计算,所以在依赖computedeffect重新收集的过程中,runner会在第一次计算value,以及重新让runner被收集依赖。这也是为什么要computedeffect的优先级要高的原因,因为让依赖的computed的effect重新收集依赖,以及让runner最早进行依赖收集,这样才能计算出最新的computed值。
exportfunctioncomputed<T>(\ngetterOrOptions:ComputedGetter<T>|WritableComputedOptions<T>\n){\nletgetter:ComputedGetter<T>\nletsetter:ComputedSetter<T>\n\nif(isFunction(getterOrOptions)){\ngetter=getterOrOptions\nsetter=__DEV__\n?()=>{\nconsole.warn(&39;)\n}\n:NOOP\n}else{\ngetter=getterOrOptions.get\nsetter=getterOrOptions.set\n}\n\nletdirty=true\nletvalue:T\nletcomputed:ComputedRef<T>\n\nconstrunner=effect(getter,{\nlazy:true,\n//markeffectascomputedsothatitgetspriorityduringtrigger\ncomputed:true,\nscheduler:()=>{\nif(!dirty){\ndirty=true\ntrigger(computed,TriggerOpTypes.SET,&39;)\n}\n}\n})\ncomputed={\n__v_isRef:true,\n//exposeeffectsocomputedcanbestopped\neffect:runner,\ngetvalue(){\nif(dirty){\nvalue=runner()\ndirty=false\n}\ntrack(computed,TrackOpTypes.GET,&39;)\nreturnvalue\n},\nsetvalue(newValue:T){\nsetter(newValue)\n}\n}asany\nreturncomputed\n}
从上面可以看出,effect有可能被多次调用,像下面中value.foo++,会导致effectFn运行两次,因为同时被effectFn同时被effectFn和c1依赖了。PS:下面这个测试用例是自己写的,不是Vue里面的。
it(&39;,()=>{\nconstvalue=reactive({foo:0})\nconstgetter1=jest.fn(()=>value.foo)\nconstc1=computed(getter1)\nconsteffectFn=jest.fn(()=>{\nvalue.foo\nc1.value\n})\neffect(effectFn)\nexpect(effectFn).toBe(1)\nvalue.foo++\n//原本以为是2\nexpect(effectFn).toHaveBeenCalledTimes(3)\n})
对于computed暴露出来的effect,主要为了调用effect里面stop方法停止依赖收集。至此,响应式模块分析完毕。
本篇文章完结
推荐Vue学习资料文章:
《1.1万字深入细品Vue3.0源码响应式系统笔记「上」》
《「实践」Vue数据更新7种情况汇总及延伸解决总结》
《尤大大细说Vue3的诞生之路「译」》
《提高10倍打包速度工具Snowpack2.0正式发布,再也不需要打包器》
《大厂CodeReview总结Vue开发规范经验「值得学习」》
《Vue3插件开发详解尝鲜版「值得收藏」》
《带你五步学会VueSSR》
《记一次Vue3.0技术干货分享会》
《Vue3.x如何有惊无险地快速入门「进阶篇」》
《「干货」微信支付前后端流程整理(Vue+Node)》
《带你了解vue-next(Vue3.0)之炉火纯青「实践」》
《「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分》
《「干货」Vue+Element前端导入导出Excel》
《「实践」Denobytes模块全解析》
《细品pdf.js实践解决含水印、电子签章问题「Vue篇」》
《基于vue+element的后台管理系统解决方案》
《Vue仿蘑菇街商城项目(vue+koa+mongodb)》
《基于electron-vue开发的音乐播放器「实践」》
《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》
《基于Vue技术栈的微前端方案实践》
《消息队列助你成为高薪Node.js工程师》
《Node.js中的stream模块详解》
《「干货」DenoTCPEchoServer是怎么运行的?》
《「干货」了不起的Deno实战教程》
《「干货」通俗易懂的Deno入门教程》
《Deno正式发布,彻底弄明白和node的区别》
《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》
《「实践」深入对比Vue3.0CompositionAPI和ReactHooks》
《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》
《深入Vue必学高阶组件HOC「进阶篇」》
《深入学习Vue的data、computed、watch来实现最精简响应式系统》
《10个实例小练习,快速入门熟练Vue3核心新特性(一)》
《10个实例小练习,快速入门熟练Vue3核心新特性(二)》
《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》
《2020前端就业Vue框架篇「实践」》
《详解Vue3中router带来了哪些变化?》
《Vue项目部署及性能优化指导篇「实践」》
《Vue高性能渲染大数据Tree组件「实践」》
《尤大大细品VuePress搭建技术网站与个人博客「实践」》
《10个Vue开发技巧「实践」》
《是什么导致尤大大选择放弃Webpack?【vite原理解析】》
《带你了解vue-next(Vue3.0)之小试牛刀【实践】》
《带你了解vue-next(Vue3.0)之初入茅庐【实践】》
《实践Vue3.0做JSX(TSX)风格的组件开发》
《一篇文章教你并列比较React.js和Vue.js的语法【实践】》
《手拉手带你开启Vue3世界的鬼斧神工【实践】》
《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》
《怎样为你的Vue.js单页应用提速》
《聊聊昨晚尤雨溪现场针对Vue3.0Beta版本新特性知识点汇总》
《【新消息】Vue3.0Beta版本发布,你还学的动么?》
《Vue真是太好了壹万多字的Vue知识点超详细!》
《Vue+Koa从零打造一个H5页面可视化编辑器——Quark-h5》
《深入浅出Vue3跟着尤雨溪学TypeScript之Ref【实践】》
《手把手教你深入浅出vue-cli3升级vue-cli4的方法》
《Vue3.0Beta和React开发者分别杠上了》
《手把手教你用vuedragchart实现一个可以拖动/缩放的图表组件》
《Vue3尝鲜》
《总结Vue组件的通信》
《Vue开源项目TOP45》
《2020年,Vue受欢迎程度是否会超过React?》
《尤雨溪:Vue3.0的设计原则》
《使用vue实现HTML页面生成图片》
《实现全栈收银系统(Node+Vue)(上)》
《实现全栈收银系统(Node+Vue)(下)》
《vue引入原生高德地图》
《Vue合理配置WebSocket并实现群聊》
《多年vue项目实战经验汇总》
《vue之将echart封装为组件》
《基于Vue的两层吸顶踩坑总结》
《Vue插件总结【前端开发必备】》
《Vue开发必须知道的36个技巧【近1W字】》
《构建大型Vue.js项目的10条建议》
《深入理解vue中的slot与slot-scope》
《手把手教你Vue解析pdf(base64)转图片【实践】》
《使用vue+node搭建前端异常监控系统》
《推荐8个漂亮的vue.js进度条组件》
《基于Vue实现拖拽升级(九宫格拖拽)》
《手摸手,带你用vue撸后台系列二(登录权限篇)》
《手摸手,带你用vue撸后台系列三(实战篇)》
《前端框架用vue还是react?清晰对比两者差异》
《Vue组件间通信几种方式,你用哪种?【实践】》
《浅析React/Vue跨端渲染原理与实现》
《10个Vue开发技巧助力成为更好的工程师》
《手把手教你Vue之父子组件间通信实践讲解【props、$ref、$emit】》
《1W字长文+多图,带你了解vue的双向数据绑定源码实现》
《深入浅出Vue3的响应式和以前的区别到底在哪里?【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》
《手把手教你D3.js实现数据可视化极速上手到Vue应用》
《吃透Vue项目开发实践|16个方面深入前端工程化开发技巧【上】》
《吃透Vue项目开发实践|16个方面深入前端工程化开发技巧【中】》
《吃透Vue项目开发实践|16个方面深入前端工程化开发技巧【下】》
《Vue3.0权限管理实现流程【实践】》
《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》
作者:hkc52前端巅峰
转发链接:https://mp.weixin.qq.com/s/A6WgCjQj3KsaKC6kSLy-1A
原文作者:KC
原文链接:https://hkc452.github.io/slamdunk-the-vue3/
OK,本文到此结束,希望对大家有所帮助。
