最小的网站源码分享(网站最小字体)

老铁们,大家好,相信还有很多朋友对于最小的网站源码分享和网站最小字体的相关问题不太懂,没关系,今天就由我来为大家分享分享最小的网站源码分享以及网站最小字体的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!

前言

源码阅读可能会迟到,但是一定不会缺席!

众所周知,以下代码就是vue的一种直接上手方式。通过cdn可以在线打开vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。

<html>\n<head></head>\n<body>\n<divid=&34;>\n{{message}}\n</div>\n</body>\n<scriptsrc=&34;></script>\n<script>\nvarapp=newVue({\nel:&app&39;SeeVueagain!&39;object&39;undefined&39;function&39;usestrict&39;object&39;undefined&39;function&34;纯粹的对象&39;slot,component&39;slot&39;slot1&39;key,ref,slot,slot-scope,is&34;bilibli&34;b&34;i&34;l&34;i&34;b&34;l&34;i&34;parent&34;height:0&34;2222&39;div&34;2222&34;parent&34;0&34;111111&39;__ob__&39;production&39;length&6813,out-intransitions).\n//Also,using(macro)tasksineventhandlerwouldcausesomeweirdbehaviors\n//thatcannotbecircumvented(e.g.7153,7834,4521,6566).\n\n在vue2.5之前的版本中,nextTick基本上基于microtask来实现的,但是在某些情况下microtask具有太高的优先级,并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。但是如果全部都改成macrotask,对一些有重绘和动画的场景也会有性能影响,如issue39;name&39;name&39;change&39;setter前:&39;vue3&39;同步方式:&34;setTimeout方式:&39;setter后:&39;Promise方式:&34;元编程&39;c&34;object&39;sdata.renderStatic//Runtimehelperforrenderingstatictrees.markOnce//Runtimehelperforv-once.

这一部分讲的是辅助程序——Vue的各类渲染方法,从字面意思中可以知道一些方法的用途,这些方法用在Vue生成的渲染函数中。

installRenderHelpers//installRenderHelpers用于执行以上。

第2962行至第3515行

FunctionalRenderContext//创建一个包含渲染要素的函数createFunctionalComponent

函数式组件的实现

Ctor,//Ctro:组件的构造对象(Vue.extend()里的那个Sub函数)\npropsData,//propsData:父组件传递过来的数据(还未验证)\ndata,//data:组件的数据\ncontextVm,//contextVm:Vue实例\nchildren//children:引用该组件时定义的子节点\n

//createFunctionalComponent最后会执行我们的render函数

特注:Vue组件是Vue的核心之一

组件分为:异步组件和函数式组件

这里就是函数式组件相关

Vue提供了一种可以让组件变为无状态、无实例的函数化组件。从原理上说,一般子组件都会经过实例化的过程,而单纯的函数组件并没有这个过程,它可以简单理解为一个中间层,只处理数据,不创建实例,也是由于这个行为,它的渲染开销会低很多。实际的应用场景是,当我们需要在多个组件中选择一个来代为渲染,或者在将children,props,data等数据传递给子组件前进行数据处理时,我们都可以用函数式组件来完成,它本质上也是对组件的一个外部包装。

函数式组件会在组件的对象定义中,将functional属性设置为true,这个属性是区别普通组件和函数式组件的关键。同样的在遇到子组件占位符时,会进入createComponent进行子组件Vnode的创建。**由于functional属性的存在,代码会进入函数式组件的分支中,并返回createFunctionalComponent调用的结果。**注意,执行完createFunctionalComponent后,后续创建子Vnode的逻辑不会执行,这也是之后在创建真实节点过程中不会有子Vnode去实例化子组件的原因。(无实例)

官方说明

cloneAndMarkFunctionalResultmergePropscomponentVNodeHookscreateComponent

//createComponent方法创建一个组件的VNode。这createComponent是创建子组件的关键

//创建组件的VNode时,若组件是函数式组件,则其VNode的创建过程将与普通组件有所区别。

createComponentInstanceForVnode//linkinstallComponentHooks//installComponentHooks就是把componentVNodeHooks的钩子函数合并到data.hook中,,在合并过程中,如果某个时机的钩子已经存在data.hook中,那么通过执行mergeHook函数做合并勾子。mergeHook$1transformModelcreateElement//创建元素_createElementapplyNSregisterDeepBindingsinitRender//初识渲染

阶段小结

这一部分主要是围绕Vue的组件的创建。Vue将页面划分成各类的组件,组件思想是Vue的精髓之一。

第3517行至第3894行

renderMixin//引入视图渲染混合函数ensureCtorcreateAsyncPlaceholderresolveAsyncComponentisAsyncPlaceholdergetFirstComponentChildinitEvents//初始化事件addremove$1createOnceHandlerupdateComponentListenerseventsMixin//挂载事件响应相关方法

第3898行至第4227行

setActiveInstanceinitLifecyclelifecycleMixin//挂载生命周期相关方法mountComponentupdateChildComponentisInInactiveTreeactivateChildComponentdeactivateChildComponentcallHook

几乎所有JS框架或插件的编写都有一个类似的模式,即向全局输出一个类或者说构造函数,通过创建实例来使用这个类的公开方法,或者使用类的静态全局方法辅助实现功能。相信精通Jquery或编写过Jquery插件的开发者会对这个模式非常熟悉。Vue.js也如出一辙,只是一开始接触这个框架的时候对它所能实现的功能的感叹盖过了它也不过是一个内容较为丰富和精致的大型类的本质。

阶段小结

这里要对js的继承有一个深刻的理解。

类继承

functionAnimal(){\nthis.live=true;\n}\nfunctionDog(name){\nthis.name=name\n}\nDog.prototype=newAnimal()\nvardog1=newDog(&34;)\nconsole.log(dog1)//Dog{name:&34;}\nconsole.log(dog1.live)//true\n

构造继承

functionAnimal(name,color){\nthis.name=name;\nthis.color=color;}\nfunctionDog(){\nAnimal.apply(this,arguments)\n}\nvardog1=newDog(&34;,&34;)\nconsole.log(dog1)//Dog{name:&34;,color:&34;}\n

组合继承(类继承+构造继承)

functionAnimal(name,color){\nthis.name=name;\nthis.color=color;\nthis.live=true;\n}\nfunctionDog(){\nAnimal.apply(this,arguments);\n}\nDog.prototype=newAnimal()\nvardog1=newDog(&34;,&34;)\nconsole.log(dog1)//Dog{name:&34;,color:&34;,live:true}\n

寄生组合式继承extend继承

Vue同Jquery一样,本质也是一个大型的类库。

//定义Vue构造函数,形参options

functionVue(options){\nif(process.env.NODE_ENV!==&39;&&!(thisinstanceofVue){\nwarn(&39;)\n}\n//…\nthis._init(options)\n}\n

//功能函数

//引入初始化混合函数\nimport{initMixin}from&39;\n//引入状态混合函数\nimport{stateMixin}from&39;\n//引入视图渲染混合函数\nimport{renderMixin}from&39;\n//引入事件混合函数\nimport{eventsMixin}from&39;\n//引入生命周期混合函数\nimport{lifecycleMixin}from&39;\n//引入warn控制台错误提示函数\nimport{warn}from&39;\n…\n\n//挂载初始化方法\ninitMixin(Vue)\n//挂载状态处理相关方法\nstateMixin(Vue)\n//挂载事件响应相关方法\neventsMixin(Vue)\n//挂载生命周期相关方法\nlifecycleMixin(Vue)\n//挂载视图渲染方法\nrenderMixin(Vue)\n

第4231行至第4406行

resetSchedulerState//重置状态flushSchedulerQueue//据变化最终会把flushSchedulerQueue传入到nextTick中执行flushSchedulerQueue函数会遍历执行watcher.run()方法,watcher.run()方法最终会完成视图更新

vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom。

callUpdatedHooksqueueActivatedComponentcallActivatedHooksqueueWatcher

第4412行至第4614行

Watcher//!important重中之重的重点

这一part在Watcher的原型链上定义了get、addDep、cleanupDeps、update、run、evaluate、depend、teardown方法,即Watcher的具体实现的一些方法,比如新增依赖、清除、更新试图等。

每个Vue组件都有一个对应的watcher,这个watcher将会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候,触发组件重新渲染。

第4618行至第5071行

exportfunctioninitMixin(Vue:Class<Component>){\nVue.prototype._init=function(options?:Object){\nconstvm:Component=this\n//auid\nvm._uid=uid++\n\nletstartTag,endTag\n/*istanbulignoreif*/\nif(process.env.NODE_ENV!==&39;&&config.performance&&mark){\nstartTag=`vue-perf-start:${vm._uid}`\nendTag=`vue-perf-end:${vm._uid}`\nmark(startTag)\n}\n//如果是Vue的实例,则不需要被observe\n//aflagtoavoidthisbeingobserved\nvm._isVue=true\n//mergeoptions\n//第一步:options参数的处理\nif(options&&options._isComponent){\n//optimizeinternalcomponentinstantiation\n//sincedynamicoptionsmergingisprettyslow,andnoneofthe\n//internalcomponentoptionsneedsspecialtreatment.\ninitInternalComponent(vm,options)\n}else{\n//mergeOptions接下来我们会详细讲哦~\nvm.$options=mergeOptions(\nresolveConstructorOptions(vm.constructor),\noptions||{},\nvm\n)\n}\n//第二步:renderProxy\n/*istanbulignoreelse*/\nif(process.env.NODE_ENV!==&39;){\ninitProxy(vm)\n}else{\nvm._renderProxy=vm\n}\n//exposerealself\nvm._self=vm\n//第三步:vm的生命周期相关变量初始化\ninitLifecycle(vm)\n\n//第四步:vm的事件监听初始化\ninitEvents(vm)\n//第五步:vm的编译render初始化\ninitRender(vm)\n//第六步:vm的beforeCreate生命钩子的回调\ncallHook(vm,&39;)\n//第七步:vm在data/props初始化之前要进行绑定\ninitInjections(vm)//resolveinjectionsbeforedata/props\n\n//第八步:vm的sate状态初始化\ninitState(vm)\n//第九步:vm在data/props之后要进行提供\ninitProvide(vm)//resolveprovideafterdata/props\n//第十步:vm的created生命钩子的回调\ncallHook(vm,&39;)\n\n/*istanbulignoreif*/\nif(process.env.NODE_ENV!==&39;&&config.performance&&mark){\nvm._name=formatComponentName(vm,false)\nmark(endTag)\nmeasure(`vue${vm._name}init`,startTag,endTag)\n}\n//第十一步:render&mount\nif(vm.$options.el){\nvm.$mount(vm.$options.el)\n}\n}\n}\n

主要是为我们的Vue原型上定义一个方法_init。然后当我们执行newVue(options)的时候,会调用这个方法。而这个_init方法的实现,便是我们需要关注的地方。前面定义vm实例都挺好理解的,主要我们来看一下mergeOptions这个方法,其实Vue在实例化的过程中,会在代码运行后增加很多新的东西进去。我们把我们传入的这个对象叫options,实例中我们可以通过vm.$options访问到。

0至5000行总结

从0至5000行我们可以清晰看到Vue模板编译的轮廓了。

笔者将这一部分出现的关键词进行按顺序罗列:function(global,factory)工具函数DepObserveVNodenextTick事件机制RendercomponentsWatcher

我们可以总结:Vue的核心就是VDOM!对DOM对象的操作调整为操作VNode对象,采用diff算法比较差异,一次patch。

render的流程是:

Vue使用HTML的Parser将HTML模板解析为ASTfunctionrender(){}VirtualDOMwatcher将会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候,触发组件重新渲染

第5073行至第5446行

//定义Vue构造函数\nfunctionVue(options){\nif(!(thisinstanceofVue)\n){\nwarn(&39;);\n}\nthis._init(options);\n}\n\n//将Vue作为参数传递给导入的五个方法\ninitMixin(Vue);//初始化Mixin\nstateMixin(Vue);//状态Mixin\neventsMixin(Vue);//事件Mixin\nlifecycleMixin(Vue);//生命周期Mixin\nrenderMixin(Vue);//渲染Mixin\n

这一部分就是初始化函数的调用。

//\nObject.defineProperty(Vue.prototype,&39;,{\nget:isServerRendering\n});\n

为什么这么写?

Object.defineProperty能保护引入的库不被重新赋值,如果你尝试重写,程序会抛出“TypeError:Cannotassigntoreadonlyproperty”的错误。

【译】Vue框架引入JS库的正确姿势

//版本\nVue.version=&39;;\n

阶段小结

这一部分是Vueindex.js的内容,包括Vue的整个挂在过程

先进入initMixin(Vue),在prototype上挂载

Vue.prototype._init=function(options){}\n

进入stateMixin(Vue),在prototype上挂载Vue.prototype.$data

Vue.prototype.$props\nVue.prototype.$set=set\nVue.prototype.$delete=del\nVue.prototype.$watch=function(){}\n

进入eventsMixin(Vue),在prototype上挂载

Vue.prototype.$on\nVue.prototype.$once\nVue.prototype.$off\nVue.prototype.$emit\n

进入lifecycleMixin(Vue),在prototype上挂载

Vue.prototype._update\nVue.prototype.$forceUpdate\nVue.prototype.$destroy\n

最后进入renderMixin(Vue),在prototype上挂载Vue.prototype.$nextTick

Vue.prototype._render\nVue.prototype._o=markOnce\nVue.prototype._n=toNumber\nVue.prototype._s=toString\nVue.prototype._l=renderList\nVue.prototype._t=renderSlot\nVue.prototype._q=looseEqual\nVue.prototype._i=looseIndexOf\nVue.prototype._m=renderStatic\nVue.prototype._f=resolveFilter\nVue.prototype._k=checkKeyCodes\nVue.prototype._b=bindObjectProps\nVue.prototype._v=createTextVNode\nVue.prototype._e=createEmptyVNode\nVue.prototype._u=resolveScopedSlots\nVue.prototype._g=bindObjectListeners\n

mergeOptions使用策略模式合并传入的options和Vue.options合并后的代码结构,

可以看到通过合并策略components,directives,filters继承了全局的,这就是为什么全局注册的可以在任何地方使用,因为每个实例都继承了全局的,所以都能找到。

new一个Vue对象发生了什么:

第5452行至第5655行

//thesearereservedforwebbecausetheyaredirectlycompiledaway\n//duringtemplatecompilation\n\n//这些是为web保留的,因为它们是直接编译掉的\n//在模板编译期间\n

isBooleanAttrgenClassForVnode//class转码获取vonde中的staticClass静态class和class动态class转义成真实dom需要的class格式。然后返回class字符串mergeClassData//mergeClassDatarenderClass//渲染calss这里获取到已经转码的calssstringifyClass//转码class,把数组格式,对象格式的calss全部转化成字符串格式stringifyArray//数组字符串变成字符串,然后用空格隔开拼接起来变成字符串stringifyObject//对象字符串变成字符串,然后用空格隔开拼接起来变成字符串namespaceMapisHTMLTagisSVG//判断svg标签isUnknownElement//检查dom节点的tag标签类型是否是VPre标签或者是判断是否是浏览器自带原有的标签isTextInputType////匹配&39;

这一part没有特别要说的,主要是对class的转码、合并和其他二次封装的工具函数。实际上我们在Vue源码很多地方看到了这样的封装,在平常的开发中,我们也得要求自己封装基本的函数。如果能形成自己习惯用的函数的库,会方便很多,且对自己能力也是一个提升。

第5659行至第5792行

createElement//创建元素,实例化VNodecreateElementNScreateTextNodecreateCommentinsertBeforeremoveChildappendChildparentNodenextSiblingtagNamesetTextContentsetStyleScopenodeOps

//nodeOps:\ncreateElement:createElement$1,//创建一个真实的dom\ncreateElementNS:createElementNS,//创建一个真实的domsvg方式\ncreateTextNode:createTextNode,//创建文本节点\ncreateComment:createComment,//创建一个注释节点\ninsertBefore:insertBefore,//插入节点在xxxdom前面插入一个节点\nremoveChild:removeChild,//删除子节点\nappendChild:appendChild,//添加子节点尾部\nparentNode:parentNode,//获取父亲子节点dom\nnextSibling:nextSibling,//获取下一个兄弟节点\ntagName:tagName,//获取dom标签名称\nsetTextContent:setTextContent,////设置dom文本\nsetStyleScope:setStyleScope//设置组建样式的作用域\n

refregisterRef

//注册ref或者删除ref。比如标签上面设置了ref=&39;那么该函数就是为this.$refs.abc注册ref把真实的dom存进去

阶段小结

这里的重点想必就是“ref”了

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作DOM元素。不过也确实在一些情况下做这些事情是合适的。ref为我们提供了解决途径。

ref属性不是一个标准的HTML属性,只是Vue中的一个属性。

第5794行至第6006行

VirtualDOM!

没错,这里就是虚拟dom生成的源码相关。

sameVnodesameInputTypecreateKeyToOldIdxcreatePatchFunction//!important:patch把vonde渲染成真实的domemptyNodeAtcreateRmCbremoveNodeisUnknownElement?1createElm//创造dom节点createComponent//创建组件,并且判断它是否实例化过initComponent

createElement方法接收一个tag参数,在内部会去判断tag标签的类型,从而去决定是创建一个普通的VNode还是一个组件类VNode;

createComponent的实现,在渲染一个组件的时候的3个关键逻辑:

构造子类构造函数,安装组件钩子函数实例化vnode。createComponent后返回的是组件vnode,它也一样走到vm._update方法

我们传入的vnode是组件渲染的vnode,也就是我们之前说的vm._vnode,如果组件的根节点是个普通元素,那么vm._vnode也是普通的vnode,这里createComponent(vnode,insertedVnodeQueue,parentElm,refElm)的返回值是false。接下来的过程就系列一的步骤一样了,先创建一个父节点占位符,然后再遍历所有子VNode递归调用createElm,在遍历的过程中,如果遇到子VNode是一个组件的VNode,则重复过程,这样通过一个递归的方式就可以完整地构建了整个组件树。

initComponent初始化组建,如果没有tag标签则去更新真实dom的属性,如果有tag标签,则注册或者删除ref然后为insertedVnodeQueue.push(vnode);

第6008行至第6252行

reactivateComponentinsertcreateChildrenisPatchableinvokeCreateHookssetScopeaddVnodes//添加VnodesinvokeDestroyHookremoveVnodes//移除VnodesremoveAndInvokeRemoveHookupdateChildren//在patchVnode中提到,如果新老节点都有子节点,但是不相同的时候就会调用updateChildren,这个函数通过diff算法尽可能的复用先前的DOM节点。

//diff算法就在这里辣!

functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue){\nletoldStartIdx=0\nletnewStartIdx=0\nletoldEndIdx=oldCh.length-1\nletoldStartVnode=oldCh[0]\nletoldEndVnode=oldCh[oldEndIdx]\nletnewEndIdx=newCh.length-1\nletnewStartVnode=newCh[0]\nletnewEndVnode=newCh[newEndIdx]\nletoldKeyToIdx,idxInOld,elmToMove,refElm\n\nwhile(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){\nif(isUndef(oldStartVnode)){\noldStartVnode=oldCh[++oldStartIdx]\n}elseif(isUndef(oldEndVnode)){\noldEndVnode=oldCh[–oldEndIdx]\n}elseif(sameVnode(oldStartVnode,newStartVnode)){\npatchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)\noldStartVnode=oldCh[++oldStartIdx]\nnewStartVnode=newCh[++newStartIdx]\n}elseif(sameVnode(oldEndVnode,newEndVnode)){\npatchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue)\noldEndVnode=oldCh[–oldEndIdx]\nnewEndVnode=newCh[–newEndIdx]\n}elseif(sameVnode(oldStartVnode,newEndVnode)){\npatchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue)\ncanMove&&nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))\noldStartVnode=oldCh[++oldStartIdx]\nnewEndVnode=newCh[–newEndIdx]\n}elseif(sameVnode(oldEndVnode,newStartVnode)){\npatchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue)\ncanMove&&nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)\noldEndVnode=oldCh[–oldEndIdx]\nnewStartVnode=newCh[++newStartIdx]\n}else{\nif(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)\nidxInOld=isDef(newStartVnode.key)?oldKeyToIdx[newStartVnode.key]:null\nif(isUndef(idxInOld)){\ncreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm)\nnewStartVnode=newCh[++newStartIdx]\n}else{\nelmToMove=oldCh[idxInOld]\nif(sameVnode(elmToMove,newStartVnode)){\npatchVnode(elmToMove,newStartVnode,insertedVnodeQueue)\noldCh[idxInOld]=undefined\ncanMove&&nodeOps.insertBefore(parentElm,newStartVnode.elm,oldStartVnode.elm)\nnewStartVnode=newCh[++newStartIdx]\n}else{\ncreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm)\nnewStartVnode=newCh[++newStartIdx]\n}\n}\n}\n}\nif(oldStartIdx>oldEndIdx){\nrefElm=isUndef(newCh[newEndIdx+1])?null:newCh[newEndIdx+1].elm\naddVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx,insertedVnodeQueue)\n}elseif(newStartIdx>newEndIdx){\nremoveVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)\n}\n}\n

