You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
link
compile阶段将指令解析成为指令描述对象(descriptor),闭包在了link函数里,link函数会把descriptor传入Directive构造函数,创建出真正的指令实例。此外link函数是作为参数传入linkAndCaptrue中的,后者负责执行link,同时取出这些新生成的指令,先按照指令的预置的优先级从高到低排好顺序,然后遍历指令执行指令的_bind方法,这个方法会为指令创建watcher,并计算表达式的值,完成前面描述的依赖收集。并最后执行对应指令的bind和update方法,使指令生效、界面更新。
Vue.prototype._compile=function(el){varoptions=this.$options// transclude and init element// transclude can potentially replace original// so we need to keep reference; this step also injects// the template and caches the original attributes// on the container node and replacer node.varoriginal=elel=transclude(el,options)// 在el这个dom上挂一些参数,并触发'beforeCompile'钩子,为compile做准备this._initElement(el)// handle v-pre on root node (#2026)// v-pre指令的话就什么都不用做了。if(el.nodeType===1&&getAttr(el,'v-pre')!==null){return}// root is always compiled per-instance, because// container attrs and props can be different every time.varcontextOptions=this._context&&this._context.$optionsvarrootLinker=compileRoot(el,options,contextOptions)// resolve slot distribution// 具体是将各个slot存储到vm._slotContents的对应属性里面去,// 然后后面的compile阶段会把slot解析为指令然后进行处理resolveSlots(this,options._content)// compile and link the restvarcontentLinkFnvarctor=this.constructor// component compilation can be cached// as long as it's not using inline-template// 这里是组件的情况才进入的,大家先忽略此段代码if(options._linkerCachable){contentLinkFn=ctor.linkerif(!contentLinkFn){contentLinkFn=ctor.linker=compile(el,options)}}// link phase// make sure to link root with prop scope!varrootUnlinkFn=rootLinker(this,el,this._scope)// compile和link一并做了varcontentUnlinkFn=contentLinkFn
? contentLinkFn(this,el)
: compile(el,options)(this,el)// register composite unlink function// to be called during instance destructionthis._unlinkFn=function(){rootUnlinkFn()// passing destroying: true to avoid searching and// splicing the directivescontentUnlinkFn(true)}// finally replace originalif(options.replace){replace(original,el)}this._isCompiled=truethis._callHook('compiled')}
/** * Process an element or a DocumentFragment based on a * instance option object. This allows us to transclude * a template node/fragment before the instance is created, * so the processed fragment can then be cloned and reused * in v-for. * * @param {Element} el * @param {Object} options * @return {Element|DocumentFragment} */exportfunctiontransclude(el,options){// extract container attributes to pass them down// to compiler, because they need to be compiled in// parent scope. we are mutating the options object here// assuming the same object will be used for compile// right after this.if(options){options._containerAttrs=extractAttrs(el)}// for template tags, what we want is its content as// a documentFragment (for fragment instances)if(isTemplate(el)){el=parseTemplate(el)}if(options){// 如果当前是component,并且没有模板,只有一个壳// 那么只需要处理内容的嵌入if(options._asComponent&&!options.template){options.template='<slot></slot>'}if(options.template){//基本都会进入到这里options._content=extractContent(el)el=transcludeTemplate(el,options)}}if(isFragment(el)){// anchors for fragment instance// passing in `persist: true` to avoid them being// discarded by IE during template cloningprepend(createAnchor('v-start',true),el)el.appendChild(createAnchor('v-end',true))}returnel}
/** * Process the template option. * If the replace option is true this will swap the $el. * * @param {Element} el * @param {Object} options * @return {Element|DocumentFragment} */functiontranscludeTemplate(el,options){vartemplate=options.templatevarfrag=parseTemplate(template,true)if(frag){// 对于非片段实例情况且replace为true的情况下,frag的第一个子节点就是最终el元素的替代者varreplacer=frag.firstChildvartag=replacer.tagName&&replacer.tagName.toLowerCase()if(options.replace){/* istanbul ignore if */if(el===document.body){process.env.NODE_ENV!=='production'&&warn('You are mounting an instance with a template to '+'<body>. This will replace <body> entirely. You '+'should probably use `replace: false` here.')}// there are many cases where the instance must// become a fragment instance: basically anything that// can create more than 1 root nodes.if(// multi-children templatefrag.childNodes.length>1||// non-element templatereplacer.nodeType!==1||// single nested componenttag==='component'||resolveAsset(options,'components',tag)||hasBindAttr(replacer,'is')||// element directiveresolveAsset(options,'elementDirectives',tag)||// for blockreplacer.hasAttribute('v-for')||// if blockreplacer.hasAttribute('v-if')){returnfrag}else{// 抽取replacer自带的属性,他们将在自身作用域下编译options._replacerAttrs=extractAttrs(replacer)// 把el的所有属性都转移到replace上面去,因为我们后面将不会再处理el直至他最后被replacer替换mergeAttrs(el,replacer)returnreplacer}}else{el.appendChild(frag)returnel}}else{process.env.NODE_ENV!=='production'&&warn('Invalid template option: '+template)}}
_compile
介绍完响应式的部分,算是开了个头了,后面的内容很多,但是层层递进,最终完成响应式精确订阅和批处理更新的整个过程,过程比较流程,内容耦合度也高,所以我们先来给后文的概览,介绍一下大体过程。
我们最开始的代码里提到了Vue处理完数据和event之后就到了$mount,而$mount就是在this._compile后触发编译完成的钩子而已,所以核心就是Vue.prototype._compile。
_compile
包含了Vue构建的三个阶段,transclude,compile,link。而link阶段其实是放在linkAndCapture里执行的,这里又包含了watcher的生成,指令的bind、update等操作。我先简单讲讲什么是指令,虽然Vue文档里说的指令是v-if,v-for等这种HTML的attribute,其实在Vue内部,只要是被Vue处理的dom上的东西都是指令,比如dom内容里的
{{a}}
,最终会转换成一个v-text的指令和一个textNode,而一个子组件<component><component>
也会生成指令,还有slot,或者是你自己在元素上写的attribute比如hello={{you}}
也会被编译为一个v-bind指令。我们看到,基本只要是涉及dom的(不是响应式的也包含在内,只要是vue提供的功能),不管是dom标签,还是dom属性、内容,都会被处理为指令。所以不要有指令就是attribute的惯性思维。回过头来,_compile部分大致分为如下几个部分
transclude
transclude的意思是内嵌,这个步骤会把你template里给出的模板转换成一段dom,然后抽取出你el选项指定的dom里的内容(即子元素,因为模板里可能有slot),把这段模板dom嵌入到el里面去,当然,如果replace为true,那他就是直接替换el,而不是内嵌。我们大概明白transclude这个名字的意义了,但其实更关键的是把template转换为dom的过程(如
<p>{{a}}<p>
字符串转为真正的段落元素),这里为后面的编译准备好了dom。compile
compile的的过程具体就是遍历模板解析出模板里的指令。更精确的说是解析后生成了指令描述对象。
同时,compile函数是一个高阶函数,他执行完成之后的返回值是另一个函数:link,所以compile函数的第一个阶段是编译,返回出去的这个函数完成另一个阶段:link。
link
compile阶段将指令解析成为指令描述对象(descriptor),闭包在了link函数里,link函数会把descriptor传入Directive构造函数,创建出真正的指令实例。此外link函数是作为参数传入linkAndCaptrue中的,后者负责执行link,同时取出这些新生成的指令,先按照指令的预置的优先级从高到低排好顺序,然后遍历指令执行指令的_bind方法,这个方法会为指令创建watcher,并计算表达式的值,完成前面描述的依赖收集。并最后执行对应指令的bind和update方法,使指令生效、界面更新。
此外link函数最终的返回值是unlink函数,负责在vm卸载时取消对应的dom到数据的绑定。
是时候回过头来看看Vue官网这张经典的图了,以前我刚学Vue时也是对于Watcher,Directive之类的概念云里雾里。但是现在大家看这图是不是很清晰明了?
上代码:
尤雨溪的注释已经极尽详细,上面的代码很清晰(如果你用过angular,那你会感觉很熟悉,angular里也是有transclude,compile和link的,虽然实际差别很大)。我们在具体进入各部分代码前先说说为什么dom的编译要分成compile和link两个phase。
在组件的多个实例、v-for数组等场合,我们会出现同一个段模板要绑定不同的数据然后分发到dom里面去的需求。这也是mvvm性能考量的主要场景:大数据量的重复渲染生成。而重复渲染的模板是一致的,不一致的是他们需要绑定的数据,因此compile阶段找出指令的过程是不用重复计算的,只需要link函数(和里面闭包的指令),而模板生成的dom使用原生的cloneNode方法即可复制出一份新的dom。现在,复制出的新dom+ link+具体的数据即可完成渲染,所以分离compile、并缓存link使得Vue在渲染时避免大量重复的性能消耗。
transclude函数
这里大家可以考虑一下,我给你一个空的documentFragment和一段html字符串,让你把html生成dom放进fragment里,你应该怎么做?innerHTML?documentFragment可是没有innerHtml的哦。那先建个div再innerHTML?那万一我的html字符串的是tr元素呢?tr并不能直接放进div里哦,那直接用outerHTML?没有parent Node的元素是不能设置outerHTML的哈(parent是fragment也不行),那我先用正则提取第一个标签,先createElement这个标签然后在写他的innerHTML总可以了吧?并不行,我没告诉你我给你的这段HTML最外层就一个元素啊,万一是个片段实例呢(也就是包含多个顶级元素,如
<p>1<p><p>2<p>
),所以我才说给你一个fragment当容器,让你把dom装进去。上面这个例子说明了实际转换dom过程中,可能遇到的一个小坑,只是想说明字符串转dom并不是看起来那么一行innerHTML的事。
我们看上面的代码,先
options._containerAttrs = extractAttrs(el)
,这样就把el元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上。因为我们前面说过,这些属性是vm实际挂载的根元素上的,如果vm是一个组件之类的,那么他们应该是在父组件的作用于编译/link的,所以需要预先提取出来,因为如果replace为true,el元素会被模板元素替换,但是他上面的属性是会编译link后merge到模板元素上面去。然后进入到那个两层的if里, extractContent(el),将el的内容(子元素和文本节点)抽取出来,因为如果模板里有slot,那么他们要分发到对应的slot里。
然后就到
el = transcludeTemplate(el, options)
:首先执行解析
parseTemplate(template, true)
,得到一段存放在documentFragment里的真实dom,然后就判断是否需要replace。(若replace为true)之后判断是否是片段实例,官网已经讲述哪几种情况对应片段实例,而代码里那几个判断就是对应的处理。若不是,那就进入后续的情况,我已经注释代码作用,就不再赘述。我们来说说parseTemplate,因为vue支持template选项写#app
这样的HTML选择符,也支持直接存放模板字符串、document fragment、dom元素等等,所以针对各种情况作了区分,如果是一个已经好的dom那几乎不用处理,否则大部分情况下都是执行stringToFragment:这个部分的代码就是用来处理我一开始介绍transclude提到的那个把html字符串转换为真正dom的问题。原理在代码的注释里已经说得很清楚了,比如
<tr>a</tr>
这段dom,那么代码里的tag就匹配上了'tr',map对象是预先写好的一个对象,map['tr']存放的内容就是这么个数组[2, '<table><tbody>', '</tbody></table>']
,2
表示真正的元素在2层dom里。剩下的两段字符串是用于添加在你的HTML字符串两端(prefix + templateString + suffix),现在innerHTML就设置为了'<table><tbody><tr>a</tr></tbody></table>'
,不会出现问题了。现在transclude之后,字符串已经变成了dom。后续的就依据此dom,遍历dom树,提取其中的指令,那如果Vue一开始就没有把字符串转成dom,而是直接解析字符串,提取其中的指令的话,其实工程量是非常大的。一方面要自己构建dom结构,一方面还要解析dom的attribute和内容,而这三者在Vue允许实现自定义组件、自定义指令、自定义prop的情况下给直接分析纯字符串带来了很大难度。所以,实先构造为dom是很有必要的。
The text was updated successfully, but these errors were encountered: