Open Ma63d opened 7 years ago
啥时候整个vue2的吧
@william-xue 最近出来实习,真的是腾不出时间了,vue2 的计划应该得今年晚些时候的了。 不过 github 上已经有一些 vue2 的源码解析了,你可以找找他们的看看。
(若replace为false)之后判断是否是片段实例,官网已经讲述哪几种情况对应片段实例,而代码里那几个判断就是对应的处理。
是不是应该为 若 replace 为 true
才判断是否是片段实例?
是不是应该为 若 replace 为 true 才判断是否是片段实例?
@chenjsh36 是的,笔误,多谢指正。
_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是很有必要的。