checkDuplicateKeysfindIdxInOld

reactivateComponent承接上文createComponent

第6259行至第6561行

patchVnode//如果符合sameVnode,就不会渲染vnode重新创建DOM节点,而是在原有的DOM节点上进行修补,尽可能复用原有的DOM节点。invokeInsertHookisRenderedModulehydrateassertNodeMatchpatch//!important:patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例

阶段小结

Vue的核心思想:组件化。

这一部分是关于构建组件树,形成虚拟dom,以及非常重要的patch方法。

再来亿遍:

原因:当修改某条数据的时候,这时候js会将整个DOMTree进行替换,这种操作是相当消耗性能的。所以在Vue中引入了Vnode的概念:Vnode是对真实DOM节点的模拟,可以对VnodeTree进行增加节点、删除节点和修改节点操作。这些过程都只需要操作VNodeTree,不需要操作真实的DOM,大大的提升了性能。修改之后使用diff算法计算出修改的最小单位,在将这些小单位的视图进行更新。原理:data中定义了一个变量a,并且模板中也使用了它,那么这里生成的Watcher就会加入到a的订阅者列表中。当a发生改变时,对应的订阅者收到变动信息,这时候就会触发Watcher的update方法,实际update最后调用的就是在这里声明的updateComponent。

当数据发生改变时会触发回调函数updateComponent,updateComponent是对patch过程的封装。patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。

