双向绑定底层剖析(用于自定义组件双向数据绑定和非form表单元素)

适用于一切双向数据绑定,不局限与下面的input输入,文末链接1就是例子

Angular 中常见的 ControlValueAccessor 有:

  • DefaultValueAccessor - 用于 text 和 textarea 类型的输入控件

  • SelectControlValueAccessor - 用于 select 选择控件

  • CheckboxControlValueAccessor - 用于 checkbox 复选控件

(注:妹的快写完的时候突然网页挂了,没保存到浪费了半小时重写)

一、问题的发现

公司最近的项目都是通过PrimeNG(ng2的UI组件)来开发,但别人的组件永远都够不着用,所以很有必要进行二次开发,或者自定义组件。今天看到项目中的大神把PrimeNG<p-autoComplete>的组件,自己自定义写了一份,趁有空我也研究了一下,发现里面存在着双向绑定的深层原理及使用方式。(我一直以为双向绑定原理就是[value]="value" (valueChange)="value=$event"  《揭秘Angular2》P202介绍)

import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';

这是ng2内置接口ControlValueAccessor

二、什么是ControlValueAccessor?

ControlValueAccessor 是一个接口,它的作用是:

  • 把 form 模型中值映射到视图中

  • 当视图发生变化时,通知 form directives 或 form controls

Angular 引入这个接口的原因是,不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的 value 值,而对于复选框 (checkbox) 我们是设置它的 checked 属性。实际上,不同类型的输入控件都有一个 ControlValueAccessor,用来更新视图。

这就是MVVM模型,Model -> View,View -> Model 之间的数据绑定

1、ControlValueAccessor接口底层代码

// angular2/packages/forms/src/directives/control_value_accessor.ts 
export interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}
  • writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中。
  • registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数
  • registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数
  • setDisabledState?(isDisabled: boolean):当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素。

三、怎么使用达到双向绑定

import {Component, OnInit, Input, forwardRef} from '@angular/core';
import {API} from "app/share/lib/api/api";
import {NG_VALUE_ACCESSOR} from "@angular/forms";

// 封装一个对象,固定写法
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => GoodSelectComponent),
    multi: true
};

@Component({
    selector: 'good-select',
    templateUrl: './good-select.component.html',
    styleUrls: ['./good-select.component.css'],
    providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class GoodSelectComponent implements OnInit {

    /**
     * 数据
     */
    suggestions: any[];

    @Input()
    defaultLabel: string = "请选择…";
    @Input()
    multiSelect: boolean = false;
    @Input()
    width: string = "";
    @Input()
    height: string = "";
    @Input()
    valueField: string="";
    @Input()
    styleClass: string;

    constructor(public api: API) {
    }

    ngOnInit() {
        this.suggestions = [];
    }


    public onTouchedCallback: () => () => {};
    public onChangeCallback: (_: any) => () => {};
    public innerValue;

    // 获取属性
    get value(): any {
        return this.innerValue;
    };

    // 设置属性,并触发监听器
    set value(v: any) {
        let vv = v && v.name?v.name:v;
        this.innerValue = vv;
        console.info(v)
        if(this.valueField){
            this.onChangeCallback(v[this.valueField]);
        }else{
            this.onChangeCallback(vv);
        }
    }

    // 写入值
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }

    // 注册变化处理事件
    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    // 注册触摸事件
    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

    /**
     * 查询数据
     * @param $event
     */
    queryData($event: any) {
        let value = $event.query;

        let pageParms = {"first": 0, "rows": 9999};
        this.api.call("abnormalOtherHandleController.waybillGoodsQuery", pageParms, {
            name: value
        }).ok(json => {
            let result: any = json.result && json.result.content || [];
            this.suggestions = result || [];
        }).fail(err => {
            throw new Error(err);
        });
    }

}

1、剖析

上面没看到接口ControlValueAccessor,其实NG_VALUE_ACCESSOR就是其别名。固定格式如下

{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => GoodSelectComponent),
    multi: true
}

不知道说别名规范不规范,在别人的例子中都会继承ControlValueAccessor(implements ControlValueAccessor),但本文例子中就没有这种写法也可以实现双向数据绑定

这里面有两个比较重要的知识点

知识点1

1、当组件继承了ControlValueAccessor(implements ControlValueAccessor),那么writeValue、registerOnChange、registerOnTouched,在自定义组件过程中三者缺一不可,就相当于组件implements OnInit 其组件必须包含ngOnInit否则会报错

2、registerOnChange、registerOnTouched都是传入一个函数作为参数,所以上面都各自定义了一个空函数

public onTouchedCallback: () => () => {};
public onChangeCallback: (_: any) => () => {};

这里展开registerOnChange来讲,这个函数是用来监听视图层值(就是[(ngModle)]的值)的变化,当视图层值变化时会调用这个方法,实现视图层传值到模型层,writeValue相反。

知识点2

1、setter、getter的使用,老大说仅适合[(ngModle)]传进来的场景,但在我用了一个月来看,这种拦截适合父传子传参的一切输入属性,当视图层接收父组件传进来的[parentvalue]值([parentvalue]="value"),在子组件通过@Input() set parentvalue(value: any){ this._value = value},(注意:这里的parentvalue要和传进来的同名) get parentvalue() {return this._value} ,(注意:this._value会隐式替换[parentvalue]="value"这里的value值)

2.误区

这个案例也有误导的地方就是set/get和writeValue的混用,前面说了writeValue()是M->V的过程,但这里set也是过滤拦截视图层的值的操作,那么究竟以哪个为准。

经过多次断点测试 writeValue()这个方法好像只会在组件ngAfterViewInit之前调用,当过了这个生命周期就不会再进入该方法。初始化的时候,将会使用表单模型中对应的初始值作为参数,调用 writeValue() 方法,但往往不会都有初始值的情况,所以才会出现

writeValue(value: any) {
     if (value !== this.innerValue) {  // !== undefined null ''的情况出现
         this.innerValue = value;
     }
}

那么显然主导ngModel双向数据绑定的是set/get拦截器,阿里的NG-ZORRO的组件大量用到set/get拦截器也是这个原理

 

推荐2个网址

 http://www.jianshu.com/p/a01015d5d83b  双向绑定底层原理简单案例

https://segmentfault.com/a/1190000009126012   双向绑定的过程

转载于:https://my.oschina.net/u/2949632/blog/917413

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值