当前位置: 首页 > 科技观察

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

时间:2023-03-11 21:05:07 科技观察

分享之前,之前已经介绍过vue1.0是如何实现observer和watcher的。本来还想继续写的,没想到vue2.0就这么横空出世了。所以只看vue2.0。这篇文章在公司分享,终于写出来了。我们用最精简的代码还原vue2.0响应式架构来实现之前写的vue源码分析。如何实现observer和watcher可以作为本次分享的参考。但是不看也没关系,但是***看懂了Object.defineProperty这篇文章分享的内容才能看懂vue2.0的响应式架构,这也是它比reactin快的原因之一下图。constdemo=newVue({data:{text:"before",},//对应的模板为

{{text}}}
render(h){returnh('div',{},[h('span',{},[this.__toString__(this.text)])])}})setTimeout(function(){demo.text="after"},3000)对应的virtualdom将从
before
开始变为
after
好了,让我们开始吧!!!第一步,更改data下的所有属性来看看observable的代码classVue{constructor(options){this.$options=optionsthis._data=options.dataobserver(options.data,this._update)this._update()}_update(){this.$options.render()}}functionobserver(value,cb){Object.keys(value).forEach((key)=>defineReactive(value,key,value[key],cb))}functiondefineReactive(obj,key,val,cb){Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:()=>{},set:newVal=>{cb()}})}vardemo=newVue({el:'#demo',data:{text:123,},render(){console.log("Iwanttorender")}})setTimeout(function(){demo._data.text=444},3000)为了演示,我们只考虑最简单的情况,如果你看vue源码分析中如何实现observer和watcher可能比较容易理解,但是没关系,简单说几句吧。这段代码要实现的功能是使用vardemo=newVue({el:'#demo',data:{text:123,},render(){console.log("我要渲染")}})observer中数据中的所有属性,然后数据中的属性,比如文本,都会被改变,这会导致调用_update()函数,然后重新渲染,怎么做,我们知道赋值时需要更改它,对吗?当我给data下的text赋值时,set函数就会被触发。这时候调用_update就可以了,但是setTimeout(function(){demo._data.text=444},3000)demo._data.text没有demo.text就爽了,没关系,我们加个代理_proxy(键){constself=thisObject.defineProperty(self,key,{configurable:true,enumerable:true,get:functionproxyGetter(){returnself._data[key]},set:functionproxySetter(val){self._data[key]=val}})}然后添加到Vue的构造函数上面这句Object.keys(options.data).forEach(key=>this._proxy(key))第一步到这里,我们会发现一个问题,value数据中的任何属性发生变化,都会触发_update然后重新渲染。属性显然不够准确。第二步,详细解释第一步不够准确的原因。例如,考虑以下代码newVue({template:`
name:{{name}}
age:{{age}}
`,data:{name:'js',age:24,height:180}})setTimeout(function(){demo.height=181},3000)template只用了data上的name和age这两个属性,但是当我改变高度时,使用第一步代码,会不会触发重新-渲染?是的!,但实际上不需要触发重新渲染,这就是问题所在!!第三步,如何解决上面的问题,先简单说一下virtualDOM,template***被编译成renderfunction(具体怎么做,我就不展开了,后面再说),以及那么render函数执行后会得到一个虚拟DOM。为了更好的理解,我们写一个最简单的虚拟DOM函数VNode(tag,data,children,text){return{tag:tag,data:data,children:children,text:text}}classVue{constructor(options){this.$options=optionsconstvdom=this._update()console.log(vdom)}_update(){returnthis._render.call(this)}_render(){constvnode=this.$options.render.call(this)returnvnode}__h__(tag,attr,children){returnVNode(tag,attr,children.map((child)=>{if(typeofchild==='string'){returnVNode(undefined,undefined,undefined,child)}else{returnchild}}))}__toString__(val){returnval==null?'':typeofval==='object'?JSON.stringify(val,null,2):String(val);}}vardemo=newVue({el:'#demo',data:{text:"before",},render(){returnthis.__h__('div',{},[this.__h__('span',{},[this.__toString__(this.text)])])}})让我们运行它,它会输出{tag:'div',data:{},children:[{tag:'span',data:{},children:[{children:undefined,data:undefined,tag:undefined,text:''//正常情况是前面的字符串,因为我们不写agent的代码来演示,所以这里是空的}]}]}This是最简单的虚拟DOM,tag是html标签的名称,data包含class、style等标签上的属性,childen是子节点。virtualDOM我就不展开了,回到原题,也就是说,我你要知道render函数中依赖了vue实例中的哪些变量(只考虑render,因为template也会帮你将其编译成渲染)。描述有点啰嗦,我们看代码vardemo=newVue({el:'#demo',data:{text:"before",name:"123",age:23},render(){returnthis.__h__('div',{},[this.__h__('span',{},[this.__toString__(this.text)])])}})就像这段代码,render函数其实只依赖ontext,notonnameandage,所以我们只需要在文字发生变化时自动触发render函数生成一个虚拟DOM(剩下的就是将这个虚拟DOM和之前的虚拟DOM进行比较,然后再操作真实的DOM,只是我后面会讲),那么我们正式考虑第三步,'touch'获取依赖返回上图,我们知道data上的属性设置为defineReactive后,修改值上的数据将触发设置。那么我们在取data的上限值的时候就会触发get。是的,我们可以玩点花样,我们先执行render,看看datatriggerget到了哪些属性,难道我们不知道datarender上会依赖哪些变量吗。然后我们操作这些变量,每次这些变量发生变化,我们就触发render。上述步骤简单地总结为四个子摘要作为计算依赖。(其实不仅仅是render,任何一个变量的变化都是由其他变量的变化引起的,可以用上面的方法,这就是computed和watch的原理,这也是mobx的核心。)第一步,我们写了一个依赖集合类,data上的每个对象都可能依赖render函数,所以在defineReactive的时候对每个属性进行初始化,简单来说就是这个classDep{constructor(){this.subs=[]}add(cb){this.subs.push(cb)}notify(){console.log(this.subs);this.subs.forEach((cb)=>cb())}}functiondefineReactive(obj,key,val,cb){constdep=newDep()Object.defineProperty(obj,key,{//omitted})}然后,当render函数执行到'touch'依赖时,会执行依赖变量get,然后我们将您可以将此渲染功能添加到subs。当我们设置的时候,我们执行notify来执行subs数组中的所有函数,包括render的执行。至此就完成了整张图,好我们将所有的代码显示出来functionVNode(tag,data,children,text){return{tag:tag,data:data,children:children,text:text}}classVue{constructor(options)){this.$options=optionsthis._data=options.dataObject.keys(options.data).forEach(key=>this._proxy(key))observer(options.data)constvdom=watch(this,this._render.bind(this),this._update.bind(this))console.log(vdom)}_proxy(key){constself=thisObject.defineProperty(self,key,{configurable:true,enumerable:true,get:functionproxyGetter(){returnsself._data[key]},set:functionproxySetter(val){self._data.text=val}})}_update(){console.log("我需要更新");constvdom=this._render.call(this)console.log(vdom);}_render(){returnthis.$options.render.call(this)}__h__(tag,attr,children){returnVNode(tag,attr,children.map((child)=>{if(typeofchild==='string'){returnVNode(undefined,undefined,undefined,child)}else{returnchild}}))}__toString__(val){returnval==null?'':typeofval==='object'?JSON.stringify(val,null,2):String(val);}}functionobserver(value,cb){Object.keys(value).forEach((key)=>defineReactive(value,key,value[key],cb))}functiondefineReactive(obj,key,val,cb){constdep=newDep()Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:()=>{if(Dep.target){dep.add(Dep.target)}returnval},set:newVal=>{if(newVal===val)returnval=newValdep.notify()}})}functionwatch(vm,exp,cb){Dep.target=cbreturnexp()}classDep{constructor(){this.subs=[]}添加(cb){this.subs.push(cb)}notify(){this.subs.forEach((cb)=>cb())}}Dep.target=nullvardemo=newVue({el:'#demo',data:{text:"before",},render(){returnthis.__h__('div',{},[this.__h__('span',{},[this.__toString__(this.text)])])}})setTimeout(function(){demo.text="after"},3000)我们来看看运行结果。解释下Dep.target是因为我们要区分普通的get和找依赖的时候get,都是在找依赖的时候赋值functionwatch(vm,exp,cb){Dep.target=cbreturnexp()}Dep.target,相当于flag,然后getwhenget:()=>{if(Dep.target){dep.add(Dep.target)}returnval},判断一下就可以了至此,如果我们再看这张图,是不是就清楚多了?总之,非常喜欢。以上vue2.0代码以最简单的方式呈现,方便展示。但是整个代码执行过程甚至命名方式都和vue2.0是一样的。与react相比,vue2.0自动帮你监控依赖,自动重新渲染,而react需要做很多工作来实现性能优化,比如我之前分享过如何最大化react(前传)的性能,即whyreact必须使用immutable.jsreact来实现纯render,bind(this)隐患。而vue2.0自然帮你做到了,而且对于标签上千百年不变的staticclass属性,vue2.0重新渲染后做diff的时候不比,vue2.0比达到性能优化后的反应更快的原因之一,源代码在这里。喜欢的话记得给个star哦😍后续我会简单说一下vue2.0的diff。