联系前后QA

Q:vue.js同时多个赋值是一次性渲染还是多次渲染DOM?

A:官网已给出答案:https://cn.vuejs.org/v2/guide/reactivity.html

可能你还没有注意到,Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际(已去重的)工作。Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn,0)代替。

例如,当你设置vm.someData=&39;,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的DOM状态来做点什么,这就可能会有些棘手。虽然Vue.js通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触DOM,但是有时我们必须要这么做。为了在数据变化之后等待Vue完成更新DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在DOM更新完成后被调用。

这样是不是有种前后连贯起来的感觉,原来nextTick是这样子的。

第6566行至第7069行

directives//官网:https://cn.vuejs.org/v2/guide/custom-directive.htmlupdateDirectives//更新指令_updatenormalizeDirectives//统一directives的格式getRawDirName//返回指令名称或者属性name名称+修饰符callHook$1//触发指令钩子函数updateAttrs//更新属性setAttr//设置属性baseSetAttrupdateClass//更新样式klassparseFilters//处理value解析成正确的value,把过滤器转换成vue虚拟dom的解析方法函数比如把过滤器&39;转换成_f(&34;)(_f(&34;)(ab))wrapFilter//转换过滤器格式baseWarn//基础警告pluckModuleFunction//循环过滤数组或者对象的值,根据key循环过滤对象或者数组[key]值,如果不存在则丢弃,如果有相同多个的key值,返回多个值的数组addProp//在虚拟dom中添加prop属性addAttr//添加attrs属性addRawAttr//添加原始attr(在预转换中使用)addDirective//为虚拟dom添加一个指令directives属性对象addHandler//为虚拟dom添加events事件对象属性

