23.angular的一些零散知识(二)

1.ElementRef、TemplateRef、ViewContainerRef

ElementRef 相当于获取一个 DOM,其nativeElement属性就是DOM
TemplateRef 用来获取一个 template 模板
ViewContainerRef 用来获取一个视图容器(作用就是获取一个DOM,并把它当插入其他DOM的容器,只不过插入是插入到其后面,成为其兄弟元素)

场景:想获取当前组件内的某个DOM。可使用ElementRef或者@ViewChild

// 使用ElementRef,直接注入ElementRef,可获得当前组件的 ElementRef
export class AboutComponent implements OnInit {
  constructor(private ele: ElementRef) {}
  ngOnInit(): void {
    const btn = this.ele.nativeElement.querySelector('#btn');
  }
}
// 使用ViewChild
export class AboutComponent implements OnInit {
  @ViewChild('#btn', {static: true}) btnEle: ElementRef
}
2.ViewChild、ViewChildren、ContentChild、ContentChildren

ViewChild 获取页面元素 \ 组件

export declare interface ViewChildDecorator {
    (selector: Type | Function | string, opts: {
        read?: any;
        static: boolean;
    }): any;
}

这个 api 的语义话非常好:可以理解为"表示查询一个子元素,并从它身上获取(read)什么"

selector
一个组件或一个指令
@ViewChild(ChildComponent)
一个模板引用变量的字符串

@ViewChild('cmp')
一个子组件上注册的提供商
@ViewChild(TOKEN)
一个TemplateRef
@ViewChild(TemplateRef) tpl: TemplateRef

函数形式怎么用不知道

read (从查询到的元素中读取另一个令牌)
read的内容才是希望获取的东西,比如:

// 查询子元素ChildComponent,并从它身上读取ElementRef
@ViewChild(ChildComponent, {static: true, read: ElementRef} ) ele: ElementRef

// 查询子元素ChildComponent,并从它身上读取一个视图容器
@ViewChild(ChildComponent, {static: true, read: ViewContainerRef} ) vcref: ViewContainerRef

// 查询子元素ChildComponent,并从它身上读取一个服务
@ViewChild(ChildComponent, {static: true, read: MsgService} ) msg: MsgService

read是可选的,当不指明的时候,应该是查询什么,就获取什么。比如

// 二者作用一样
@ViewChild(ChildComponent, {static: true, read: ChildComponent} ) component: ChildComponent
@ViewChild(ChildComponent, {static: true} ) component: ChildComponent

