GVKun编程网logo

Angular源码剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的变更检测算法(angular 变更检测)

25

针对Angular源码剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的变更检测算法和angular变更检测这两个问题,本篇文章进行了详细的解答,同时本文还

针对Angular源码剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的变更检测算法angular 变更检测这两个问题,本篇文章进行了详细的解答,同时本文还将给你拓展(OK) grub2-set-default——kernel updates keep defaulting to debug kernel、Angular Observable的Google Material Default选择值、angular – DoCheck的KeyValueDiffer仅适用于每个组件的一个对象吗?、angularjs select number type default value required validate等相关知识,希望可以帮助到你。

本文目录一览:

Angular源码剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的变更检测算法(angular 变更检测)

Angular源码剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的变更检测算法(angular 变更检测)

前言

Angular原生实现了两个工具类:DefaultKeyValueDiffer和DefaultIterableDiffer,它们分别用来检查两个对象或两个数组之间的差别(也就是diff)。典型的使用场景是:检查某个变量在两个时刻之间是否发生了改变、发生了什么样的改变,在这篇文章中,我们称它为变更检测

请将diff与change detection区分开来。

Angular的变化检测默认只比较对象的引用是否改变,但是我们可以通过DoCheck生命周期钩子来做一些额外的检测,比如检查对象是否增加删除改动了某些属性、检查数组是否增加删除移动了某些条目。这个时候变更检测就可以派上用场了。
举个例子,NgForOf指令内部就是通过DefaultIterableDiffer来检测输入数组发生了怎样的变化,从而能够用最小的代价去更新DOM。

这两个工具类中包含的算法可以说是十分通用的,甚至可以移植到其他框架、语言去。除此之外,掌握这种变更检测算法也能够帮助我们更好地理解、使用NgForOf,甚至编写自己的结构型指令。

在我们通过源码了解它们的算法之前,我先简单地介绍一下Differ是如何使用的。

如何在Angular中使用Differ

要使用这两个工具类,并不需要(也不应该)自己创建这两个类的实例,BrowserModule已经将将它们注册在注入器中。

以下代码展示了如何获取和使用DefaultkeyvalueDiffer:

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.currentValue);
    });
    changes2.forEachRemovedItem((r) => {
      console.log(r.key,r.currentValue);
    });
    console.log('--------------------')
  }
}

DefaultIterableDiffer的使用是完全类似的。你也可以参考api文档。

你可以从这个例子初步体会到“抽象”的威力。使用者调用interface KeyValueDiffer定义的API,而完全不知道(也不需要知道)背后DefaultKeyValueDiffer这个类的存在。

更棒的是,我们等一下可以看到,我们可以自己实现特殊用途的keyvalueDiffer工具类(工具类实现这个接口),然后这个工具类就能被加入到keyvalueDiffers中,从而能在应用的指定范围内分发,因此这套系统(可以命名为“Differ供应系统”)具有很强的可扩展性

keyvalueDiffer

先抛开DefaultkeyvalueDiffer本身不谈,我们先从源码来看看keyvalueDiffer供应系统是如何实现的。

keyvalueDiffer供应系统