前面围绕“指令”和“过滤器”的一些基础工具函数。

后面围绕为虚拟dom添加属性、事件等具体实现函数。

第7071行至第7298行

getRawBindingAttrgetBindingAttr//获取:属性或者v-bind:属性,或者获取属性移除传进来的属性name,并且返回获取到属性的值getAndRemoveAttr//移除传进来的属性name,并且返回获取到属性的值getAndRemoveAttrByRegexrangeSetItemgenComponentModel//为虚拟dom添加model属性

/*\n*Parseav-modelexpressionintoabasepathandafinalkeysegment.\n*Handlesbothdot-pathandpossiblesquarebrackets.\n*将v-model表达式解析为基路径和最后一个键段。\n*处理点路径和可能的方括号。\n*/\n

parseModel//转义字符串对象拆分字符串对象把后一位key分离出来

//如果数据是object.info.name的情况下则返回是{exp:&34;,key:&34;}//如果数据是object[info][name]的情况下则返回是{exp:&34;,key:&34;}

nexteofparseBracket//检测匹配[]一对这样的=括号parseString//循环匹配一对&39;或者&34;符号

这一部分包括:原生指令v-bind和为虚拟dom添加model属性,以及格式校验工具函数。

第7300行至第7473行

modelgenCheckboxModel//为inputtype=&34;虚拟dom添加change函数,根据v-model是否是数组,调用change函数,调用set去更新checked选中数据的值genRadioModel//为虚拟dominpu标签type===&39;添加change事件更新值genSelect//为虚拟dom添加change函数,change函数调用set去更新select选中数据的值genDefaultModel//如果虚拟dom标签是&39;类型不是checkbox,radio或者是&39;标签的时候,获取真实的dom的value值调用change或者input方法执行set方法更新数据

阶段小结

v-bind、v-model

区别:

v-bind用来绑定数据和属性以及表达式,缩写为&39;v-model使用在表单中,实现双向数据绑定的,在表单元素外使用不起作用

Q:你知道v-model的原理吗?说说看

A:v-model本质上是语法糖,即利用v-model绑定数据,其实就是既绑定了数据,又添加了一个input事件监听

自定义指令钩子函数

一个指令定义对象可以提供如下几个钩子函数(均为可选):

1.bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。\n2.inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。\n3.update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新(详细的钩子函数参数见下)。\n4.componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用。\n5.unbind:只调用一次,指令与元素解绑时调用。\n

指令钩子函数会被传入以下参数:

1.el:指令所绑定的元素,可以用来直接操作DOM。\n2.binding:一个对象,包含以下属性:\nname:指令名,不包括v-前缀。\nvalue:指令的绑定值,例如:v-my-directive=&34;中,绑定值为2。\noldValue:指令绑定的前一个值,仅在update和componentUpdated钩子中可用。无论值是否改变都可用。\nexpression:字符串形式的指令表达式。例如v-my-directive=&34;中,表达式为&34;。\narg:传给指令的参数,可选。例如v-my-directive:foo中,参数为&34;。\nmodifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为{foo:true,bar:true}。\n3.vnode:Vue编译生成的虚拟节点。移步VNodeAPI来了解更多详情。\n4.oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中可用。\n

除了el之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的dataset来进行。

【译】vue自定义指令的魅力

第7473行至第7697行

normalizeEvents//为事件多添加change或者input事件加进去createOnceHandler$1add$1//为真实的dom添加事件remove$2updateDOMListeners//更新dom事件updateDOMProps//更新真实dom的props属性shouldUpdateValue//判断是否需要更新valueisNotInFocusAndDirtyisDirtyWithModifiers//判断脏数据修改

脏数据概念

第7699行至第7797行

domPropsparseStyleText//把style字符串转换成对象normalizeStyleData//在同一个vnode上合并静态和动态样式数据normalizeStyleBinding//将可能的数组/字符串值规范化为对象getStyle

/**\n*parentcomponentstyleshouldbeafterchild&39;sstylecouldoverrideit\n*父组件样式应该在子组件样式之后\n*这样父组件的样式就可以覆盖它\n*循环子组件和组件的样式,把它全部合并到一个样式对象中返回样式对象如{width:100px,height:200px}返回该字符串。\n*/\n

setProp//设置prop

第7799行至第7995行

normalize//给css加前缀。解决浏览器兼用性问题,加前缀updateStyle//将vonde虚拟dom的css转义成并且渲染到真实dom的csszhongaddClass//为真实dom元素添加class类removeClass//删除真实dom的css类名resolveTransition//解析vonde中的transition的name属性获取到一个css过度对象类autoCssTransition//通过name属性获取过渡CSS类名比如标签上面定义name是fadecss就要定义.fade-enter-active,.fade-leave-active,.fade-enter,.fade-leave-to这样的classnextFrame//下一帧

第7997行至第8093行

addTransitionClass//获取真实domaddTransitionClass记录calss类removeTransitionClass//删除vonde的class类和删除真实dom的class类whenTransitionEnds//获取动画的信息,执行动画。getTransitionInfo//获取transition,或者animation动画的类型,动画个数,动画执行时间

这一部分关于:对真实dom的操作,包括样式的增删、事件的增删、动画类等。