static (指定查询元素的时机,查询只会执行一次,其结果会缓存
true 在变更检测前查询元素(ngChanges之前)
false 在变更检测后查询元素(ngDoCheck之后,ngAfterViewInit之前)

@Component({
  template: `
    
`, }) export class AboutComponent implements OnInit, AfterViewInit { @ViewChild('div1', { static: true }) div1: ElementRef; @ViewChild('div2', { static: false }) div2: ElementRef; ngOnInit() { console.log(this.div1); console.log(this.div2); } ngAfterViewInit() { console.log(this.div1); console.log(this.div2); } }

上述过程是:
div1生成 -> 查询div1 -> ngChanges -> ngOnInit -> ngDoCheck -> div2生成 -> 查询div2 -> ngAfterViewInit...

3.zone.js

angular 和 angularjs 中双向绑定的实现方式是脏值检查,当某些可能导致值发生变化的事情发生之后,就去检查值是否变化、更新视图......可能导致值发生变化的事情包括setTimeout、setInterval、XHR、dom事件。

angularjs中,脏检查就存在问题,不能让异步事件完毕后自动调用触发检测。比如:

function foo() {
    $scope.user = '张三'
}
setTomeout(foo, 0)
$apply() // 更新视图

当使用原生setTimeout去改变值后,无法让foo执行完毕之后,自动调用 $apply() 或者 $degist()。解决办法是开发者手动去调用 $apply() 触发新一轮的变更检测

function foo() {
    $scope.user = '张三';
    $apply() // 更新视图
}
setTomeout(foo, 0)

或者用封装的 $timeout 代替 setTimeout

function foo() {
    $scope.user = '张三';
}
$timeout(foo, 0)

$timeout可能就长这样:

var $timeout = function(fn, time) {
    var that = this;
    return setTimeout(function() {
        fn.apply(that);
        $apply();
    }, time)
}

angular2 改进了这个问题:
zone.js 使用mokey patch(称猴子补丁或动态补丁)的方式覆盖掉了原生的setTimeout等异步方法,其具体实现很复杂,反正效果是使得异步任务进入执行栈、执行完毕等过程都可以被监听到。

*zone.js不仅覆盖掉了 setTomeout 等原生API,还专门覆盖掉了其 toString 方法,使得直接在控制台 setTomeout 或者 setTomeout.toString 得到的是 native code,可以直接 console.dir(window) 去控制台查看 setTomeout *

这个例子展示了zone.js中onScheduleTask、onInvokeTask两个钩子监听到异步任务进入执行栈、执行完毕。

let timer;

const zone = Zone.current.fork({
    name: 'z',
    onScheduleTask(delegate, currentZone, targetZone, task) {
      const result = delegate.scheduleTask(targetZone, task);
      const name = task.callback.name;
      console.log(
          Date.now() - timer, 
         `task with callback '${name}' is added to the task queue`
      );
      return result;
    },
    onInvokeTask(delegate, currentZone, targetZone, task, ...args) {
      const result = delegate.invokeTask(targetZone, task, ...args);
      const name = task.callback.name;
      console.log(
        Date.now() - timer, 
       `task with callback '${name}' is removed from the task queue`
     );
     return result;
    }
});

function a1() {}
function a2() {}

function b() {
    timer = Date.now();
    setTimeout(a1, 2000);
    setTimeout(a2, 4000);
}

zone.run(b);

在监听到异步执行完毕之后,再由 ngZone 去执行启动变更检测等一系列调度。
所以zone.js是和angular框架完全解耦的,可以单独拿到其他项目中去用,去监听异步任务。
ngZone,还可以用来对项目进行一些优化。
比如有个频繁变动的值,将导致频繁执行变更检测。可以用ngZone使angular暂时不跟踪其变化

constructor(private zone: NgZone) { }

this.zone.runOutsideAngular(() => {
   for (let i = 0; i < 100; i++) {
     setInterval(() => this.counter++, 10);
   }
});

稳定后,重新跟踪变化

this.zone.run(() => {
  setTimeout(() => this.foo = this.foo, 1000);
});
4.变更检测

结论:

  • 可以使用ChangeDetectionStrategy.OnPush 对项目进行优化。OnPush策略会使组件及其子组件在发生外部异步事件时,不再执行变更检测。对于其输入属性发生变化,或其自己内部发生异步事件,还是会正常进行检测并更新视图
  • 输入属性改变是指 oldValue !== newValue,所以修改了对象的属性不算输入属性改变。这算是一个“小缺点”
  • 对于上面这个"小缺点",可以使用Immutable 变量避免,或使用bservable作为输入属性
  • ChangeDetectorRef 是当前组件的变更检测器的引用。可用来分离当前检测器、重新恢复变更检测、手动执行检测等等操作
  • OnPush策略下,ngDoCheck钩子函数依然会执行,可在ngDoCheck调用ChangeDetectorRef

以下是一些具体的例子或叙述说明:

angular应用是由组件组成的树状结构。每当异步事件发生后,angular都会从上而下地检测当前"存活"的每个组件

@Component({
  selector: 'parent',
  template: ``
})

export class ParentComponent implements OnInit {
  person = { name: '张三' };

  ngOnInit(): void {
    setTimeout( () => {
      this.person.name = '李四';
    }, 2000);
  }
}
@Component({
  selector: 'child',
  template: `{{user | json}}`
})
export class ChildComponent {
  @Input() user: any;
}

可以看到2秒后child组件的视图更新

当某个组件设置 ChangeDetectionStrategy.OnPush 后,无关的异步事件发生时,这个组件及其子组件不再执行变更检测。只有其输入属性改变,或其本身内部发生异步事件才检测。

输入属性改变,指的是 oldValue !== newValue,所以以下情况就不会触发变更检测。

@Component({
  selector: 'parent',
  template: ``
})

export class ParentComponent implements OnInit {
  person = { name: '张三' };

  ngOnInit(): void {
    setTimeout( () => {
      this.person.name = '李四';
    }, 2000);
  }
}
@Component({
  selector: 'child',
  template: `{{ user | json }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() user: any;
}

可以看到两秒后child组件的视图并不更新

child组件的输入属性被判定为“没有”发生变化,所以其变更检测不会执行。为避免这种情况,需使用 Immutable (不可变)变量。Immutable 变量的不是说这个变量无法修改,是指我们遵守不修改原有的数据模型,而是创建一个新的数据模型的原则。

@Component({
  selector: 'parent',
  template: ``
})

export class ParentComponent implements OnInit {
  person = { name: '张三' };

  ngOnInit(): void {
    setTimeout( () => {
      this.person = { '李四' };
    }, 2000);
  }
}
@Component({
  selector: 'child',
  template: `{{ user | json }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() user: any;
}

可以看到两秒后视图更新

除了Immutable 方式外,还可以使用 Observable作为输入属性

export class CounterComponent implements OnInit {
    counter: number = 0;

    @Input() addStream: Observable;

    constructor(private cdRef: ChangeDetectorRef) { }

    ngOnInit() {
        this.addStream.subscribe(() => {
            this.counter++;
            this.cdRef.markForCheck();
        });
    }
}

组件自己内部发生异步事件,依然可以触发变更检测

@Component({
  selector: 'child',
  template: `{{ user | json }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class ChildComponent implements OnInit {
  @Input() user: any;

  ngOnInit(): void {
     setTimeout( () => {
       this.user.name = '李四';
    }, 2000);
  }
}

参考资料
翻阅源码后,我终于理解了Zone.js

你可能感兴趣的