双向数据绑定

早在AngularJS时代(即Angular1)即有双向绑定这个功能,当然这个概念不见得NG是最早的(ActionScript也有),但NG的流行助其推广开来,这是毋庸置疑的,今天的Vue也有双向绑定,React本身并没有提供支持,但也可以自己实现做到。那,双向绑定是如何做到的,利弊又是什么呢。
以NG为例,这里就深度了解下。

抛砖引玉。

[toc]

使用

先看下双向绑定的使用,如下组件中声明username,使用[(ngModel)]进行双向绑定后,则M与V可以实时同步,这就是双向绑定。

1
2
3
4
5
6
7
8
9
<p>
form works!
<input [(ngModel)]="username"/>
</p>
<h3>
I am {{
username
}}
</h3>

这样的好处就是视图层与表单对象保持一致,你直接获得了表单对象的数据值,并且类型问题也都提前处理好了,不需要自己再操作。

实现

用起来方便,接下来看看NG是如何实现的吧。

[(ngModel)] 是语法糖

要知道[(ngModel)] 是语法糖,拆开可以看作是 <input [ngModel]="username" (ngModelChange)="username=$event"/>, ngModel是个指令,ngModelChange是指令中暴露出来的一个方法,所以聚焦ngModel指令的实现逻辑。

data => view 使用[]
view => data 使用()

[ngModel]

当我们在数据中修改了name的值,视图上的值就会实时变化

两个疑问

  1. NG如何知道对象变化?脏检查
  2. NG如何更新值到最终的原生元素身上呢?修改元素value属性

脏检查Dirty checking

脏检查全称是脏数据检查,即产生了变化的数据。

NG中异步操作,比如Click,比如XHR,自动触发change detection,发现这些变化,然后执行更新动作。

但是这些变化,我理解也不是只要你变了就立即去做更新,而是model稳定后,批量更新

注意,NG不是定时轮询去检查

更新视图

通过指令找到nativeElement,修改value属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// packages/forms/src/directives/ng_model.ts

constructor(
@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super();
this._parent = parent;
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
1
2
3
4
5
6
7

// packages/forms/src/directives/default_value_accessor.ts

writeValue(value: any): void {
const normalizedValue = value == null ? '' : value;
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
1
2
3
4
5
6
// packages/platform-browser/src/dom/dom_renderer.ts

setProperty(el: any, name: string, value: any): void {
NG_DEV_MODE && checkNoSyntheticProp(name, 'property');
el[name] = value;
}

(ngModelChange)

监测到表单输入值的变化,即更新到数据对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// packages/forms/src/directives/ng_model.ts

ngOnChanges(changes: SimpleChanges) {
this._checkForErrors();
if (!this._registered) this._setUpControl();
if ('isDisabled' in changes) {
this._updateDisabled(changes);
}

if (isPropertyUpdated(changes, this.viewModel)) {
this._updateValue(this.model);
this.viewModel = this.model;
}
}
1
2
3
4
5
6
7
8
9
10
11
12

// packages/elements/src/component-factory-strategy.ts

/** Runs change detection on the component. */
protected detectChanges(): void {
if (this.componentRef === null) {
return;
}

this.callNgOnChanges(this.componentRef);
this.componentRef.changeDetectorRef.detectChanges();
}

关键点如上,原理大致明白了,那抛开框架,如何手写个双向绑定呢。

其它手段

Object.defineProperty

  • IE9支持
  • Vue v2 双向绑定实现原理
1
2
3
4
5
6
7
8
9
10
const obj = {};
Object.defineProperty(obj, 'value', {
get: function () {
console.log('获取value值', value);
return val;
},
set: function (value) {
console.log('设置value值', value);
},
});

Proxy

  • IE不支持

  • Proxy为ES6特性

  • Vue v3双向绑定实现原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     const obj = {};
    const handler = {
    get: function (target, prop) {
    console.log('获取value值', target[prop]);
    return target[prop];
    },
    set: function (target, prop, value) {
    console.log('设置value值', value);
    target[prop] = value;
    },
    };

    const proxy = new Proxy(obj, handler);

参考文档