回过头再理一下宏观上的东西,再来亿遍-虚拟DOM:模板→渲染函数→虚拟DOM树→真实DOM

那么这一部分则处在“虚拟DOM树→真实DOM”这个阶段

第8093行至第8518行

getTimeout

//OldversionsofChromium(below61.0.3163.100)formatsfloatingpointernumbers\n//inalocale-dependentway,usingacommainsteadofadot.\n//Ifcommaisnotreplacedwithadot,theinputwillberoundeddown(i.e.acting\n//asafloorfunction)causingunexpectedbehaviors\n\n//根据本地的依赖方式,Chromium的旧版本(低于61.0.3163.100)格式化浮点数字,使用逗号而不是点。如果逗号未用点代替,则输入将被四舍五入而导致意外行为\n

toMs

//updatetoMsfunction.fix39;sparentforappearcheck.\n\n//activeInstance将一直作为<transition>的组件来管理transition。要检查的一种边缘情况:<transition>作为子组件的根节点时。在这种情况下,我们需要检查<transition>的父项的展现。\n

leave//离开动画performLeavecheckDuration//onlyusedindevmode:检测val必需是数字isValidDurationgetHookArgumentsLength//检测钩子函数fns的长度_entercreatePatchFunction//path把vonde渲染成真实的dom:创建虚拟dom-函数体在5845行directive//生命指令:包括插入和组件更新

更新指令比较oldVnode和vnode,根据oldVnode和vnode的情况触发指令钩子函数bind,update,inserted,insert,componentUpdated,unbind钩子函数

此节前部分是transition动画相关工具函数,后部分关于虚拟Dompatch、指令的更新。

第8520行至第8584行

setSelected//设置选择-指令更新的工具函数actuallySetSelected//实际选择,在setSelected()里调用hasNoMatchingOption//没有匹配项-指令组件更新工具函数getValue//获取option.valueonCompositionStart//组成开始-指令插入工具函数onCompositionEnd//组成结束-指令插入工具函数:防止无故触发输入事件trigger//触发事件

第8592行至第8728行

//定义在组件根内部递归搜索可能存在的transition

locateNodeshow//控制el的display属性platformDirectives//平台指令transitionProps//过渡Props对象

//incasethechildisalsoanabstractcomponent,e.g.<keep-alive>\n//wewanttorecursivelyretrievetherealcomponenttoberendered\n//如果子对象也是抽象组件,例如<keep-alive>\n//我们要递归地检索要渲染的实际组件\n

getRealChildextractTransitionData//提取TransitionDataplaceholder//占位提示hasParentTransition//判断是否有ParentTransitionisSameChild//判断子对象是否相同

第8730行至第9020行

Transition//!important

前部分以及此部分大部分围绕Transition这个关键对象。即迎合官网“过渡&动画”这一节,是我们需要关注的重点!

Vue在插入、更新或者移除DOM时,提供多种不同方式的应用过渡效果。包括以下工具:

在CSS过渡和动画中自动应用class可以配合使用第三方CSS动画库,如Animate.css在过渡钩子函数中使用JavaScript直接操作DOM可以配合使用第三方JavaScript动画库,如Velocity.js

在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的管理过渡状态。

vue-transition里面大有东西,这里有一篇“细谈”推荐阅读。

propsTransitionGroup//TransitionGroupcallPendingCbs//Pending回调recordPosition//记录位置applyTranslation//应用动画-TransitionGroup.updated调用

\n//wedividetheworkintothreeloopstoavoidmixingDOMreadsandwrites\n//ineachiteration-whichhelpspreventlayoutthrashing.\n\n//我们将工作分为三个loops,以避免将DOM读取和写入混合在一起\n//在每次迭代中-有助于防止布局冲撞。\n

platformComponents//平台组件

//安装平台运行时指令和组件\nextend(Vue.options.directives,platformDirectives);\nextend(Vue.options.components,platformComponents);\n

Q:vue自带的内置组件有什么?

A:Vue中内置的组件有以下几种:

component

component组件:有两个属性—isinline-template

渲染一个‘元组件’为动态组件,按照&39;特性的值来渲染成那个组件

transition

transition组件:为组件的载入和切换提供动画效果,具有非常强的可定制性,支持16个属性和12个事件

transition-group

transition-group:作为多个元素/组件的过渡效果

keep-alive

keep-alive:包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

slot

slot:作为组件模板之中的内容分发插槽,slot元素自身将被替换

第9024行至第9207行

//installplatformspecificutils//安装平台特定的工具

Vue.config.x

Vue.config.mustUseProp=mustUseProp;\nVue.config.isReservedTag=isReservedTag;\nVue.config.isReservedAttr=isReservedAttr;\nVue.config.getTagNamespace=getTagNamespace;\nVue.config.isUnknownElement=isUnknownElement;\n

Vue.prototype.$mount

//publicmountmethod安装方法实例方法挂载vm

//publicmountmethod\nVue.prototype.$mount=function(\nel,//真实dom或者是string\nhydrating//新的虚拟domvonde\n){\nel=el&&inBrowser?query(el):undefined;\nreturnmountComponent(this,el,hydrating)\n};\n

devtoolsglobalhook//开发环境全局hookTip

buildRegex//构建的正则匹配parseText//匹配view指令,并且把他转换成虚拟domvonde需要渲染的函数,比如指令{{name}}转换成_s(name)transformNode//获取class属性和:class或者v-bind的动态属性值,并且转化成字符串添加到staticClass和classBinding属性中genData//初始化扩展指令baseDirectives,on,bind,cloak方法,dataGenFns获取到一个数组,数组中有两个函数genData(转换class)和genData$1(转换style),transformNode1//transformNode1获取style属性和:style或者v-bind的动态属性值,并且转化成字符串添加到staticStyle和styleBinding属性中genData$1//参见genDatastyle$1//包含staticKeys、transformNode、genData属性

第9211行至第9537行

heisUnaryTag//工具函数canBeLeftOpenTag//工具函数isNonPhrasingTag//工具函数

RegularExpressions

parseHTML//解析成HTML!important

parseHTML这个函数实现大概两百多行,是一个比较大的函数体了。

parseHTML中的方法用于处理HTML开始和结束标签。

parseHTML方法的整体逻辑是用正则判断各种情况,进行不同的处理。其中调用到了options中的自定义方法。

options中的自定义方法用于处理AST语法树,最终返回出整个AST语法树对象。

贴一下源码,有兴趣可自行感受一二。附一篇详解Vue.jsHTML解析细节学习