这个系统主要由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供应系统有以下优点:

  1. 扩展性好,你可以自己实现keyvalueDiffer(比如Date的变更检测)。只要分别用class实现keyvalueDifferFactory和keyvalueDiffer接口,然后keyvalueDiffers就可以帮助你分发你的keyvalueDifferFactory。并且,使用者通过统一的API来与Factory和Differ进行交互。
  2. keyvalueDiffers的继承关系类似于注入器的层级关系,帮助你简化keyvalueDiffers的创建,理清find的查找顺序。
  3. 将keyvalueDiffers注册在某个ngModule providers或者Component providers中(不要覆盖掉ApplicationModule注册的keyvalueDiffers!否则你无法获取到DeafultDiffer)。通过控制keyvalueDiffers的依赖注入有效范围,你可以控制你的keyvalueDiffers的分发范围。
  4. @H_301_98@

    DefaultkeyvalueDiffer

    让我们从源码研究它。

    注意到它同时实现了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就包含了变更检测算法:

    1. 调用reset()为迁移到下一个状态作准备(包括:更新_prevIoUsMapHead链表,更新每个record的 _nextPrevIoUs指针和prevIoUsValue,清空_changesHead _additionsHead _removalsHead链表)
    2. 遍历新传入的obj的每个属性,依次与_mapHead比较(_mapHead存储的还是旧obj的record)。

      1. 如果key相同,则比较value,如果value不同,则更新这个record并将它加入_changesHead 链表。
      2. 如果key都不相同,那么有可能这个key在链表的后面。因此在_getorCreateRecordForKey方法中,先尝试从_records Map找到这个key,如果找到了就比较其value是否与新obj中的value相同(如果不同的话就_addtochanges),然后将它暂时从_mapHead链表中删除,_getorCreateRecordForKey返回这个record(等一下会插入到链表的正确位置); 如果在Map找不到这个key,说明这是一个新加入的属性,则创建一个新的record并加入_additionsHead链表,_getorCreateRecordForKey返回这个新建的record(等一下再插入到_mapHead链表中)。_getorCreateRecordForKey执行完毕以后,将返回的record插入_mapHead链表的正确位置。
      3. @H_301_98@
      4. 新传入的obj的每个属性都遍历过以后,如果_mapHead链表中还有尚未访问的record,这些record都是被删除的。将它们从_mapHead移除、加入_removalsHead、从_records中删除这些条目、更新这些record的状态。
      5. @H_301_98@

        实现这个算法时要理清楚什么时候更新_changesHead _additionsHead _removalsHead链表,也就是什么情况意味着发现了change、addition、removal。这在上面的表述中已经说明了。
        理清楚了这一点以后,剩下的就是维护链表的操作了。同时维护这么多的链表确实是一件很容易出错的事情。

        IterableDiffer

        IterableDiffer用来对数组或类数组对象进行变更检测。

        IterableDiffer供应系统

        IterableDiffer供应系统与keyvalueDiffer供应系统非常类似,这里只讨论几个比较重要的地方:

        1. IterableChanges.forEachOperation可以让用户知道,这个数组的上次变更中做了哪些操作。也就是说从旧arr经历哪些增加、删除、移动能够变成新arr。注意这些操作不一定是实际发生在旧arr上的,毕竟有不止一种操作能够将旧arr变成新arr。
        2. IterableDiffer通过trackByFn来确定新arr中的某个项与旧arr中某个项是否相同。而刚才的DefaultkeyvalueDiffer是直接通过looseIdentical来判断新旧value是否相同的(大致等同于===判断)。
        3. IterableChanges.forEachIdentityChange可以让用户看到所有trackById相同但Identity变化(相当于a!==b)的那些条目。
        4. @H_301_98@

          DefaultIterableDiffer

          那些简单的,或者DefaultkeyvalueDiffer也有的类成员我就不一一介绍了。

          与前面类似地,变更检测的逻辑封装在_check函数中。让我们从这里开始。

          1. this._reset()进行初步的状态更新。包括:更新_prevIoUsItHead链表,更新每个record的 _nextPrevIoUs指针,重置prevIoUsIndex,清空_additionsHead _movesHead _removalsHead _identityChangesHead链表。
          2. 判断Array.isArray(collection),由于DefaultIterableDiffer支持一些类数组对象,因此在判断不成功的时候会执行另一种算法来检测变更。我们不妨假设检测正常数组的变更。
          3. 对于collection(新数组)的每个项,执行以下操作(用下标index来遍历collection):

            1. this._trackByFn(index,item)计算当前项的标识值。**如果新旧数组之间的两个项的标识值相等,我们就认为它们是同一个项,不管identity是否一样(即不管a===b是否成立),我们都认为顺序没有变化。
            2. 比较_itHead链表(旧数组)的第index个项(命名为item1)与collection的第index个项(命名为item2),它们标识值是否相同:

              • 如果不相同,调用_mismatch来处理,使得item2成为_itHead链表的第index个项:

                1. 先将“item1”从_itHead链表中删除,毕竟它们没有在正确的位置上(如果后面发现有这个项,再将它加到合适的位置)。然后将它加入_unlinkedRecords中(它是_DuplicateMap类型,也就是MuitiMap的一种实现。之所以要用到MuitiMap,是因为数组中可能有多个项的标识值相同),然后将它加入_removalsHead链表中。
                2. 尝试在旧数组的index以后的项中找到有相同标识值的项。如果找到的话,就检测到移动变更,于是要将这个项从_itHead链表中原来的位置移动到index位置,并加入_movesHead链表。如果没有在旧数组找到相同标识值的项,尝试从_unlinkedRecords找到相同标识值的项。如果找到的话,同样检测到移动变更。将这个项从_unlinkedRecordsMap和_removalsHead链表移除(撤销_addToRemovals操作),然后插入到_itHead链表的index位置。如果从_unlinkedRecords还是没找到相同标识值的项,说明这是一个新增加的项,于是将它插入_itHead链表的index位置并加入_additionsHead链表。
                3. @H_301_98@
                  • _mismatch执行完毕以后。设置maybedirty = true。这个标识表示将来每次检测到item1item2标识值相等得时候,要调用_verifyReinsertion来修正某种错误,下面再谈。
                4. 如果标识值相同,且maybedirty==true,需要调用_verifyReinsertion来检查前面步骤可能产生的插入顺序错误:假设发生变更[a,a] => [b,a,a],那么在对比链表中的a和新数组的b以后,会删除链表中的a(链表存储的是旧数组),然后插入新数组的b,接下来,链表中的下一个项依然是a,就会匹配新数组中的第一个a(旧数组的第二个a匹配新数组的第一个a),接下来会在链表的末尾reinsert刚才删除的a(原数组的第一个a)。经过这样的变更检测以后,两个a的顺序变了正确的做法应该是“将b插入数组0位置”,而不是“将数组0位置的a换成b,然后在数组末尾加入a”。
                  Angular纠正这种错误的方法是:每次检测到item1(旧数组项)与item2(新数组项)标识值相等得时候,如果maybedirty==true,不马上认定item2就对应于原数组的item1,而是先检查之前是否删除过相同标识值的项(检查_unlinkedRecords中是否有相同标识值的项),如果有删除过,则这个项才与item2对应,于是撤销被删除项的_addToRemovals操作,并将这个项reinsert到链表的index位置。
                  _verifyReinsertion还有另一个作用,你那就是检查record.currentIndex是否正确。假如在record前面已经插入一个项并删除一个项,那么currentIndex不需要改变;但是如果只是前面插入了1个,那么插入项以后的所有项的currentIndex都要+1,然后记录这个移动操作(_addToMoves)。在这种情况下,虽然说“这些项都移动了”不太准确,但是毕竟它们所在的下标都变化了,我们还是先记录这些移动,以后调用forEachOperation的时候会过滤掉这种不严格的移动。
            3. @H_301_98@
            4. 如果新数组的所有项都遍历完了,_itHead链表后面还有没访问到的项,则这些项是被删除的。使用_truncate从链表中删掉它们,并记录它们的删除(_addToRemovals)。_truncate除此之外还做一些收尾工作:将检测变更时用来查询的_unlinkedRecordsMap清空(这些是被删除的项,它们已经被执行_addToRemovals了),然后将各种链表尾的next赋值为null(我们之前加入链表的时候都没有考虑它是不是链表尾)。
            5. @H_301_98@

              可以看出,算法的重点在于第3步的for循环。for循环刚开始的时候,_itHead链表还是旧数组的状态。然后经过一轮循环,就修改_itHead链表,将正确的项移动到_itHead链表的index位置。因此,这个for循环从左向右逐项更新_itHead链表,使得它有越来越长的前缀与新数组匹配。

              DefaultIterableDiffer.forEachOperation

              diff执行完毕以后,变更的信息就存储在DefaultIterableDiffer的那些链表中了,用户可以通过IterableChanges.forEachOperation得到一系列数组操作(增加删除移动),这些操作能将旧数组更新为新数组。注意,这些数组操作是通过计算得到的,不一定是实际发生在旧数组上的操作。
              forEachOperation是如何通过变更信息计算出可能发生的操作序列呢?看源码之前,首先应该思考它的思路是怎么样的,否则这段代码会看得非常费劲。
              发生在数组上的变更操作无非三种:增加项、删除项、移动项(两个项的交换可以看作两次移动)。其中,移动项又可以分为向前移动(下标变小)和向后移动(下标变大)。我们之前已经提到过,将某个项向前移动时,它所“经过”的那些项的下标会+1。这种下标+1只是其他移动的副产品,不应该算作真正的向后移动。比如对于变更[a,b,c]=>[c,b],我们自然的想法是“c从2移动到0”,而bc下标的增加不应另算作变更
              进一步思考,如果项item的下标增加,其实全都是因为item后面的一些项移动到了item前面(现在仅考虑移动项,不考虑增加项)。也就是说,向后移动都可以替换为其他项的向前移动,我们不再需要考虑向后移动了
              举个例子,[a,c,d]=>[d,a]的变更操作序列是:d向前移动到0位置,c向前移动到1位置,b向前移动到2位置,a不需要自己移动

              forEachOperation的计算操作序列算法可以简述如下(先只考虑移动项,不考虑有增加项和删除项的情况):
              遍历_itHead链表(此时diff已经执行完,_itHead链表的顺序与新数组相同),对于每一项record,依次检查其临时下标和目标下标(在源码中分别命名为adjPrevIoUsIndexcurrentIndex)。临时下标的意思是,旧数组刚执行完已计算出的操作所得到的临时状态中,这个项的下标。目标下标的意思是,这个项在最终目标数组中的下标。比如,计算[a,a]的变更操作序列时,已经计算出“d移动到0,b移动到1”,旧数组执行完这两个操作以后的临时状态为[d,c],a的临时下标为2,c的临时下标为3,目标下标始终分别是3和2。

              • 如果adjPrevIoUsIndex===currentIndex,说明在当前状态中,这个项恰好处于目标位置,不需要移动。
              • 如果adjPrevIoUsIndex>currentIndex,说明在当前状态中,这个项需要被向前移动,才能到达目标位置。这个if就是判断这个情况的。
              • 按照这个算法执行,不可能出现adjPrevIoUsIndex<currentIndex的情况。

              其实adjPrevIoUsIndexcurrentIndex分别表示【不忽略增加、删除项情况下的】临时下标和目标下标。通过以下两个减法,能计算出【忽略增加、删除项的情况下的——也就是说假设被增加、删除的项从来都不存在】临时下标和目标下标:

              const localMovePrevIoUsIndex = adjPrevIoUsIndex - addRemoveOffset;
              const localCurrentIndex = currentIndex ! - addRemoveOffset;

              因为addRemoveOffset变量记录了到目前为止的计算中,已经增加了多少个项(如果删除的项比增加的多,则这个值为负数),所以减掉这个数以后就是(忽略被增加的项的情况下的)临时下标和目标下标。

              那么adjPrevIoUsIndex(临时下标)是如何得到的呢?adjPreviousIndex的计算函数需要知道【item在旧数组的下标:prevIoUsIndex】、【刚刚讲过的addRemoveOffset】、【item被多少个向前移动的项“经过”:moveOffset】,结果adjPrevIoUsIndex就是三者之和,它就是“item在【旧数组执行完已知操作以后的临时数组】中的下标”。

              既然我们需要知道各个项被多少个向前移动的项“经过”,那么我们应该在向前移动某项的时候就记录它经过了哪些项。比如[a,d]=>[a,d,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循环比较难懂,这里解释一下:

              1. Angular使用moveOffsets这个数组来存储各个项的moveOffset。这个数组以previousIndex(旧数组中的下标)为索引。
              2. for循环的范围是(let i = 0; i < localMovePrevIoUsIndex; i++),如何理解?我们正在检查临时数组(旧数组执行完已知操作以后的临时状态,忽略增加、删除的项)的第localCurrentIndex个项,此时我们发现localMovePreviousIndex != localCurrentIndex。因此这个向要从临时数组localMovePrevIoUsIndex位置移动到localCurrentIndex位置。因此临时数组下标范围[localMovePrevIoUsIndex,localCurrentIndex)中的项都需要moveOffset+=1。为了更新moveOffset,我们需要知道这些项在【旧数组】中的下标。可是我们怎么知道这些项在【旧数组】中的下标呢?我们无法从【临时下标】计算出【旧数组下标】。但是我们能够确定的是这些项在旧数组的下标肯定小于localMovePrevIoUsIndex(因为这些项肯定还没有被向前移动,它们只能被那些【向前移动的项】“经过”,下标只可能增加),于是我们就对【旧数组中所有下标小于localMovePrevIoUsIndex的每个项】计算它们在【临时数组】中的下标(这就是for循环的范围由来),然后判断它在【临时数组】中的下标是否处于范围[localMovePrevIoUsIndex,localCurrentIndex),如果是的话我们就更新moveOffsets[i]
              3. @H_301_98@

                总结一下这个算法的思路
                算法接受一个临时数组和一个目标数组(最开始临时数组是旧数组)。这个算法不断从临时数组构造一个新的临时数组,使得新的临时数组有更长的前缀匹配目标数组,直到构造出的临时数组与目标数组完全相同。如何构造新的临时数组呢?将临时数组中的某一项向前移动,移动到正确的位置。比如临时数组是[a,e],目标数组是[a,e],我们构造出的下一个临时数组是[a,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,e],遍历到_itHead(新数组)的c时,临时数组为[d,c],发现nextRemove(在这个例子中是b)在临时数组中的c之前出现(nextIt.currentIndex>=【nextRemove的临时下标】),因此这一步不匹配c,而先删除b

                实例

                以Angular的一个单元测试为例:

                [0,1,2,3,4,5] =>
                [6,7,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]','3[3->null]','5[5->null]'],additions: ['6[null->0]',moves: ['2[2->1]','0[0->3]'],removals: ['1[1->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,e]=>[a,e,f,d]diff过程、forEachOperation输出是怎么样的?

                更多范例可以查看Angular的相关单元测试。

                至此,变更算法已经介绍完了,上面的介绍忽略了一些维护链表的细节和边界情况的考虑,有兴趣的读者可以自己阅读一遍源代码。

(OK) grub2-set-default——kernel updates keep defaulting to debug kernel

(OK) grub2-set-default——kernel updates keep defaulting to debug kernel


https://bugzilla.redhat.com/show_bug.cgi?id=1321927



Kambiz Aghaiepour 2016-03-29 07:50:53 EDT


Description of problem:

every time I apply updates if a kernel update is included, grub defaults to the debug kernel.

See:

# grep -P "submenu|^menuentry" /boot/grub2/grub.cfg | cut -d "''" -f2

Fedora (4.4.6-300.fc23.x86_64+debug) 23 (Workstation Edition)
Fedora (4.4.6-300.fc23.x86_64) 23 (Workstation Edition)
Fedora (4.4.4-301.fc23.x86_64) 23 (Workstation Edition)
Fedora (4.4.3-300.fc23.x86_64) 23 (Workstation Edition)
Fedora (4.4.4-301.fc23.x86_64+debug) 23 (Workstation Edition)
Fedora (4.4.3-300.fc23.x86_64+debug) 23 (Workstation Edition)
Fedora (0-rescue-a80b5fcc380d4d43887e523edaf01db0) 23 (Workstation Edition)

Even though:

# grub2-editenv list
saved_entry=Fedora (4.4.6-300.fc23.x86_64) 23 (Workstation Edition)

When I reboot, the highlighted entry is the first entry (4.4.6-300.fc23.x86_64+debug).  However, after I run:

# grub2-set-default "Fedora (4.4.6-300.fc23.x86_64) 23 (Workstation Edition)"

followed by:

# grub2-mkconfig -o /boot/grub2/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-4.4.6-300.fc23.x86_64
Found initrd image: /boot/initramfs-4.4.6-300.fc23.x86_64.img
Found linux image: /boot/vmlinuz-4.4.4-301.fc23.x86_64
Found initrd image: /boot/initramfs-4.4.4-301.fc23.x86_64.img
Found linux image: /boot/vmlinuz-4.4.3-300.fc23.x86_64
Found initrd image:/boot/initramfs4.4.3-300.fc23.x86_64.img
Found linux image: /boot/vmlinuz-4.4.6-300.fc23.x86_64+debug
Found initrd image: /boot/initramfs-4.4.6-300.fc23.x86_64+debug.img
Found linux image: /boot/vmlinuz-4.4.4-301.fc23.x86_64+debug
Found initrd image: /boot/initramfs-4.4.4-301.fc23.x86_64+debug.img
Found linux image: /boot/vmlinuz-4.4.3-300.fc23.x86_64+debug
Found initrd image: /boot/initramfs-4.4.3-300.fc23.x86_64+debug.img
Found linux image: /boot/vmlinuz-0-rescue-a80b5fcc380d4d43887e523edaf01db0
Found initrd image: /boot/initramfs-0-rescue-a80b5fcc380d4d43887e523edaf01db0.img
done

Then the system reboots correctly into the desired kernel, until the next kernel errata.  I will attempt to erase "kernel-debug-core" to see if this will fix the problem for future releases though the behavior above seems buggy (i.e. when the default kernel is not the debug kernel, then the new kernel installed should not default to the debug version)

Angular Observable的Google Material Default选择值

Angular Observable的Google Material Default选择值

如何解决Angular Observable的Google Material Default选择值?

我有一个Select,它绑定到可观察的值。我希望能够将默认项设置为第三项:


library(shiny)
library(questionr)
library(dplyr)
library(ggplot2)
library(tidyr)
library(haven)
library(labelled)
library(scales)
library(sjlabelled)


fre <- function(var) {
  var_str1 <- var
  var <- rlang::ensym(var)
  
  abc <- questionr::na.rm(dat[,var_str1])
  abc <- questionr::freq(abc,total = TRUE,na.last = TRUE,digits = 2)
  abc <- cbind(Label = rownames(abc),abc)
  abc <- questionr::rename.variable(abc,"n","Frequency")
  abc <- questionr::rename.variable(abc,"%","Percent")
  abc <- tidyr::separate(abc,Label,into = c("Value","Label"),sep = "] ")
  row.names(abc) <- NULL
  abc <- abc %>% dplyr::mutate(Value = gsub("[[:punct:]]",'''',Value)) %>% 
    dplyr::select(Label,Value,Frequency,Percent)
  abc
}

bar_plot <- function(data,var) {
  
  data <- do.call(as_label,list(data,ensym(var)))
  
  data %>% 
    filter({{ var }} != "Neither") %>% 
    ggplot(aes({{ var }})) +
    geom_bar() +
    coord_flip() +
    theme_classic() +
    labs(x = NULL,y = "Count",title = var_label(pull(data,{{ var }})))
}


dat <- read_spss("http://staff.bath.ac.uk/pssiw/stats2/SAQ.sav")



ui <- fluidPage(

    titlePanel(" "),sidebarLayout(
        sidebarPanel(
            selectInput("var1","Frequency Table",choices =  names(dat),selected = NULL)
        ),mainPanel(
           tableOutput("fretab"),plotOutput("barplot")
        )
    )
)


server <- function(input,output) {

    output$fretab <- renderTable({
        fre(input$var1)
    })
    
    output$barplot <- renderPlot({
      bar_plot(dat,input$var1)
    })
}


shinyApp(ui = ui,server = server)

在我的组件中,数据来自http后端Observable:

    <mat-form-field appearance="fill">
      <mat-label>Status</mat-label>
      <mat-select name="status" [formControl]="status">
        <mat-option>None</mat-option>
        <mat-option *ngFor="let status of caseStatuses$ | async" [value]="status.value">
          {{ status.name }}
        </mat-option>
      </mat-select>
    </mat-form-field>

完成可观察性之后,如何将“选择”设置为值之一(第三个值)

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

angular – DoCheck的KeyValueDiffer仅适用于每个组件的一个对象吗?

angular – DoCheck的KeyValueDiffer仅适用于每个组件的一个对象吗?

首先,我在我的ngDoCheck方法中使用了这个并且完美地工作:

var productChanges = this.differ.diff(this.myProduct);

然后决定检查我的组件的第二个模型的更改并添加以下行:

var companyChanges = this.differ.diff(this.myCompany);

并且两个更改都在单独的if语句中进行,但只有最后一个被调用(companyChanges)

这是预期的行为吗? ngDoCheck仅适用于每个组件的一个对象吗?

为了清楚起见,这是我的完整ngDoCheck方法:

ngDoCheck() {
    var productChanges = this.differ.diff(this.myProduct);

    //DOESN'T IT CHECK 2 MODELS LIKE SO BELOW ?
    //var companyChanges = this.differ.diff(this.myCompany);

    if (productChanges) {
        // console.log('Product changes detected');

        productChanges.forEachChangedItem((r: keyvalueChangeRecord) => {

            if (r.currentValue && r.currentValue != r.prevIoUsValue) {
                this.filterProduct(r.currentValue,r.key);

            }
        });
    }

编辑:通过阅读其他问题和答案,我觉得我需要分享这个:

在组件构造函数中,不同的定义如下:

this.differ = differs.find({}).create(null);

可能这是首先需要改变的.

解决方法

在阅读@ thierry-templier对这个问题的回答后: Detect changes in objects inside array in Angular2我已经知道它是如何工作的:

类级别不同对象应包含要监视的每个模型的单独键,并且对于它们的值,它们需要分别在ngOnInit或构造函数中设置分别引用每个模型的不同观察者. (Thierry在ngOnInit中做到了,我在构造函数中做到了)

constructor(private differs: keyvalueDiffers){
    this.differ = {};

    this.differ['myCompany'] = differs.find(this.myCompany).create(null);
      .
      .
      .
    this.differ['myProduct'] = differs.find(this.myProduct).create(null);
}

ngDoCheck() {
    var productChanges = this.differ['myProduct'].diff(this.myProduct);
    var companyChanges = this.differ['myCompany'].diff(this.myCompany);

    if (productChanges) {
        // Do your thing 
    }

    if (companyChanges) {
        // Do your thing 
    }
}

angularjs select number type default value required validate

angularjs select number type default value required validate

<!DOCTYPE html>
<html lang="en">
<head>
    <Meta charset="UTF-8">
    <link href="http://cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div ng-app="app" ng-controller="controller">
        <form name="form" novalidate>
            <divng->
                <label for="exampleInputEmail1">select</label>
                <!--http://stackoverflow.com/questions/32304519/ng-pattern-regex-to-allow-all-characters-except-decimal-value-->
                <selectng-model="item.value" name="value" convert-to-number ng-pattern="/^(?!0$).*/" required>
                    <option value="0" disabled>please choose a number</option>
                    <option ng-repeat="option in [1,2,3,4,5]" value="{{option}}">{{option}}</option>
                </select>
                <span ng-show="form.value.$error.required">required</span>
                <span ng-show="form.value.$error.pattern">pattern</span>
            </div>
            <button type="button"ng-click="submit(form)">submit</button>
        </form>
    </div>
    <script src="http://cdn.bootcss.com/angular.js/1.5.6/angular.min.js"></script>
    <script>
        'use strict';

        var app = angular.module('app',[])

        app.controller('controller',function ($scope) {
            $scope.item = {
                value: 0,}
            $scope.submit = function (form) {
                if (!form.$valid) {
                    return
                }
                alert(JSON.stringify($scope.item))
            }
        })

        //http://stackoverflow.com/a/35407627/2586541
        app.directive('convertToNumber',function () {
            return {
                require: 'ngModel',link: function (scope,element,attrs,ngModel) {
                    //value
                    ngModel.$parsers.push(function (val) {
                        //return '' + val;
                        return parseInt(val,10);
                    });
                    //show
                    ngModel.$formatters.push(function (val) {
                        //return parseInt(val,10);
                        return '' + parseInt(val || 0,10);
                    });
                }
            };
        });
    </script>
</body>
</html>

今天关于Angular源码剖析:DefaultKeyValueDiffer和DefaultIterableDiffer的变更检测算法angular 变更检测的分享就到这里,希望大家有所收获,若想了解更多关于(OK) grub2-set-default——kernel updates keep defaulting to debug kernel、Angular Observable的Google Material Default选择值、angular – DoCheck的KeyValueDiffer仅适用于每个组件的一个对象吗?、angularjs select number type default value required validate等相关知识,可以在本站进行查询。

本文标签: