zl程序教程

您现在的位置是:首页 >  前端

当前栏目

vue2.0源码分析之理解响应式架构

响应架构源码 分析 理解 Vue2.0
2023-09-27 14:21:43 时间

分享前啰嗦

我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以

直接看vue2.0吧。这篇文章在公司分享过,终于写出来了。我们采用用最精简的代码,还原vue2.0响应式架构实现

以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考。

不过不看也没关系,但是最好了解下Object.defineProperty

本文分享什么

理解vue2.0的响应式架构,就是下面这张图

顺带介绍他比react快的其中一个原因

本分实现什么


const demo = new Vue({    data: {      text: "before",    },    //对应的template 为  div span {{text}} /span /div     render(h){      return h(div, {}, [        h(span, {}, [this.__toString__(this.text)])      ])   setTimeout(function(){     demo.text = "after"   }, 3000)  

对应的虚拟dom会从

div span before /span /div 变为 div span after /span /div

好,开始吧!!!

第一步, 讲data 下面所有属性变为observable

来来来先看代码吧


class Vue {        constructor(options) {          this.$options = options          this._data = options.data          observer(options.data, this._update)          this._update()        }        _update(){          this.$options.render()        }      }      function observer(value, cb){        Object.keys(value).forEach((key) =  defineReactive(value, key, value[key] , cb))      }      function defineReactive(obj, key, val, cb) {        Object.defineProperty(obj, key, {          enumerable: true,          configurable: true,          get: ()= {},          set:newVal=  {            cb()          }        })      }      var demo = new Vue({        el: #demo,        data: {          text: 123,        },        render(){          console.log("我要render了")        }      })       setTimeout(function(){         demo._data.text = 444       }, 3000)  

为了好演示我们只考虑最简单的情况,如果看了vue 源码分析之如何实现 observer 和 watcher可能就会很好理解,不过没关系,我们三言两语再说说,这段代码要实现的功能就是将


var demo = new Vue({       el: #demo,       data: {         text: 123,       },       render(){         console.log("我要render了")       }     })  

中data 里面所有的属性置于 observer,然后data里面的属性,比如 text 以改变,就引起_update()函数调用进而重新渲染,是怎样做到的呢,我们知道其实就是赋值的时候就要改变对吧,当我给data下面的text 赋值的时候 set 函数就会触发,这个时候 调用_update 就ok了,但是


setTimeout(function(){        demo._data.text = 444      }, 3000)  

demo._data.text没有demo.text用着爽,没关系,我们加一个代理


_proxy(key) {        const self = this        Object.defineProperty(self, key, {          configurable: true,          enumerable: true,          get: function proxyGetter () {            return self._data[key]          },          set: function proxySetter (val) {            self._data[key] = val          }        })      }  

然后在Vue的constructor加上下面这句


Object.keys(options.data).forEach(key =  this._proxy(key)) 

第一步先说到这里,我们会发现一个问题,data中任何一个属性的值改变,都会引起

_update的触发进而重新渲染,属性这显然不够精准啊

第二步,详细阐述第一步为什么不够精准

比如考虑下面代码


new Vue({       template: `          div             section               span name: /span  {{name}}            /section             section               span age: /span  {{age}}            /section           div `,       data: {         name: js,         age: 24,         height: 180       }     })     setTimeout(function(){       demo.height = 181     }, 3000)  

template里面只用到了data上的两个属性name和age,但是当我改变height的时候,用第一步的代码,会不会触发重新渲染?会!,但其实不需要触发重新渲染,这就是问题所在!!

第三步,上述问题怎么解决

简单说说虚拟 DOM

首先,template最后都是编译成render函数的(具体怎么做,就不展开说了,以后我会说的),然后render 函数执行完就会得到一个虚拟DOM,为了好理解我们写写最简单的虚拟DOM



      __h__(tag, attr, children) {          return VNode(tag, attr, children.map((child)= {            if(typeof child === string){              return VNode(undefined, undefined, undefined, child)            }else{              return child            }          }))        }        __toString__(val) {          return val == null ?  : typeof val === object ? JSON.stringify(val, null, 2) : String(val);        }      }      var demo = new Vue({        el: #demo,        data: {          text: "before",        },        render(){          return this.__h__(div, {}, [            this.__h__(span, {}, [this.__toString__(this.text)])          ])        }      })  

我们运行一下,他会输出


      tag: div,        data: {},        children:[          {            tag: span,            data: {},            children: [              {                children: undefined,                data: undefined,                tag: undefined,                text:  // 正常情况为 字符串 before,因为我们为了演示就不写代理的代码,所以这里为空              }            ]          }        ]      }  

这就是 虚拟最简单虚拟DOM,tag是html 标签名,data 是包含诸如 class 和 style 这些标签上的属性,childen就是子节点,关于虚拟DOM就不展开说了。

回到开始的问题,也就是说,我得知道,render 函数里面依赖了vue实例里面哪些变量(只考虑render 就可以,因为template 也会是帮你编译成render)。叙述有点拗口,还是看代码吧


var demo = new Vue({        el: #demo,        data: {          text: "before",          name: "123",          age: 23        },        render(){          return this.__h__(div, {}, [            this.__h__(span, {}, [this.__toString__(this.text)])          ])        }      })  

就像这段代码,render 函数里其实只依赖text,并没有依赖 name和 age,所以,我们只要text改变的时候

我们自动触发 render 函数 让它生成一个虚拟DOM就ok了(剩下的就是这个虚拟DOM和上个虚拟DOM做比对,然后操作真实DOM,只能以后再说了),那么我们正式考虑一下怎么做

第三步,touch 拿到依赖

回到最上面那张图,我们知道data上的属性设置defineReactive后,修改data 上的值会触发 set。

那么我们取data上值是会触发 get了。

对,我们可以在上面做做手脚,我们先执行一下render,我们看看data上哪些属性触发了get,我们岂不是就可以知道 render 会依赖data 上哪些变量了。

然后我么把这些变量做些手脚,每次这些变量变的时候,我们就触发render。

上面这些步骤简单用四个子概括就是 计算依赖。

(其实不仅是render,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computed 和 watch 的原理,也是mobx的核心)

第一步,

我们写一个依赖收集的类,每一个data 上的对象都有可能被render函数依赖,所以每个属性在defineReactive

时候就初始化它,简单来说就是这个样子的


class Dep {        constructor() {          this.subs = []        }        add(cb) {          this.subs.push(cb)        }        notify() {          console.log(this.subs);          this.subs.forEach((cb) =  cb())        }      }      function defineReactive(obj, key, val, cb) {        const dep = new Dep()        Object.defineProperty(obj, key, {          // 省略        })      }  

然后,当执行render 函数去touch依赖的时候,依赖到的变量get就会被执行,然后我们就可以把这个 render 函数加到 subs 里面去了。

当我们,set 的时候 我们就执行 notify 将所有的subs数组里的函数执行,其中就包含render 的执行。

至此就完成了整个图,好我们将所有的代码展示出来



       this._data = options.data         Object.keys(options.data).forEach(key =  this._proxy(key))         observer(options.data)         const vdom = watch(this, this._render.bind(this), this._update.bind(this))         console.log(vdom)       }       _proxy(key) {         const self = this         Object.defineProperty(self, key, {           configurable: true,           enumerable: true,           get: function proxyGetter () {             return self._data[key]           },           set: function proxySetter (val) {             self._data.text = val           }         })       }       _update() {         console.log("我需要更新");         const vdom = this._render.call(this)         console.log(vdom);       }       _render() {         return this.$options.render.call(this)       }       __h__(tag, attr, children) {         return VNode(tag, attr, children.map((child)= {           if(typeof child === string){             return VNode(undefined, undefined, undefined, child)           }else{             return child           }         }))       }       __toString__(val) {         return val == null ?  : typeof val === object ? JSON.stringify(val, null, 2) : String(val);       }     }     function observer(value, cb){       Object.keys(value).forEach((key) =  defineReactive(value, key, value[key] , cb))     }     function defineReactive(obj, key, val, cb) {       const dep = new Dep()       Object.defineProperty(obj, key, {         enumerable: true,         configurable: true,         get: ()= {           if(Dep.target){             dep.add(Dep.target)           }           return val         },         set: newVal =  {           if(newVal === val)             return           val = newVal           dep.notify()         }       })     }     function watch(vm, exp, cb){       Dep.target = cb       return exp()     }     class Dep {       constructor() {         this.subs = []       }       add(cb) {         this.subs.push(cb)       }       notify() {         this.subs.forEach((cb) =  cb())       }     }     Dep.target = null     var demo = new Vue({       el: #demo,       data: {         text: "before",       },       render(){         return this.__h__(div, {}, [           this.__h__(span, {}, [this.__toString__(this.text)])         ])       }     })      setTimeout(function(){        demo.text = "after"      }, 3000)  

我们看一下运行结果

好我们解释一下 Dep.target 因为我们得区分是,普通的get,还是在查找依赖的时候的get,

所有我们在查找依赖时候,我们将



我非常喜欢,vue2.0 以上代码为了好展示,都采用最简单的方式呈现。

不过整个代码执行过程,甚至是命名方式都和vue2.0一样

对比react,vue2.0 自动帮你监测依赖,自动帮你重新渲染,而

react 要实现性能最大化,要做大量工作,比如我以前分享的

react如何性能达到最大化(前传),暨react为啥非得使用immutable.js

react 实现pure render的时候,bind(this)隐患。

而 vue2.0 天然帮你做到了最优,而且对于像万年不变的 如标签上静态的class属性,

vue2.0 在重新渲染后做diff 的时候是不比较的,vue2.0比 达到性能最大化的react 还要快的一个原因

然后源码在此,喜欢的记得给个 star 哦

后续,我会简单聊聊,vue2.0的diff。


作者:杨川宝

来源:51CTO


微服务架构 | 5.4 Sentinel 流控、统计和熔断的源码分析 调用链路是 Sentinel 的工作主流程,由各个 Slot 槽组成,将不同的 Slot 槽按照顺序串在一起,从而将不同的功能(限流、降级、系统保护)组合在一起; 本篇《2. 获取 ProcessorSlot 链》将从源码级讲解如何获取调用链路,接着会以遍历链表的方式处理每一个 Slot 槽,其中就有:FlowSlot、StatisticSlot、DegradeSlot 等。分别对应本篇《3. 流控槽实施流控逻辑》、《4. 统计槽实施指标数据统计》和《5. 熔断槽实施服务熔断》;
微服务架构 | *2.5 Nacos 长轮询定时机制的源码分析 为方便理解与表达,这里把 Nacos 控制台和 Nacos 注册中心称为 Nacos 服务器(就是 web 界面那个),我们编写的业务服务称为 Nacso 客户端; 由于篇幅有限,这里将源码分析分为上下两篇,其中上篇讲获取配置与事件订阅机制,下篇讲长轮询定时机制;在《微服务架构 | 2.2 Alibaba Nacos 的统一配置管理》中提到一张 Nacos 动态监听的长轮询机制原理图,本篇将围绕这张图剖析长轮询定时机制的原理;
微服务架构 | *2.4 Nacos 获取配置与事件订阅机制的源码分析 为方便理解与表达,这里把 Nacos 控制台和 Nacos 注册中心称为 Nacos 服务器(就是 web 界面那个),我们编写的业务服务称为 Nacso 客户端; 由于篇幅有限,这里将源码分析分为上下两篇,其中上篇讲获取配置与事件订阅机制,下篇讲长轮询定时机制;
微服务架构 | *2.3 Spring Cloud 启动及加载配置文件源码分析(以 Nacos 为例) Spring Cloud 要实现统一配置管理,需要解决两个问题:如何获取远程服务器配置和如何动态更新配置;在这之前,我们先要知道 Spring Cloud 什么时候给我们加载配置文件;
微服务架构 | *3.5 Nacos 服务注册与发现的源码分析 为方便理解与表达,这里把 Nacos 控制台和 Nacos 注册中心称为 Nacos 服务器(就是 web 界面那个),我们编写的业务服务称为 Nacso 客户端; Nacos 客户端将自己注册进 Nacos 服务器。《1. 服务如何注册进 Nacos 注册中心》主要从 Nacos 客户端角度解释如何发送信息给 Nacos 服务器;《2. Nacos 服务器注册服务》主要从 Nacos 服务器角度解释注册原理; 《3. 客户端查询所有服务实例》将从服务消费者和提供者的角度,解释服务消费者如何获取提供者的所有实例。服务消费者和提供者都是 Nacos 的客户端;
netty案例,netty4.1源码分析篇六《Netty异步架构监听类Promise源码分析》 Netty是一个异步网络处理框架,在实现中大量使用了Future机制,并在Java自带Future的基础上,增加了Promise机制。这两个实现类的目的都是为了使异步编程更加方便使用。