当前位置: 首页 > Web前端 > vue.js

vue源码解读(二)说说vue中的VirtualDOM

时间:2023-04-01 01:02:56 vue.js

欢迎star我的github仓库一起学习~目前vue源码学习系列已经更新5篇了~https://github.com/yisha0307/...快速跳转:Vue的双向绑定原理(已完结)说说VirtualDOM在vue中(已完结)Reactdiff与Vuediff的区别在Vue中实现异步更新策略(已完结)Vuex实现理解Typescript学习笔记(持续更新)Vue源码中闭包的使用(已完成)真实DOM的运行过程首先回顾一下真实DOM的解析和渲染过程。浏览器的渲染机制主要有以下步骤:创建DOM树——CreateStyleRules——创建Render树——Layout布局——DrawPainting使用HTML分析器分析HTML元素,构建DOM树;使用css分析器分析样式文件和元素上的内联样式,生成样式表;结合DOM树和样式表,每个DOM都有一个attach方法,接受对应的样式信息,返回一个rendertree。通过渲染树,可以确定每个节点在显示上的精确定位(布局步骤);render树和各个节点的坐标也是有的,调用各个节点的paint节点来绘制。可以看出,操作真实DOM的步骤很多,真实DOM坐标点的计算非常复杂。虽然我们现在的电脑更新迭代很快,但是DOM的成本还是很昂贵的,频繁的操作还是会造成页面卡顿,影响用户体验。虚拟DOM和VNODE虚拟DOM是一个可以模拟真实DOM节点的JS对象。好处是在数据更新的时候,不需要经过解析渲染真实DOM这五个步骤,只要在js对象中体现出更新的diff即可。操作JS肯定会比操作DOM快很多,避免了很多计算,更新完成后,将最终的虚拟DOM映射到真实DOM。vue.js中的虚拟DOM称为VNODE节点,源码见vue-src/core/vdom中类VNode的定义。比如虚拟DOM(也叫VNODE)的JS对象,基本上是这样的:class:'demo'}text:'hello,VNode'}]}定义了这个节点的tagName是div,className是test,有一个span子节点,className是demo,text是hello,VNODE,所以映射真正的dom是这样的:hello,VNode

updateview回顾一下Vue中的双向绑定原理的内容上一节,视图的更新主要是通过Observer绑定数据之后。一旦数据更新,set方法会调用对应dep的notify()通知watchers更新。watcher需要调用get方法获取值,字符串|函数参数expOrFn是在实例化watcher类时决定watcher.get()结果的元素(详见上一节watcher代码)。然后我们去找实例化watcher的代码,于是找到了mountComponent的方法(其实就是挂载组件的方法,如上一节的图所示,第一次挂载组件的时候,会接触数据,收集依赖,绑定观察者),你可以看到这个expOrFn是什么。(很多与本节讨论无关的代码也省略了)code/*updateComponentasWatcher对象的getter函数,用来依赖集合*/letupdateComponent/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&mark){//blablabla}else{updateComponent=()=>{vm._update(vm._render(),hydrating)}}/*这里,为vm注册了一个Watcher实例。Watcher的getter是updateComponent函数,用于触发渲染需要的所有数据的getter。对于依赖收集,Watcher实例将存在于渲染所需的所有数据的闭包Dep*/vm._watcher=newWatcher(vm,updateComponent,noop)hydrating=false//省略以下代码//...所以,Watcher实际上是通过get方法执行vm._update(vm._render(),hydrating)。所以看一下_update方法:Vue.prototype._update=function(vnode:VNode,hydrating?:boolean){constvm:Component=this/*如果组件已经挂载,说明进入这一步是一个更新进程,触发beforeUpdatehook*/if(vm._isMounted){callHook(vm,'beforeUpdate')}constprevEl=vm.$elconstprevVnode=vm._vnodeconstprevActiveInstance=activeInstanceactiveInstance=vmvm._vnode=vnode//Vue.prototype.__patch__被注入入口点//基于所使用的渲染后端。/*基于渲染后端Vue.prototype.__patch__作为入口点*/if(!prevVnode){//初始渲染vm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/*removeOnly*/,vm.$options._parentElm,vm.$options._refElm)}else{//更新vm.$el=vm.__patch__(prevVnode,vnode)}activeInstance=prevActiveInstance//更新__vue__引用/*Update新实例对象的__vue__*/if(prevEl){prevEl.__vue__=null}if(vm.$el){vm.$el.__vue__=vm}//如果parent是HOC,也更新它的$elif(vm.$vnode&&vm.$parent&&vm.$vnode===vm.$parent._vnode){vm.$parent.$el=vm.$el}//updatedhook由调度程序调用以确保子级//在父级的updatedhook中更新。}可以看到,_update最重要的方法是调用__patch__如果是第一次render,vm.$el就是vm.__patch__(vm.$el,vnode,hydrating,false,vm.$options._parentElm,vm.$options._refElm);如果是update,vm.$el等于vm.__patch__(prevVnode,vnode)。接下来,我们来研究一下什么是__patch__。__patch__和sameVnode决定了在vue.js中,__patch__的作用其实是比较新旧节点,然后用比较结果的最小级别修改视图,而不是根据新绘制的vnode重置整个视图.所以__patch__中,其实就是vue的diff算法。diff算法是比较同一层的树节点,而不是逐层搜索遍历树,所以时间复杂度仅为O(n),是一种相当高效的算法。diff算法的核心是patchVnode只有在oldVnode和vnode在同一个Vnode时才会执行,即patchVnode过程只有在确定新旧VNode节点为同一节点时才会执行,否则将创建一个新的DOM并删除旧的DOM。那么什么样的节点是sameNode呢?/*判断两个VNode节点是否是同一个节点,需要满足以下条件:key是同一个tag(当前节点的标签名),同一个isComment(是否是注释节点)是相同的数据(当前节点对应的对象,包括一些具体的Data信息是一个VNodeData类型,可以参考VNodeData类型中的数据信息)在label为时定义,类型必须相同*/functionsameVnode(a,b){return(a.key===b.key&&a.tag===b.tag&&a.isComment===b.isComment&&isDef(a.data)===isDef(b.data)&&sameInputType(a,b))}//有些浏览器不支持为动态改变类型//所以需要把它们当作不同的节点/*判断类型是否相同时标签是有些浏览器不支持类型的动态修改,所以他们被当作不同的节点*/functionsameInputType(a,b){if(a.tag!=='input')return真的让我常量typeA=isDef(i=a.data)&&isDef(i=i.attrs)&&i.typeconsttypeB=isDef(i=b.data)&&isDef(i=i.attrs)&&i.type返回typeA===typeB}所以我们可以得出结论,只有a和b两个节点的key和tagName相同,都是注释节点或者都不是注释节点,定义了data,如果输入tagName这时,当输入类型相同时,可以说a和b是同一个Vnode。这就是为什么我们在使用v-for的时候需要传入key,并且key建议是唯一的比如id或者name,并且可以对应到节点。值,不是索引,与节点数据无关。当a和b判断为sameVnode时,会执行patchVnode操作。关于diff算法,我打算写一章reactdiff和vuediff的实现差异,到时候详细讨论。