前言在看Vue源码的时候,觉得需要总结一下Vue的这些核心概念,于是写了一篇文章,忘记了回来看.模板渲染Vue使用声明式渲染,不同于命令式渲染。声明式渲染只需要告诉程序我们想要什么效果,其他的事情让程序自己去做。命令式渲染需要命令程序根据命令一步步执行渲染。例如:letarr=[1,2,3,4,5];//命令式渲染,关心每一步每一个过程。使用命令实现letnewArr=[];for(leti=0;i{this.defineReactive(data,key,data[key]);})}defineReactive(data,key,value){this.observe(value);让dep=newDep();这个.$dps.push(dep);Object.defineProperty(data,key,{enumerable:true,configurableable:false,get(){//由于需要在闭包中添加watcher,所以通过Dep定义了一个全局的target属性,//暂存watcher,添加后移除if(Dep.target)//dep.addSub(Dep.target);dep.depend();/***dep.depend();*两种写法一致*/返回值;},})}}Dep类首先定义一个全局uid,方便每个dep实例在创建dep时绑定自增1。每个dep都会有一个subs队列,里面存放着watcher的每一个数据以及里面所有的对象变量,唯一对应一个dep。如果想从model->View实现绑定,只需要这样做,把所有的publisherwatchers放到一个dep中即可。当改变一个变量时,只需要获取这个变量对应的dep,因为dep有一个subs队列,里面存放了所有相关的publisherwatcher,只需要遍历subs,调用publisher的update方法更新Page,这就是设计dep类的思路。letguid=0;exportdefaultclassDep{statictarget:any=null;潜艇:阵列<任何>;uid:数字;constructor(){this.subs=[];这个.uid=guid++;}addSub(sub){这个。潜艇。推(分);}depend(){部门。目标。添加深度(这个);}通知(){这个。潜艇。forEach(sub=>{sub.update();})}}Dep.target=null;Dep.target是一个静态变量。所有dep实例的目标都指向同一事物。也就是说,这个目标是全球唯一的。可以理解为全局变量,其实是一个watcher。当defineProperty的get事件被触发时,就会进行依赖收集。编译模板编译器的主要作用是将html节点与watcher关联起来。至于如何更新html内容,由watcher的callback/updater函数决定。这里暂时不深入,只需要知道更新DOM的是一个watcher。watcher和dep绑定到这一步,model关闭了自己的dep,html节点也关联了watcher,watcher几乎被推入对应的dep。然后看Watcher类exportdefaultclassWatcher{privatevm;私人经验;私人转帐;私人价值;私有depIds={};构造函数(vm,exp,cb){this.vm=vm;这个.exp=exp;这个.cb=cb;//创建时必须触发依赖集合this.triggerDepCollection();}更新(){this.triggerDepCollection();this.cb(this.value);}addDep(dep){if(!this.depIds.hasOwnProperty(dep.uid)){dep.addSub(this);this.depIds[dep.uid]=dep;}}//收集依赖因为definePropreity的get()被触发//或者重新收集triggerDepCollection(){Dep.target=this;this.value=this.getDeepValue();Dep.target=null;}getDeepValue(){让数据=this.vm.$data;this.exp.split(".").forEach(key=>{data=data[key];})返回数据;}}在编译html代码的时候,遇到一个需要收集的变量,现在为它创建一个watcher,在watcher里面和dep建立连接。我们称此步骤为依赖集合。我们可以看到,在构造函数的最后一行,triggerDepCollection()表示watcher自己触发依赖收集。这个函数首先把我们前面说的Dep.target设置为watcher本身,然后getDeepValue()这里只需要知道你访问了一次exp变量,就会触发exp变量的get事件,也就是提醒depofexp,"Youcancollectme",get事件的主要内容就是收集这个依赖,然后结合开头提到的代码,触发dep.depend()。如果上面defineReactive方法中get方法中的(Dep.target)dep.depend()调用了dep的Dep.target.addDep(this),也就是当前watcher的addDep(this),watcher的addDep(this)调用这个部门的addSub()。本来收集依赖只需要dep调用自己的addSub(watcher),把watcher推到自己的subs队列即可。但是现在,dep把自己传给watcher,watcher再把自己传给dep,dep再把watcher加到自己的队列里,岂不是多此一举?其实并不是。它位于观察者的addDep步骤。关键是判断dep的uid是不是自己添加的dep,也可以使用defineReactive方法中的set来实现。每次调用update()都会触发对应属性的getDeepvalue,在getDeepvalue中会触发dep.depend(),然后会触发这里的addDep。1、如果对应属性的dep.id已经在当前watcher的depIds中,说明不是新属性,只是改变了它的值而已,所以不需要再添加当前watcher属性的深度。2.如果对应的属性是新属性,则将当前watcher添加到新属性的dep中,因为新属性之前的setter和dep已经过期,如果没有将watcher添加到新属性的dep中,通过对象。当赋值xxx=xxx时,对应的watcher将收不到通知,即无效。因此,必须为每个更新重新收集依赖项。3、每个子属性的watcher添加到子属性的dep中,同时也会添加到父属性的dep中。它在监视子属性的同时监视父属性的变化。这样当父属性发生变化时,子属性的watcher也可以收到更新的通知,这一步是在this.get()-->this.getVMVal()中完成的,forEach会开??始获取来自parent的value,间接调用它的getter,触发addDep(),在整个forEach过程中,currentwatcher会被添加到各个父进程属性的dep中,例如:currentwatcher是child.child.name,那么child,child.child,child.child.name三个属性的dep会添加Currentwatcher。至此,所有内容就完成了,watcher也绑定了dep。VirtualDOM在Vue中,模板被编译成浏览器可执行的渲染函数,然后配合响应式系统将渲染函数挂载在render-watcher中。当有数据变化时,调度中心Dep通知render-watcher执行render函数,完成视图的渲染和更新。整个过程看似很顺利,但是在执行render函数的时候,如果每次都删除DOM并重新构建,无疑是执行性能上的巨大损失,因为我们知道浏览器的DOM是“昂贵”的,当我们频繁更新DOM时,会出现一定的性能问题。为了解决这个问题,Vue使用JS对象来抽象浏览器的DOM。这种抽象称为虚拟DOM。VirtualDOM的每个节点都被定义为一个VNode。每次执行render函数时,Vue都会对更新前后的VNode进行一次Diff比较,找出尽可能少的需要更新的真实DOM节点,然后只更新需要更新的节点。从而解决频繁更新DOM带来的性能问题。VNodeVNode,全称virtualnode,即虚拟节点,是对真实DOM节点的虚拟描述。在Vue的每个组件实例中,都会挂载一个$createElement函数,所有的VNode都是由这个函数创建的。比如创建一个div://Declarerenderfunctionrender:function(createElement){//你也可以使用this.$createElement创建一个VNodereturncreateElement('div','hellowworld');}//上面的render方法返回一个html片段helloworld
render函数执行后,会根据VNodeTree映射VNode生成真实的DOM,从而完成视图的渲染。DiffDiff比较新旧VNode节点,然后以最小为单位逐个修改视图,而不是根据新的VNode重新绘制整个视图,从而达到提升性能的目的。patchVue里面的diff叫做patch。它的diff算法是比较同一层的树节点,而不是逐层搜索遍历树,所以时间复杂度仅为O(n),是一种非常高效的算法。首先定义新旧节点是否相同判断函数sameVnode:满足key值key和tag名称tag必须一致等条件,返回true,否则返回false。打补丁前,新旧VNode是否满足条件sameVnode(oldVnode,newVnode),条件满足后进入patchVnode流程,否则判断为不同节点,移除旧节点,新节点节点将被创建。patchVnodepatchVnode的主要作用是决定如何更新子节点。如果新旧VNode都是静态的,并且它们的key相同(代表同一个节点),而新的VNode是clone或者标记一次(标记v-once属性,Onlyrenderonce),那么只需要更换DOM和VNode。新老节点都有子节点,然后对子节点进行diff操作,updateChildren,这也是diff的核心。1.如果旧节点没有子节点,新节点有子节点,先清空旧节点DOM的文本内容,再在当前DOM节点中添加子节点。2.当新节点没有子节点,旧节点有子节点时,移除DOM节点的所有子节点。3.当新旧节点都没有子节点时,只是文本替换。updateChildrenDiff的核心是通过比较新旧子节点数据来决定如何操作子节点。在比较过程中,由于旧的子节点有当前真实DOM的引用,新的子节点只是一个VNode数组,所以遍历过程中,如果发现需要更新真实DOM,就会直接对旧的子节点进行真正的DOM操作。当遍历结束时,新旧子节点已经同步。1、updateChildren内部定义了4个变量,分别是oldStartIdx、oldEndIdx、newStartIdx、newEndIdx,分别代表Diff比较的新旧子节点的左右边界点索引。在old子节点数组中,索引在oldStartIdx和oldEndIdx之间。表示old子节点是已经遍历过的节点,所以小于oldStartIdx或者大于oldEndIdx的代表没有遍历过的节点。2、同理,在新的子节点数组中,索引在newStartIdx和newEndIdx之间的节点表示旧子节点是已经遍历过的节点,所以小于newStartIdx或者大于newEndIdx的都代表没有遍历过的节点被遍历。3、每遍历一次,oldStartIdx和oldEndIdx以及newStartIdx和newEndIdx的距离都会向中间靠拢。当oldStartIdx>oldEndIdx或newStartIdx>newEndIdx时结束循环。4、遍历中,取出4个索引对应的Vnode节点:(1)。oldStartIdx:oldStartVnode(2)。oldEndIdx:oldEndVnode(3)。newStartIdx:newStartVnode(4).newEndIdx:newEndVnode5。在diff过程中,如果有key,并且满足sameVnode,则重复使用DOM节点,否则创建新的DOM节点。比较过程首先将oldStartVnode、oldEndVnode和newStartVnode、newEndVnode两两进行比较,共有2*2=4种比较方式。情况一:oldStartVnode和newStartVnode遇到sameVnode,然后oldStartVnode和newStartVnode执行patchVnode,oldStartIdx和newStartIdx向右移动。情况2:与情况1类似,oldEndVnode和newEndVnode遇到sameVnode时,oldEndVnode和newEndVnode为patchVnode,oldEndIdx和newEndIdx向左移动。情况三:oldStartVnode和newEndVnode遇到同一个Vnode,说明oldStartVnode已经落后于oldEndVnode。此时oldStartVnode和newEndVnode在执行patchVnode的同时,需要将oldStartVnode真正的DOM节点移到oldEndVnode的后面,oldStartIdx移到右边。newEndIdx左移。情况四:与情况三类似,当oldEndVnode和newStartVnode满足sameVnode时,说明oldEndVnode先于oldStartVnode。此时oldEndVnode和newStartVnode在执行patchVnode的同时,需要将oldEndVnode真正的DOM节点移到oldStartVnode的前面。oldStartIdx右移,newEndIdx左移。如果不存在,说明newStartVnode是一个新节点,新建一个节点放在oldStartVnode前面即可。当oldStartIdx>oldEndIdx或newStartIdx>newEndIdx时,循环结束。这时候我们就需要处理那些没有遍历过的VNode。当oldStartIdx>oldEndIdx时,表示旧节点已经遍历完,新节点还没有遍历。这时候需要创建新节点,放在oldEndVnode后面。当newStartIdx>newEndIdx时,表示新节点已经遍历完,旧节点还没有遍历。这时候应该删除所有没有遍历过的老节点。借用网上的一个动画来说明以上部分内容的出处以及本人复习时的网络搜索,也主要用于个人学习,相当于一个记事本的存在,链接的文章就不一一列举了暂且。如果作者看到了,可以联系我,贴出原文链接。