当前位置: 首页 > Web前端 > JavaScript

技术博客|深入浅出Vue数据响应原理

时间:2023-03-26 23:47:08 JavaScript

作者简介:DougGewutiInfraTeam运维开发工程师浅显Vue数据响应原理在使用Vue框架开发的过程中,经常会遇到这样的问题数据更新了,但是视图不能更新,阻碍了开发进度的bug。为了提高开发效率,我们可以通过遵循最佳实践来减少类似错误的出现频率。另外,如果开发者对数据响应过程有更好的理解,在功能实现过程中也能更好地控制代码,从而减少类似问题的发生。因此,为了帮助团队小伙伴提高开发效率,我探索了vue数据响应式的实现,并将其实现简化为可执行的示例代码,帮助大家更好的理解视图更新和依赖收集的过程。无论你是前端开发者、后端开发者,还是算法工程师,相信看完本文你都会有所收获。什么是数据响应?数据响应改变了前端渲染视图的代码。与传统的web开发相比,让代码的可读性更强,更强大。我将分为3个步骤来介绍如何实现简单的响应式数据。非响应式Observer模式数据响应式解决方案首先通篇简单介绍一下这两个类:-State&View类//State负责管理页面状态classState{text="hello"constructor(initText){这。text=initText}}//TextView负责接受一个参数并暴露一个render方法来渲染viewclassTextView{state={}//TextView初始化时会自动调用render函数渲染到viewconstructor(s){this.state=sthis.render()}render(){//使用console.log输出到终端模拟渲染功能console.log(this.state.text)}}基于在这两个类上,我们可以通过如下代码模拟前端渲染:letstate=newState("helloworld");//renderhelloworldletnode=newTextView(state);当节点状态改变需要重新渲染时无响应分为两步:state.text="foobar";node.render();假设有3个节点订阅了这个状态,代码如下:state.text="foobar";node.render();node0.render();node1.render();显然,可扩展性差,难以维护。观察者模式可以通过稍微修改上面的代码来实现更好的扩展性。classState{/**省略上面部分重复的代码**/watchList=[]watchText(view){this.watchList.push(view)}updateText(text){this.text=text;this.watchList.forEach(v=>{v.render()})}}classTextView{/**其他代码保持不变**/constructor(s){s.watchText(this)this.state=sthis.render()}}这样,当TextView实例初始化时,它会把自己注册到State的监听列表中。当有人通过updateText函数更新数据时,所有监听文本类的View实例都会被调用重新渲染:letstate=newState("helloworld");letnode=newView(state);letnode0=newView(state);letnode1=newView(state);//终端输出3foobarstate.updateText("foobar")看来扩展性问题解决了?不不不!假设我们现在有一个HeaderView视图类,代码如下://这里唯一不同的是console.log(this.state.header)}}那么我们的State代码大概会变成这样:classStateWithHeader{text="hello"header="header"watchTextList=[]watchHeaderList=[]constructor(header,text){}updateText(text){/**省略实现**/}watchText(view){/*??省略实现**/}watchHeader(view){/*??省略实现**/}updateHeader(header){/**省略实现**/}}如果字段越来越多,方法就会越来越多,重复的代码也会越来越多。你可能会想,这不是问题,还是可以解决的:classStateWithManyFields{//storemanystatesstate={}//一个map,key是string类型代表被监控的字段,val是一个ArraytypefieldWatchers={}constructor(initState){this.state=initState;}//只监听相应的字段,并传入View实例watch(field,view){if(this.fieldWatchers[field]==undefined){this.fieldWatchers[field]=[];}this.fieldWatchers[field].push(view)}//更新字段并调用所有View实例的render函数update(field,val){this.state[field]=val;if(this.fieldWatchers[field]!=undefined){this.fieldWatchers[field].forEach(v=>v.render());}}}太棒了!您退回到传统的Web开发,并且类型提示丢失了。假设你有20个状态,由于这些状态变成动态的,ide无法给你提示,你可能要靠记忆来记住这些字符串的值。假设您有20个具有这么多状态的组件,并且ide无法为您提供类型提示。祝你好运,我的朋友!那么数据响应性是如何解决的呢?在回答这个问题之前,我们需要介绍一下javascript的Proxy类,我们用它来代理我们的State实例:letstate=newState("helloworld");letproxyedState=newProxy(state,{//当有人调用someVar=proxyedState.,会打印对应的get:(obj,field)=>{console.log(field)returnobj[field]},set:(obj,field,val)=>{//当有人调用proxyedState.=时,会打印出对应的和传入的console.log(field,val)obj[field]=val;returntrue}})下面是调用方法//触发Proxy类的get代理//终端输出"text"leta=proxyedState.text;//触发Proxy类的setProxy//终端输出"textfoobar"proxyedState.text="foobar"如果我??们理解了上面Proxy的代码,那么根据Proxy,我们可以优化我们的state,省去updateField的代码而不会丢失类型提示。classState{reactive={}//一个map,key是string类型表示被监控的字段,val是Array类型watchers={}constructor(initState){this.reactive=initState;this.reactive=newProxy(this.reactive,{//假设当有人调用reactive.text时,所有监听文本状态的视图都会被更新set:(obj,field,val)=>{obj[field]=val;if(this.watchers[field]!=undefined){this.watchers[field].forEach(v=>v.render())}returntrue;},})}}好的,我们离成功只有一步之遥远方:View类如何在不使用Watch方法的情况下将自己添加到State的Watcher中?在继续阅读之前,您可以先考虑一下解决方案。下面简单介绍下Vue中的依赖收集是如何进行的。首先在ReactiveState的类声明之前添加一个全局变量声明:letcurrentCreatingView=undefined;classReactiveState{/**省略**/}接下来,修改View类的构造函数代码;classTextView{state={}constructor(s){//在全局变量中注册自己currentCreatingView=this;this.state=s//触发getterthis.render()//初始化完成currentCreatingView=undefined;}render(){console.log(this.state.text)}}最后完成State代码:classState{/**省略其他字段**/constructor(initState){this.reactive=initState;this.reactive=newProxy(this.reactive,{get:(obj,field)=>{//当视图正在初始化时,将正在初始化的视图添加到观察者if(currentCreatingView!=undefined){if(this.watchers[field]==undefined){this.watchers[field]=[]}this.watchers[field].push(currentCreatingView)}returnobj[field]}/**省略get**/})}}在继续解析之前尝试查看结果。lets=newState({header:"hello",text:"world"}).reactive;console.log("----initviews----")lettextView=newTextView(s)console.log("----initcomplete----")console.log("modifyingheader")//我们没有调用State.updateField方法,而是直接改了数据,让视图重新渲染//同时,我们也没有调用State.watch方法,将View类注册到监听列表中s.text="foo";output:----initviews----world/*由TextView.render打印*/----initcomplete----修改headerfoo/*由TextView.render打印*/现在,让我们最终创建State响应式:实现观察者模式,数据变化时自动调用渲染函数。它避免了传统开发中可读性差的问题,避免了update、watch等功能,而是直接通过类的getter和setter来监控和修改其状态。如果你看懂了前面Proxy的demo代码,那么通过setter通知状态更新就可以理解了。难点在于如何通过getter将View类注册到对应的State中。下面通过流程图进行分析:总结通过上面的示例代码,我们明确了Vue数据响应的过程,分为两个步骤:视图更新:通过Observe模式重新渲染。依赖收集:通过一定的机制收集谁订阅了数据中的特定字段。Vue的实际实现肯定会比较复杂,但希望通过本文介绍的内容,能够让大家更好地理解它的原理和流程。希望大家有所收获~参考:https://www.infoq.cn/article/...