双向数据绑定

· 3 min read

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

抛砖引玉。

[toc]

使用

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

<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属性。

// 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);
  }

// 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);
  }
// 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)

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


// 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;
    }
  }

// 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 双向绑定实现原理
      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双向绑定实现原理
         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);
   ```

## 参考文档
- [Angular 2 Change Detection - 2](https://segmentfault.com/a/1190000008754052)
- [angular脏检查原理及伪代码实现](https://juejin.im/post/5b193353f265da6e0d7a2dcd)