functionparseHTML(html,options){\nvarstack=[];\nvarexpectHTML=options.expectHTML;\nvarisUnaryTag?1=options.isUnaryTag||no;\nvarcanBeLeftOpenTag?1=options.canBeLeftOpenTag||no;\nvarindex=0;\nvarlast,lastTag;\nwhile(html){\nlast=html;\n//确保我们不在像脚本/样式这样的纯文本内容元素中\nif(!lastTag||!isPlainTextElement(lastTag)){\nvartextEnd=html.indexOf(&39;);\nif(textEnd===0){\n//Comment:\nif(comment.test(html)){\nvarcommentEnd=html.indexOf(&39;);\n\nif(commentEnd>=0){\nif(options.shouldKeepComment){\noptions.comment(html.substring(4,commentEnd),index,index+commentEnd+3);\n}\nadvance(commentEnd+3);\ncontinue\n}\n}\n\n//http://en.wikipedia.org/wiki/Conditional_comment39;]>&39;<&39;([\\\\s\\\\S]*?)(</&39;[^>]*>)&39;i&39;noscript&39;$1&7298\n.replace(/<!\\[CDATA\\[([\\s\\S]*?)]]>/g,&39;);\n}\nif(shouldIgnoreFirstNewline(stackedTag,text)){\ntext=text.slice(1);\n}\nif(options.chars){\noptions.chars(text);\n}\nreturn&39;\n});\nindex+=html.length-rest$1.length;\nhtml=rest$1;\nparseEndTag(stackedTag,index-endTagLength,index);\n}\n\nif(html===last){\noptions.chars&&options.chars(html);\nif(!stack.length&&options.warn){\noptions.warn((&34;&34;\\&34;),{\nstart:index+html.length\n});\n}\nbreak\n}\n}\n\n//Cleanupanyremainingtags\nparseEndTag();\n\nfunctionadvance(n){\nindex+=n;\nhtml=html.substring(n);\n}\n\nfunctionparseStartTag(){\nvarstart=html.match(startTagOpen);\nif(start){\nvarmatch={\ntagName:start[1],\nattrs:[],\nstart:index\n};\nadvance(start[0].length);\nvarend,attr;\nwhile(!(end=html.match(startTagClose))&&(attr=html.match(dynamicArgAttribute)||html.match(attribute))){\nattr.start=index;\nadvance(attr[0].length);\nattr.end=index;\nmatch.attrs.push(attr);\n}\nif(end){\nmatch.unarySlash=end[1];\nadvance(end[0].length);\nmatch.end=index;\nreturnmatch\n}\n}\n}\n\nfunctionhandleStartTag(match){\nvartagName=match.tagName;\nvarunarySlash=match.unarySlash;\n\nif(expectHTML){\nif(lastTag===&39;&&isNonPhrasingTag(tagName)){\nparseEndTag(lastTag);\n}\nif(canBeLeftOpenTag?1(tagName)&&lastTag===tagName){\nparseEndTag(tagName);\n}\n}\n\nvarunary=isUnaryTag?1(tagName)||!!unarySlash;\n\nvarl=match.attrs.length;\nvarattrs=newArray(l);\nfor(vari=0;i<l;i++){\nvarargs=match.attrs[i];\nvarvalue=args[3]||args[4]||args[5]||&39;;\nvarshouldDecodeNewlines=tagName===&39;&&args[1]===&39;?\noptions.shouldDecodeNewlinesForHref:\noptions.shouldDecodeNewlines;\nattrs[i]={\nname:args[1],\nvalue:decodeAttr(value,shouldDecodeNewlines)\n};\nif(options.outputSourceRange){\nattrs[i].start=args.start+args[0].match(/^\\s*/).length;\nattrs[i].end=args.end;\n}\n}\n\nif(!unary){\nstack.push({\ntag:tagName,\nlowerCasedTag:tagName.toLowerCase(),\nattrs:attrs,\nstart:match.start,\nend:match.end\n});\nlastTag=tagName;\n}\n\nif(options.start){\noptions.start(tagName,attrs,unary,match.start,match.end);\n}\n}\n\nfunctionparseEndTag(tagName,start,end){\nvarpos,lowerCasedTagName;\nif(start==null){\nstart=index;\n}\nif(end==null){\nend=index;\n}\n\n//Findtheclosestopenedtagofthesametype\nif(tagName){\nlowerCasedTagName=tagName.toLowerCase();\nfor(pos=stack.length-1;pos>=0;pos–){\nif(stack[pos].lowerCasedTag===lowerCasedTagName){\nbreak\n}\n}\n}else{\n//Ifnotagnameisprovided,cleanshop\npos=0;\n}\n\nif(pos>=0){\n//Closealltheopenelements,upthestack\nfor(vari=stack.length-1;i>=pos;i–){\nif(i>pos||!tagName&&\noptions.warn\n){\noptions.warn(\n(&34;+(stack[i].tag)+&34;),{\nstart:stack[i].start,\nend:stack[i].end\n}\n);\n}\nif(options.end){\noptions.end(stack[i].tag,start,end);\n}\n}\n\n//Removetheopenelementsfromthestack\nstack.length=pos;\nlastTag=pos&&stack[pos-1].tag;\n}elseif(lowerCasedTagName===&39;){\nif(options.start){\noptions.start(tagName,[],true,start,end);\n}\n}elseif(lowerCasedTagName===&39;){\nif(options.start){\noptions.start(tagName,[],false,start,end);\n}\nif(options.end){\noptions.end(tagName,start,end);\n}\n}\n}\n}\n

第9541行至第9914行

RegularExpressions//相关正则

createASTElement

//ConvertHTMLstringtoAST.

parse//!important

parse函数从9593行至9914行,共三百多行。核心吗?当然核心!

引自wikipedia:

在计算机科学和语言学中,语法分析(英语:syntacticanalysis,也叫parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。

语法分析器(parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成。

parse的整体流程实际上就是先处理了一些传入的options,然后执行了parseHTML函数,传入了template,options和相关钩子。

具体实现这里盗一个图:

parse中的语法分析可以看这一篇这一节

startcharcommentend

parse、optimize、codegen的核心思想解读可以看这一篇这一节

这里实现的细节还真不少!

阶段小结(重点)

噫嘘唏!来到第20篇的小结!来个图镇一下先!

还记得官方这样的一句话吗?

下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

看了这么多,我们再回头看看注释版。

上图值得一提的是:Has&34;option?这个逻辑的细化

碰到是否有template选项时,会询问是否要对template进行编译:即模板通过编译生成AST,再由AST生成Vue的渲染函数,渲染函数结合数据生成VirtualDOM树,对VirtualDOM进行diff和patch后生成新的UI。

如图(此图前文也有提到,见0至5000行总结):

将Vue的源码的“数据监听”、“虚拟DOM”、“Render函数”、“组件编译”、结合好,则算是融会贯通了!

一图胜万言

好好把上面的三张图看懂,便能做到“成竹在胸”,走遍天下的VUE原理面试都不用慌了。框架就在这里,细化的东西就需要多多记忆了!

第9916行至第10435行

到1w行了,自我庆祝一下!

processRawAttrs//parse方法里用到的工具函数用于将特性保存到AST对象的attrs属性上processElement//parse方法工具函数元素填充

exportfunctionprocessElement(\nelement:ASTElement,\noptions:CompilerOptions\n){\nprocessKey(element)\n\n//determinewhetherthisisaplainelementafter\n//removingstructuralattributes\nelement.plain=(\n!element.key&&\n!element.scopedSlots&&\n!element.attrsList.length\n)\n\nprocessRef(element)\nprocessSlotContent(element)\nprocessSlotOutlet(element)\nprocessComponent(element)\nfor(leti=0;i<transforms.length;i++){\nelement=transforms[i](element,options)||element\n}\nprocessAttrs(element)\nreturnelement\n}\n

可以看到主要函数包括:processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最后遍历执行的transforms。

processElement完成的slotTarget的赋值,这里则是将所有的slot创建的astElement以对象的形式赋值给currentParent的scopedSlots。以便后期组件内部实例话的时候可以方便去使用vm.?slot。

processKeyprocessRef首先最为简单的是processKey和processRef,在这两个函数处理之前,我们的key属性和ref属性都是保存在astElement上面的attrs和attrsMap,经过这两个函数之后,attrs里面的key和ref会被干掉,变成astElement的直属属性。探讨一下slot的处理方式,我们知道的是,slot的具体位置是在组件中定义的,而需要替换的内容又是组件外面嵌套的代码,Vue对这两块的处理是分开的。

先说组件内的属性摘取,主要是slot标签的name属性,这是processSlotOutLet完成的。

processForparseForprocessIfprocessIfConditionsfindPrevElementaddIfConditionprocessOnceprocessSlotContentgetSlotNameprocessSlotOutlet

//handle<slot/>outlets\nfunctionprocessSlotOutlet(el){\nif(el.tag===&39;){\nel.slotName=getBindingAttr(el,&39;)//就是这一句了。\nif(process.env.NODE_ENV!==&39;&&el.key){\nwarn(\n`\\`key\\`doesnotworkon<slot>becauseslotsareabstractoutlets`+\n`andcanpossiblyexpandintomultipleelements.`+\n`Usethekeyonawrappingelementinstead.`,\ngetRawBindingAttr(el,&39;)\n)\n}\n}\n}\n//其次是摘取需要替换的内容,也就是processSlotContent,这是是处理展示在组件内部的slot,但是在这个地方只是简单的将给el添加两个属性作用域插槽的slotScope和slotTarget,也就是目标slot。\n

processComponent//processComponent并不是处理component,而是摘取动态组件的is属性。processAttrs是获取所有的属性和动态属性。processAttrscheckInForparseModifiersmakeAttrsMap

这一部分仍是衔接这parsefunction里的具体实现:start、end、comment、chars四大函数。

流程再回顾一下:

一、普通标签处理流程描述

识别开始标签,生成匹配结构match。

constmatch={//匹配startTag的数据结构tagName:&39;,attrs:[{&34;xxx&39;,&39;,&39;,&39;},…],start:index,end:xxx}复制代码2.处理attrs,将数组处理成{name:&39;,value:&39;}3.生成astElement,处理for,if和once的标签。4.识别结束标签,将没有闭合标签的元素一起处理。5.建立父子关系,最后再对astElement做所有跟Vue属性相关对处理。slot、component等等。

二、文本或表达式的处理流程描述。

截取符号<之前的字符串,这里一定是所有的匹配规则都没有匹配上,只可能是文本了。使用chars函数处理该字符串。判断字符串是否含有delimiters,默认也就是${},有的话创建type为2的节点,否则type为3.

三、注释流程描述

匹配注释符号。使用comment函数处理。直接创建type为3的节点。

阶段小结

parseHTML()和parse()这两个函数占了很大的篇幅,值得重点去看看。的确也很多细节,一些正则的匹配,字符串的操作等。从宏观上把握从template到vnode的parse流程也无大问题。

第10437行至第10605行

isTextTag//functionchars()里的工具函数isForbiddenTag//functionparseHTML()用到的工具函数用于检查元素标签是否合法(不是保留命名)guardIESVGBug//parsestart()中用到的工具函数checkForAliasModel//checkForAliasModel用于检查v-model的参数是否是v-for的迭代对象preTransformNode//preTransformNode方法对el进行预处理,便于后续对标签上的指令和属性进行处理,然后进行树结构的构建,确定el的root,parent,children等属性。总结下来就是生成树节点,构建树结构(关联树节点)。cloneASTElement//转换属性,把数组属性转换成对象属性,返回对象AST元素text//为虚拟dom添加textContent属性html//为虚拟dom添加innerHTML属性baseOptions

varbaseOptions={\nexpectHTML:true,//标志是html\nmodules:modules$1,//为虚拟dom添加staticClass,classBinding,staticStyle,styleBinding,for,\n//alias,iterator1,iterator2,addRawAttr,type,key,ref,slotName\n//或者slotScope或者slot,component或者inlineTemplate,plain,if,else,elseif属性\ndirectives:directives$1,//根据判断虚拟dom的标签类型是什么?给相应的标签绑定相应的v-model双数据绑定代码函数,\n//为虚拟dom添加textContent属性,为虚拟dom添加innerHTML属性\nisPreTag:isPreTag,//判断标签是否是pre\nisUnaryTag:isUnaryTag,//匹配标签是否是area,base,br,col,embed,frame,hr,img,input,\n//isindex,keygen,link,meta,param,source,track,wbr\nmustUseProp:mustUseProp,\ncanBeLeftOpenTag:canBeLeftOpenTag,//判断标签是否是colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source\nisReservedTag:isReservedTag,//保留标签判断是不是真的是html原有的标签或者svg标签\ngetTagNamespace:getTagNamespace,//判断tag是否是svg或者math标签\nstaticKeys:genStaticKeys(modules$1)//把数组对象[{staticKeys:1},{staticKeys:2},{staticKeys:3}]连接数组对象中的staticKeyskey值,连接成一个字符串str=‘1,2,3’\n};\n

genStaticKeysCached

第10607行至第10731行

/**\n*Goaloftheoptimizer:walkthegeneratedtemplateASTtree\n*anddetectsub-treesthatarepurelystatic,i.e.partsof\n*theDOMthatneverneedstochange.\n*\n*Oncewedetectthesesub-trees,wecan:\n*\n*1.Hoistthemintoconstants,sothatwenolongerneedto\n*createfreshnodesforthemoneachre-render;\n*2.Completelyskiptheminthepatchingprocess.\n*/\n//优化器的目标:遍历生成的模板AST树检测纯静态的子树,即永远不需要更改的DOM。\n//一旦我们检测到这些子树,我们可以:\n//1。把它们变成常数,这样我们就不需要了\n//在每次重新渲染时为它们创建新的节点;\n//2。在修补过程中完全跳过它们。\n

optimize//!important:过parse过程后,会输出生成AST树,接下来需要对这颗树做优化。即这里的optimize//循环递归虚拟node,标记是不是静态节点//根据node.static或者node.once标记staticRoot的状态genStaticKeys$1markStatic$1//标准静态节点markStaticRoots//标注静态根(重要)isStatic//isBuiltInTag(即tag为component和slot)的节点不会被标注为静态节点,isPlatformReservedTag(即平台原生标签,web端如h1、div标签等)也不会被标注为静态节点。isDirectChildOfTemplateFor

阶段小结

简单来说:整个optimize的过程实际上就干2件事情,markStatic(root)标记静态节点,markStaticRoots(root,false)标记静态根节点。

那么被判断为静态根节点的条件是什么?

该节点的所有子孙节点都是静态节点(判断为静态节点要满足7个判断,详见)必须存在子节点子节点不能只有一个纯文本节点

其实,markStaticRoots()方法针对的都是普通标签节点。表达式节点与纯文本节点都不在考虑范围内。

markStatic()得出的static属性,在该方法中用上了。将每个节点都判断了一遍static属性之后,就可以更快地确定静态根节点:通过判断对应节点是否是静态节点且内部有子元素且单一子节点的元素类型不是文本类型。

只有纯文本子节点时,他是静态节点,但不是静态根节点。静态根节点是optimize优化的条件,没有静态根节点,说明这部分不会被优化。

Q:为什么子节点的元素类型是静态文本类型,就会给optimize过程加大成本呢?

A:optimize过程中做这个静态根节点的优化目是:在patch过程中,减少不必要的比对过程,加速更新。但是需要以下成本

维护静态模板的存储对象

一开始的时候,所有的静态根节点都会被解析生成VNode,并且被存在一个缓存对象中,就在Vue.proto._staticTree中。随着静态根节点的增加,这个存储对象也会越来越大,那么占用的内存就会越来越多势必要减少一些不必要的存储,所有只有纯文本的静态根节点就被排除了

多层render函数调用

这个过程涉及到实际操作更新的过程。在实际render的过程中,针对静态节点的操作也需要调用对应的静态节点渲染函数,做一定的判断逻辑。这里需要一定的消耗。

纯文本直接对比即可,不进行optimize将会更高效。

第10733行至第10915行

//KeyboardEvent.keyCodealiases

keyCodes//内置按键keyNamesgenGuard//genGuard=condition=>if(${condition})returnnull;modifierCode//modifierCode生成内置修饰符的处理genHandlersgenHandler//调用genHandler处理events[name],events[name]可能是数组也可能是独立对象,取决于name是否有多个处理函数。genKeyFilter//genKeyFilter用于生成一段过滤的字符串:genFilterCode//在genKeyFilter里被调用onbind$1baseDirectives//CodegenState里的工具函数

不管是组件还是普通标签,事件处理代码都在genData的过程中,和之前分析原生事件一致,genHandlers用来处理事件对象并拼接成字符串。

第10921行至第11460行

//generate(ast,options)

exportfunctiongenerate(\nast:ASTElement|void,\noptions:CompilerOptions\n):CodegenResult{\nconststate=newCodegenState(options)\nconstcode=ast?genElement(ast,state):&34;div&39;\nreturn{\nrender:`with(this){return${code}}`,\nstaticRenderFns:state.staticRenderFns\n}\n}\n

CodegenStategenerate//!importantgenElement

exportfunctiongenElement(el:ASTElement,\nstate:CodegenState):string{\nif(el.parent){\nel.pre=el.pre||el.parent.pre\n}\n\nif(el.staticRoot&&!el.staticProcessed){\n//如果是一个静态的树,如<divid=&34;>123</div>\n//生成_m()方法\n//静态的渲染函数被保存至staticRenderFns属性中\nreturngenStatic(el,state)\n}elseif(el.once&&!el.onceProcessed){\n//v-once转化为_o()方法\nreturngenOnce(el,state)\n}elseif(el.for&&!el.forProcessed){\n//_l()\nreturngenFor(el,state)\n}elseif(el.if&&!el.ifProcessed){\n//v-if会转换为表达式\nreturngenIf(el,state)\n}elseif(el.tag===&39;&&!el.slotTarget&&!state.pre){\n//如果是template,处理子节点\nreturngenChildren(el,state)||&39;\n}elseif(el.tag===&39;){\n//如果是插槽,处理slot\nreturngenSlot(el,state)\n}else{\n//componentorelement\nletcode\n//如果是组件,处理组件\nif(el.component){\ncode=genComponent(el.component,el,state)\n}else{\nletdata\nif(!el.plain||(el.pre&&state.maybeComponent(el))){\ndata=genData(el,state)\n}\n\nconstchildren=el.inlineTemplate?null:genChildren(el,state,true)\ncode=`_c(&39;${\ndata?`,${data}`:&39;//data\n}${\nchildren?`,${children}`:&39;//children\n})`\n}\n//moduletransforms\nfor(leti=0;i<state.transforms.length;i++){\ncode=state.transforms[i](el,code)\n}\nreturncode\n}\n}\n\n

genStatic//genStatic会将ast转化为_m()方法genOnce//如果v-once在v-for中,那么就会生成_o()方法,否则将其视为静态节点genIf//genIf会将v-if转换为表达式,示例如下genIfConditionsgenFor//v-for会转换为_l()genData$2genDirectives//genData()里调用genInlineTemplate//genData()里调用genScopedSlots//genData()里调用genScopedSlotgenChildren//处理子节点getNormalizationType//用于判断是否需要规范化genNode//处理NodegenText//处理TextgenCommentgenSlot//处理插槽genComponent//处理组件genProps//处理propstransformSpecialNewlines

这里面的逻辑、细节太多了,不做赘述,有兴趣了解的童鞋可以去看推荐阅读

阶段小结

generate方法内部逻辑还是很复杂的,但仅做了一件事情,就是将ast转化为render函数的字符串,形成一个嵌套结构的方法,模版编译生成的_c(),_m(),_l等等其实都是生成vnode的方法,在执行vue.$mount方法的时候,会调用vm._update(vm._render(),hydrating)方法,此时_render()中方法会执行生成的render()函数,执行后会生成vnode,也就是虚拟dom节点。

第11466行至第11965行

prohibitedKeywordRE//正则校验:禁止关键字unaryOperatorsRE//正则校验:一元表达式操作stripStringRE//正则校验:脚本字符串detectErrors//检测错误工具函数checkNode//检查NodecheckEvent//检查EventcheckFor//检查For循环checkIdentifier//检查IdentifiercheckExpression//检查表达式checkFunctionParameterExpression//检查函数表达式generateCodeFramerepeat$1createFunction//构建函数createCompileToFunctionFn//构建编译函数compile//!important

returnfunctioncreateCompiler(baseOptions){\nfunctioncompile(\ntemplate,\noptions\n){\nvarfinalOptions=Object.create(baseOptions);\nvarerrors=[];\nvartips=[];\n\nvarwarn=function(msg,range,tip){\n(tip?tips:errors).push(msg);\n};\n\nif(options){\nif(options.outputSourceRange){\n//$flow-disable-line\nvarleadingSpaceLength=template.match(/^\\s*/)[0].length;\n\nwarn=function(msg,range,tip){\nvardata={msg:msg};\nif(range){\nif(range.start!=null){\ndata.start=range.start+leadingSpaceLength;\n}\nif(range.end!=null){\ndata.end=range.end+leadingSpaceLength;\n}\n}\n(tip?tips:errors).push(data);\n};\n}\n//mergecustommodules\nif(options.modules){\nfinalOptions.modules=\n(baseOptions.modules||[]).concat(options.modules);\n}\n//mergecustomdirectives\nif(options.directives){\nfinalOptions.directives=extend(\nObject.create(baseOptions.directives||null),\noptions.directives\n);\n}\n//copyotheroptions\nfor(varkeyinoptions){\nif(key!==&39;&&key!==&39;){\nfinalOptions[key]=options[key];\n}\n}\n}\n\nfinalOptions.warn=warn;\n\nvarcompiled=baseCompile(template.trim(),finalOptions);\n{\ndetectErrors(compiled.ast,warn);\n}\ncompiled.errors=errors;\ncompiled.tips=tips;\nreturncompiled\n}\n

再看这张图,对于“模板编译”是不是有一种新的感觉了。

compileToFunctions

//最后的最后

returnVue;\n

哇!历时一个月左右,我终于完成啦!!!

完结撒花!激动+释然+感恩+小满足+……??ヽ(°▽°)ノ?

这生啃给我牙齿都啃酸了!!

总结

emmm,本来打算再多修补一下,但是看到vue3的源码解析已有版本出来啦(扶朕起来,朕还能学),时不我待,Vue3奥利给,干就完了!

后续仍会完善此文,您的点赞是我最大的动力!也望大家不吝赐教,不吝赞美~

最最最最后,还是那句老话,与君共勉:

纸上得来终觉浅绝知此事要躬行

我是掘金安东尼:一名人气前端技术博主(文章100w+阅读量)

终身写作者(INFP写作人格)???

坚持与热爱(简书打卡1000日)

陪你一起度过漫长技术岁月吧(以梦为马)

觉得不错,点个赞吧(您的三连,我最大的动力)

关于最小的网站源码分享的内容到此结束,希望对大家有所帮助。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平