Angular原生实现了两个工具类:DefaultKeyValueDiffer和DefaultIterableDiffer,它们分别用来检查两个对象或两个数组之间的差异(也就是diff)。典型的使用场景是:检查某个变量在两个时刻之间是否发生了改变、发生了什么样的改变,在这篇文章中,咱们称它为变动检测。html
请将diff与change detection区分开来。
Angular的变化检测默认只比较对象的引用是否改变,可是咱们能够经过DoCheck生命周期钩子来作一些额外的检测,好比检查对象是否增长删除改动了某些属性、检查数组是否增长删除移动了某些条目。这个时候变动检测就能够派上用场了。
举个例子,NgForOf指令内部就是经过DefaultIterableDiffer
来检测输入数组发生了怎样的变化,从而可以用最小的代价去更新DOM。git
这两个工具类中包含的算法能够说是十分通用的,甚至能够移植到其余框架、语言去。除此以外,掌握这种变动检测算法也可以帮助咱们更好地理解、使用NgForOf
,甚至编写本身的结构型指令。github
在咱们经过源码了解它们的算法以前,我先简单地介绍一下Differ是如何使用的。算法
要使用这两个工具类,并不须要(也不该该)本身建立这两个类的实例,BrowserModule已经将将它们注册在注入器中。segmentfault
如下代码展现了如何获取和使用DefaultKeyValueDiffer:api
import { Component, KeyValueDiffers, KeyValueDiffer } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { constructor(keyValueDiffers: KeyValueDiffers) { const someObj: any = { a: 1, b: 2 }; console.log('KeyValueDiffers"', keyValueDiffers); const defaultKeyValueDifferFactory = keyValueDiffers.find(someObj); console.log('defaultKeyValueDifferFactory:', defaultKeyValueDifferFactory); console.log('test defaultKeyValueDifferFactory.supports:', defaultKeyValueDifferFactory.supports({}), defaultKeyValueDifferFactory.supports([]), defaultKeyValueDifferFactory.supports('string') ) const defaultKeyValueDiffer = defaultKeyValueDifferFactory.create(); console.log('defaultKeyValueDiffer:', defaultKeyValueDiffer); const changes1 = defaultKeyValueDiffer.diff(someObj); console.log('changes1:') changes1.forEachAddedItem((r) => { console.log(r.key, r.previousValue, r.currentValue); }); console.log('--------------------') delete someObj.a; someObj.c = 'new value'; const changes2 = defaultKeyValueDiffer.diff(someObj); console.log('changes2:') changes2.forEachAddedItem((r) => { console.log(r.key, r.previousValue, r.currentValue); }); changes2.forEachRemovedItem((r) => { console.log(r.key, r.previousValue, r.currentValue); }); console.log('--------------------') } }
DefaultIterableDiffer的使用是彻底相似的。你也能够参考api文档。数组
你能够从这个例子初步体会到“抽象”的威力。使用者调用interface KeyValueDiffer定义的API,而彻底不知道(也不须要知道)背后DefaultKeyValueDiffer这个类的存在。app
更棒的是,咱们等一下能够看到,咱们能够本身实现特殊用途的KeyValueDiffer工具类(工具类实现这个接口),而后这个工具类就能被加入到KeyValueDiffers
中,从而能在应用的指定范围内分发,所以这套系统(能够命名为“Differ供应系统”)具备很强的可扩展性。框架
先抛开DefaultKeyValueDiffer自己不谈,咱们先从源码来看看KeyValueDiffer供应系统是如何实现的。ide
这个系统主要由3个类或接口组成:KeyValueDiffers类, KeyValueDifferFactory接口, KeyValueDiffer接口。
从前面的使用示例能够看出,使用者最开始须要经过依赖注入拿到KeyValueDiffers类的实例:
constructor(keyValueDiffers: KeyValueDiffers)
ApplicationModule已经注册了这个服务的provider,咱们的AppModule在引入BrowserModule的时候会获得这个provider。
有意思的是,Angular注册的_keyValueDiffersFactory直接返回同一个KeyValueDiffers实例,所以,这个根KeyValueDiffers是全局惟一的,即便你在同一个页面运行多个Angular程序。效果等同于在Platform Injector注册了这个服务。
好,使用者已经能够获取到KeyValueDiffers实例了,它是干什么的呢?
KeyValueDiffers持有一些KeyValueDifferFactory,而且能够经过find方法返回支持指定对象kv的Differ的工厂(某种Differ只支持某种特定的对象,好比说,咱们能够实现一个专门支持Date的Differ)。
KeyValueDiffers的静态方法create能够在建立新实例的时候指定一个"parent",新的实例会得到parent拥有的factories,相似于继承。注意到concat时,本身的factories在前面,parent的factories在后面,而find方法是从前日后查找的,所以find先查找本身拥有的factories,再检查parent的factories。
KeyValueDiffers的静态方法extend,注释已经写得很清楚了,而且源码也很简单,它是生成一个StaticProvider的工具函数。你能够将KeyValueDiffers注册在某个的依赖注入层级上,从而在此层级如下的组件、指令可以经过依赖注入获取它。
在咱们经过find方法获得KeyValueDifferFactory之后,能够经过KeyValueDifferFactory.supports
检查KeyValueDiffer是否支持某个对象的变动检测,而后能够经过KeyValueDifferFactory.create
得到新的KeyValueDiffer对象。显然每种KeyValueDiffer必须有一个对应的KeyValueDifferFactory,好比DefaultKeyValueDiffer有本身的DefaultKeyValueDifferFactory。所以咱们在实现本身的Differ的时候要实现Factory和Differ来分别implements这两个接口。
假设咱们不实现本身的KeyValueDiffer,从KeyValueDiffers获取到DefaultKeyValueDifferFactory之后,直接调用DefaultKeyValueDifferFactory.create()
就能够得到DefaultKeyValueDiffer对象,就像最前面的例子同样。经过KeyValueDiffer.diff(obj)
能够追踪obj与上次调用diff传入的obj相比,发生了哪些改变。至此,"KeyValueDiffer供应系统"的使命就完成了。
这套KeyValueDiffer供应系统有如下优势:
find
的查找顺序。让咱们从源码研究它。
注意到它同时实现了KeyValueDiffer和KeyValueChanges接口,所以这个类不只要发现新旧对象之间变动,并且要给用户提供遍历这些变化的API。
既然Differ要检测“变化”,那么它就要存储状态,也就是上次调用diff传入的obj是怎么样的。从类成员能够看出,每一个Differ对象要存储obj的全部条目,分别经过Map和链表。用户可以经过forEachItem
遍历当前obj的全部属性。
此外,为了存储有用的信息,还定义了4个链表,分别是_previousMapHead
(旧obj的全部属性) _changesHead
_additionsHead
_removalsHead
。若是用户想要获取这4个信息,能够分别调用forEachPreviousItem
(遍历旧obj的全部属性) forEachChangedItem
forEachAddedItem
forEachRemovedItem
来遍历这些列表。
有这么多的链表,为了节约内存,一个链表条目,有各类不一样的链表next指针,能够同时做为多个链表的成员。
剩下的全部函数都是围绕diff
来服务的。能够看到diff基本上至关于直接调用check。check就包含了变动检测算法:
reset()
为迁移到下一个状态做准备(包括:更新_previousMapHead
链表,更新每一个record的 _nextPrevious
指针和previousValue
,清空_changesHead
_additionsHead
_removalsHead
链表)遍历新传入的obj
的每一个属性,依次与_mapHead
比较(_mapHead
存储的仍是旧obj的record)。
_changesHead
链表。_getOrCreateRecordForKey
方法中,先尝试从_records Map找到这个key,若是找到了就比较其value是否与新obj中的value相同(若是不一样的话就_addToChanges
),而后将它暂时从_mapHead链表中删除,_getOrCreateRecordForKey
返回这个record(等一下会插入到链表的正确位置); 若是在Map找不到这个key,说明这是一个新加入的属性,则建立一个新的record并加入_additionsHead
链表,_getOrCreateRecordForKey
返回这个新建的record(等一下再插入到_mapHead
链表中)。_getOrCreateRecordForKey
执行完毕之后,将返回的record插入_mapHead链表的正确位置。_mapHead
链表中还有还没有访问的record,这些record都是被删除的。将它们从_mapHead
移除、加入_removalsHead
、从_records
中删除这些条目、更新这些record的状态。实现这个算法时要理清楚何时更新_changesHead _additionsHead _removalsHead链表,也就是什么状况意味着发现了change、addition、removal。这在上面的表述中已经说明了。
理清楚了这一点之后,剩下的就是维护链表的操做了。同时维护这么多的链表确实是一件很容易出错的事情。
IterableDiffer用来对数组或类数组对象进行变动检测。
IterableDiffer供应系统与KeyValueDiffer供应系统很是相似,这里只讨论几个比较重要的地方:
IterableChanges.forEachIdentityChange
可让用户看到全部trackById相同但Identity变化(至关于a!==b)的那些条目。那些简单的,或者DefaultKeyValueDiffer
也有的类成员我就不一一介绍了。
与前面相似地,变动检测的逻辑封装在_check函数中。让咱们从这里开始。
this._reset()
进行初步的状态更新。包括:更新_previousItHead
链表,更新每一个record的 _nextPrevious
指针,重置previousIndex
,清空_additionsHead
_movesHead
_removalsHead
_identityChangesHead
链表。Array.isArray(collection)
,因为DefaultIterableDiffer支持一些类数组对象,所以在判断不成功的时候会执行另外一种算法来检测变动。咱们不妨假设检测正常数组的变动。对于collection
(新数组)的每一个项,执行如下操做(用下标index
来遍历collection
):
this._trackByFn(index, item)
计算当前项的标识值。**若是新旧数组之间的两个项的标识值相等,咱们就认为它们是同一个项,无论identity是否同样(即无论a===b是否成立),咱们都认为顺序没有变化。比较_itHead
链表(旧数组)的第index
个项(命名为item1)与collection
的第index
个项(命名为item2),它们标识值
是否相同:
若是不相同,调用_mismatch
来处理,使得item2成为_itHead
链表的第index
个项:
_itHead
链表中删除,毕竟它们没有在正确的位置上(若是后面发现有这个项,再将它加到合适的位置)。而后将它加入_unlinkedRecords
中(它是_DuplicateMap类型,也就是MuitiMap的一种实现。之因此要用到MuitiMap,是由于数组中可能有多个项的标识值相同),而后将它加入_removalsHead
链表中。index
之后的项中找到有相同标识值的项。若是找到的话,就检测到移动变动,因而要将这个项从_itHead
链表中原来的位置移动到index
位置,并加入_movesHead
链表。若是没有在旧数组找到相同标识值的项,尝试从_unlinkedRecords
找到相同标识值的项。若是找到的话,一样检测到移动变动。将这个项从_unlinkedRecords
Map和_removalsHead
链表移除(撤销_addToRemovals
操做),而后插入到_itHead
链表的index
位置。若是从_unlinkedRecords
仍是没找到相同标识值的项,说明这是一个新增长的项,因而将它插入_itHead
链表的index
位置并加入_additionsHead
链表。_mismatch
执行完毕之后。设置mayBeDirty = true
。这个标识表示未来每次检测到item1
与item2
标识值相等得时候,要调用_verifyReinsertion
来修正某种错误,下面再谈。_verifyReinsertion
来检查前面步骤可能产生的插入顺序错误:假设发生变动[a, a]
=> [b, a, a]
,那么在对比链表中的a和新数组的b之后,会删除链表中的a(链表存储的是旧数组),而后插入新数组的b,接下来,链表中的下一个项依然是a,就会匹配新数组中的第一个a(旧数组的第二个a匹配新数组的第一个a),接下来会在链表的末尾reinsert
刚才删除的a(原数组的第一个a)。通过这样的变动检测之后,两个a的顺序变了。正确的作法应该是“将b插入数组0位置”,而不是“将数组0位置的a换成b,而后在数组末尾加入a”。_unlinkedRecords
中是否有相同标识值的项),若是有删除过,则这个项才与item2对应,因而撤销被删除项的_addToRemovals
操做,并将这个项reinsert
到链表的index
位置。_verifyReinsertion
还有另外一个做用,你那就是检查record.currentIndex
是否正确。假如在record
前面已经插入一个项并删除一个项,那么currentIndex不须要改变;可是若是只是前面插入了1个,那么插入项之后的全部项的currentIndex都要+1,而后记录这个移动操做(_addToMoves)。在这种状况下,虽说“这些项都移动了”不太准确,可是毕竟它们所在的下标都变化了,咱们仍是先记录这些移动,之后调用forEachOperation
的时候会过滤掉这种不严格的移动。_itHead
链表后面还有没访问到的项,则这些项是被删除的。使用_truncate从链表中删掉它们,并记录它们的删除(_addToRemovals
)。_truncate
除此以外还作一些收尾工做:将检测变动时用来查询的_unlinkedRecords
Map清空(这些是被删除的项,它们已经被执行_addToRemovals
了),而后将各类链表尾的next赋值为null(咱们以前加入链表的时候都没有考虑它是否是链表尾)。能够看出,算法的重点在于第3步的for循环。for循环刚开始的时候,_itHead
链表仍是旧数组的状态。而后通过一轮循环,就修改_itHead
链表,将正确的项移动到_itHead
链表的index位置。所以,这个for循环从左向右逐项更新_itHead
链表,使得它有愈来愈长的前缀与新数组匹配。
diff
执行完毕之后,变动的信息就存储在DefaultIterableDiffer
的那些链表中了,用户能够经过IterableChanges.forEachOperation
获得一系列数组操做(增长删除移动),这些操做能将旧数组更新为新数组。注意,这些数组操做是经过计算获得的,不必定是实际发生在旧数组上的操做。forEachOperation
是如何经过变动信息计算出可能发生的操做序列呢?看源码以前,首先应该思考它的思路是怎么样的,不然这段代码会看得很是费劲。
发生在数组上的变动操做无非三种:增长项、删除项、移动项(两个项的交换能够看做两次移动)。其中,移动项又能够分为向前移动(下标变小)和向后移动(下标变大)。咱们以前已经提到过,将某个项向前移动时,它所“通过”的那些项的下标会+1。这种下标+1只是其余移动的副产品,不该该算做真正的向后移动。好比对于变动[a,b,c]=>[c,a,b]
,咱们天然的想法是“c从2移动到0”,而bc下标的增长不该另算做变动。
进一步思考,若是项item的下标增长,其实全都是由于item后面的一些项移动到了item前面(如今仅考虑移动项,不考虑增长项)。也就是说,向后移动均可以替换为其余项的向前移动,咱们再也不须要考虑向后移动了。
举个例子,[a,b,c,d]=>[d,c,b,a]
的变动操做序列是:d向前移动到0位置,c向前移动到1位置,b向前移动到2位置,a不须要本身移动!
forEachOperation
的计算操做序列算法能够简述以下(先只考虑移动项,不考虑有增长项和删除项的状况):
遍历_itHead
链表(此时diff
已经执行完,_itHead
链表的顺序与新数组相同),对于每一项record
,依次检查其临时下标和目标下标(在源码中分别命名为adjPreviousIndex
和currentIndex
)。临时下标的意思是,旧数组刚执行完已计算出的操做所获得的临时状态中,这个项的下标。目标下标的意思是,这个项在最终目标数组中的下标。好比,计算[a,b,c,d]=>[d,b,c,a]
的变动操做序列时,已经计算出“d移动到0,b移动到1”,旧数组执行完这两个操做之后的临时状态为[d,b,a,c]
,a的临时下标为2,c的临时下标为3,目标下标始终分别是3和2。
adjPreviousIndex===currentIndex
,说明在当前状态中,这个项刚好处于目标位置,不须要移动。adjPreviousIndex>currentIndex
,说明在当前状态中,这个项须要被向前移动,才能到达目标位置。这个if就是判断这个状况的。adjPreviousIndex<currentIndex
的状况。其实adjPreviousIndex
和currentIndex
分别表示【不忽略增长、删除项状况下的】临时下标和目标下标。经过如下两个减法,能计算出【忽略增长、删除项的状况下的——也就是说假设被增长、删除的项历来都不存在】临时下标和目标下标:
const localMovePreviousIndex = adjPreviousIndex - addRemoveOffset; const localCurrentIndex = currentIndex ! - addRemoveOffset;
由于addRemoveOffset
变量记录了到目前为止的计算中,已经增长了多少个项(若是删除的项比增长的多,则这个值为负数),因此减掉这个数之后就是(忽略被增长的项的状况下的)临时下标和目标下标。
那么adjPreviousIndex
(临时下标)是如何获得的呢?adjPreviousIndex的计算函数须要知道【item在旧数组的下标:previousIndex】、【刚刚讲过的addRemoveOffset】、【item被多少个向前移动的项“通过”:moveOffset】,结果adjPreviousIndex
就是三者之和,它就是“item在【旧数组执行完已知操做之后的临时数组】中的下标”。
既然咱们须要知道各个项被多少个向前移动的项“通过”,那么咱们应该在向前移动某项的时候就记录它通过了哪些项。好比[a,b,c,d]=>[a,d,c,b]
计算出第一个操做“d移动到1”,d向前移动的时候依次通过c,b,所以它们的moveOffset要+1;接下来计算出第二个操做“c向前移动到2”,通过b,所以b的moveOffset要再次+1。这个for循环就是作这个事情的:
for (let i = 0; i < localMovePreviousIndex; i++) { const offset = i < moveOffsets.length ? moveOffsets[i] : (moveOffsets[i] = 0); // 对于每一个可能被通过的项(旧数组第i项),计算它在临时数组(仅仅考移动的项,不考虑增长、删除的项)中的下标 const index = offset + i; // 判断它是否是在临时数组的[localMovePreviousIndex,localCurrentIndex)范围 if (localCurrentIndex <= index && index < localMovePreviousIndex) { // 若是是,说明这一项是被“通过”的 moveOffsets[i] = offset + 1; } }
这个for循环比较难懂,这里解释一下:
moveOffsets
这个数组来存储各个项的moveOffset
。这个数组以previousIndex(旧数组中的下标)为索引。(let i = 0; i < localMovePreviousIndex; i++)
,如何理解?咱们正在检查临时数组(旧数组执行完已知操做之后的临时状态,忽略增长、删除的项)的第localCurrentIndex
个项,此时咱们发现localMovePreviousIndex != localCurrentIndex。所以这个向要从临时数组的localMovePreviousIndex
位置移动到localCurrentIndex
位置。所以临时数组下标范围[localMovePreviousIndex,localCurrentIndex)中的项都须要moveOffset+=1。为了更新moveOffset,咱们须要知道这些项在【旧数组】中的下标。但是咱们怎么知道这些项在【旧数组】中的下标呢?咱们没法从【临时下标】计算出【旧数组下标】。可是咱们可以肯定的是这些项在旧数组的下标确定小于localMovePreviousIndex
(由于这些项确定尚未被向前移动,它们只能被那些【向前移动的项】“通过”,下标只可能增长),因而咱们就对【旧数组中全部下标小于localMovePreviousIndex
的每一个项】计算它们在【临时数组】中的下标(这就是for循环的范围由来),而后判断它在【临时数组】中的下标是否处于范围[localMovePreviousIndex,localCurrentIndex),若是是的话咱们就更新moveOffsets[i]
。总结一下这个算法的思路:
算法接受一个临时数组和一个目标数组(最开始临时数组是旧数组)。这个算法不断从临时数组构造一个新的临时数组,使得新的临时数组有更长的前缀匹配目标数组,直到构造出的临时数组与目标数组彻底相同。如何构造新的临时数组呢?将临时数组中的某一项向前移动,移动到正确的位置。好比临时数组是[a,d,c,b,e]
,目标数组是[a,b,c,d,e]
,咱们构造出的下一个临时数组是[a,b,d,c,e]
(b向前移动到正确的位置),使得新的临时数组有更长的前缀与目标数组匹配(前缀a,b
)。
继续重复这个过程,使得新的临时数组有更长的前缀匹配目标数组,直到构造出的临时数组与目标数组彻底相同。
在实现的时候,Angular并无直接存储临时数组,而是经过一个
moveOffsets
数组,表示如何经过移动旧数组的项获得临时数组(这也是为何moveOffsets是以previousIndex(旧数组中的下标)为索引的)。
刚才对于forEachOperation
的讨论咱们常常忽略项的增长和删除。其实增长、删除项对其余项的下标也有影响,道理相似,只不过此次咱们只须要用addRemoveOffset
变量记录【到目前为止的计算中,已经增长了多少个项】(若是删除的项比增长的多,则这个值为负数),而后【在经过原下标计算临时下标的时候】加上这个值就行了。
刚才对于forEachOperation
的讨论中,咱们也没有说明【在什么状况咱们要计算出一个项的增长或删除操做】。全部要被删除的项,在diff
执行完毕后都被放到了_removalsHead
链表中。诚然,咱们能够在计算出全部移动操做以前先将删除操做输出,可是Angular彷佛以为这样不够天然。按照咱们上面的算法,【旧数组】的每一步操做,逐渐使得【更长的旧数组前缀与新数组匹配】,而先执行全部删除操做会破坏这种【从左往右逐一匹配】的感受。所以Angular实现的forEachOperation
,对_itHead
从左往右匹配,当匹配到被删除项的时候,再执行删除操做:
在遍历_itHead
链表时,正在匹配的项在目标数组的下标是nextIt.currentIndex,若是nextIt.currentIndex>=【nextRemove的临时下标】(此时这个三元表达式的值是nextRemove),就要输出删除nextRemove的操做(若是咱们不删除也不移动nextRemove,此时应该轮到nextRemove被匹配了)。好比[a,b,c,d]=>[d,a,c,e]
,遍历到_itHead
(新数组)的c
时,临时数组为[d,a,b,c]
,发现nextRemove(在这个例子中是b)在临时数组中的c
以前出现(nextIt.currentIndex>=【nextRemove的临时下标】),所以这一步不匹配c
,而先删除b
。
以Angular的一个单元测试为例:
[0, 1, 2, 3, 4, 5] => [6, 2, 7, 0, 4, 8]
在diff的过程当中,_itHead
和_unlinkedRecords
的变化过程以下(括号中的项是被放入_unlinkedRecords
的,加粗表示这部分_itHead
前缀已经与目标数组相匹配):
0 1 2 3 4 5 ()
6 1 2 3 4 5 (0)
6 2 3 4 5 (0 1)
6 2 7 4 5 (0 1 3)
6 2 7 0 5 (1 3 4)
6 2 7 0 4 (1 3 5)
6 2 7 0 4 8 (1 3 5)
所以diff完成之后,DefaultIterableDiffer
内部的链表处于以下状态([]
表示该项下标的变化):
collection: ['6[null->0]', '2[2->1]', '7[null->2]', '0[0->3]', '4', '8[null->5]'], previous: ['0[0->3]', '1[1->null]', '2[2->1]', '3[3->null]', '4', '5[5->null]'], additions: ['6[null->0]', '7[null->2]', '8[null->5]'], moves: ['2[2->1]', '0[0->3]'], removals: ['1[1->null]', '3[3->null]', '5[5->null]']
diff完成之后就能够经过forEachOperation
来获取(逻辑上的)更新操做了。forEachOperation
会输出以下更新操做,这些操做能将旧数组更新为当前数组。(()
中表示这次操做形成的临时下标的变化,[]
中表示这一项在就旧组中的下标,也就是item.previousIndex
)
'INSERT 6 (VOID -> 0)', 'MOVE 2 (3 -> 1) [o=2]', 'INSERT 7 (VOID -> 2)', 'REMOVE 1 (4 -> VOID) [o=1]', 'REMOVE 3 (4 -> VOID) [o=3]', 'REMOVE 5 (5 -> VOID) [o=5]', 'INSERT 8 (VOID -> 5)'
forEachOperation
的执行过程当中,构造出的临时数组以下:
0 1 2 3 4 5
6 0 1 2 3 4 5 // 'INSERT 6 (VOID -> 0)',
6 2 0 1 3 4 5 // 'MOVE 2 (3 -> 1) [o=2]',
6 2 7 0 1 3 4 5 // 'INSERT 7 (VOID -> 2)',
6 2 7 0 1 3 4 5 // 0 不须要移动
6 2 7 0 3 4 5 // 'REMOVE 1 (4 -> VOID) [o=1]',
6 2 7 0 4 5 // 'REMOVE 3 (4 -> VOID) [o=3]',
6 2 7 0 4 5 // 4 不须要移动
6 2 7 0 4 // 'REMOVE 5 (5 -> VOID) [o=5]',
6 2 7 0 4 8 // 'INSERT 8 (VOID -> 5)'
小练习:[a,b,c,d,e]=>[a,e,f,b,d]
的diff
过程、forEachOperation
输出是怎么样的?
更多范例能够查看Angular的相关单元测试。
至此,变动算法已经介绍完了,上面的介绍忽略了一些维护链表的细节和边界状况的考虑,有兴趣的读者能够本身阅读一遍源代码。