8
原文链接: Do you really know what unidirectional data flow means in Angular
关于单向数据流,还可以参考这篇文章,且文中还有 youtube 视频解析:Angular - What is Unidirectional Data Flow? Learn How the Angular Development Mode Works, why it's important to use it and how to Troubleshoot it 。单向数据流一句话解释就是:不要在 Angular 使用 Model 生成 View 这个过程中再去修改 Model。

One Way

大多数架构模式是很难理解的,尤其是在相关资料很少时那就更加头疼,比如 Angular 的单向数据流(unidirectional data flow)文档资料就很少,即使官方文档上,也仅仅在 表达式指南模板表达式 两小块中略有提及。我也很少在网上搜到比较好的解释文章,所以写此文是为了给 单向数据流(unidirectional data flow) 有个详细的解释。

双向数据流和单向数据流

在讨论 AngularJS 和 Angular 性能异同点时,就经常会提到单向数据流模式,它也是 Angular 比 AngularJS 性能更快的原因所在。让我们一起看看单向数据流究竟咋回事吧!(译者注:AngularJS 指的是 1.X 版本,2+ 版本叫 Angular,使用 TypeScript 语言重写了框架,包括架构模式进行了重新设计,但两者形似也神似。)

AngularJS 和 Angular 都是通过绑定来实现组件间数据通信,比如在 AngularJS 定义一个父组件 A

app.component('aComponent', {
  controller: class ParentComponent() {
    this.value = {name: 'initial'};
  },
  template: `
    <b-component obj="$ctrl.value"></b-component>
  `
});
----------------
app.component('bComponent', {
    bindings: {
        obj: '='
    },

可以看到父组件 A 有个子组件 B,并且通过输入绑定把父组件 Avalue 属性值传给子组件 Bobj 属性:

<b-component obj="$ctrl.value"></b-component>

这和 Angular 中组件间通信很类似了啊:

@Component({
    template: `
        <b-component [obj]="value"></b-component>
    ...
export class AppComponent {
    value = {name: 'initial'};
}
----------------
export class BComponent {
    @Input() obj;

第一件重要的事情是,Angular 和 AngularJS 都是在变更检测(change detection)期间才去更新绑定值。所以,当 Angular 框架在为父组件 A 运行变更检测时,它会更新子组件 Bobj 属性:

bComponentInstance.obj = aComponentInstance.value;

上面过程证明了单向数据流是数据从上往下流(译者注:即数据从组件树的父组件往子组件流),但是 AngularJS 却不一样,它可以允许在子组件中更新父组件的 value 属性值

app.component('parentComponent', {
  controller: function ParentComponent($timeout) {    
    $timeout(()=>{
      console.log(this.value); // logs {name: 'updated'}
    }, 3000)
  }
----------------
  
app.component('childComponent', {
    controller: function ChildComponent($timeout) {      
      $timeout(()=>{
        this.obj = { name: 'updated' };  
      }, 2000)

上面代码你可以看到两个 timeout 回调,第一个回调会更新子组件属性,第二个回调会延迟第一个回调一秒,检查父组件属性是否被更新。如果你在 AngularJS 中运行上面代码你会发现父组件属性已经被更新了。让我们一起看看究竟发生了什么?

当第一个回调运行时,子组件 Bobj 属性被更新为 {name: 'updated'},然后 AngularJS 运行变更检测。在变更检测过程中,AngularJS 检测到子组件绑定属性的值发生改变,它会更新父组件 Avalue 属性值。这是 AngularJS 变更检测的内置功能。如果你在 Angular 中做同样的事情,它仅仅更新子组件的属性值,但是子组件的改变不会冒泡到父组件,即 Angular 不会改变父组件的属性值。这是升级版后的 Angular 变更检测实现,相较于 AngularJS 的最重大区别。然而,它却困扰了我好久啊。

然而,Angular 也同样可以通过子组件来更新父组件,这个机制就是输出绑定(output binding)。可以像这样使用输出绑定:

@Component({
    template: `
        <h1>Hello {{value.name}}</h1>
        <a-comp (updateObj)="value = $event"></a-comp>
    ...
export class AppComponent {
    value = {name: 'initial'};
    
    constructor() {
        setTimeout(() => {
            console.log(this.value); // logs {name: 'updated'}
        }, 3000);
----------------
@Component({...})
export class AComponent {
    @Output() updateObj = new EventEmitter();
    
    constructor() {
        setTimeout(() => {
            this.updateObj.emit({name: 'updated'});
        }, 2000);

我承认这个和从子组件直接更新还不太一样,但是父组件值的确是更新了啊。很长一段时间我不明白为何这没有被看做双向数据绑定?毕竟,数据通信是在两个方向进行的。

直到有一晚我读到了 Two Phases of Angular Applications by Victor Savkin,他解释道(译者注:为清晰理解,这个解释不翻译):

Angular 2 separates updating the application model and reflecting the state of the model in the view into two distinct phases. The developer is responsible for updating the application model. Angular, by means of change detection, is responsible for reflecting the state of the model in the view.
(译者注:意思就是 Angular 划分了更新程序 model在前同步 model 和 view 在后两个步骤,开发者只需要关注更新程序 model同步 model 和 view 由 Angular 框架负责,这个同步过程就是变更检测,说白了就是更新 view。)
(译者注:比如朋友圈点赞,首先就是手指触发异步事件来更新程序 model,如在 Post 组件里给 likes 属性加 1,然后在下一个 VM turn 时 Angular 会自动更新视图中绑定的 likes 值, 即同步 model 和 view,点赞数就会从 0 变为 1。)

这让我花费好些天才明白,所谓的使用输出绑定机制来更新父组件并不是在变更检测中执行的:

<a-comp (updateObj)="value = $event"></a-comp>

相反,它是在变更检测前的更新程序 model 阶段执行的。所以,单向数据流的意思是指在变更检测期间属性绑定变更的架构。在上面 Angular 代码示例中,不像 AngularJS,在 Angular 变更检测期间,并没有代码去让子组件 AComponent 去更新父组件 AppComponent 的属性。相反,输出绑定过程并没有在变更检测期间内运行,所以它没有把单向数据流转变为双向数据流。

另外,尽管 Angular 没有内置机制可以使得在变更检测期间去更新父组件,然而可以通过共享服务或广播同步事件做到这一点。但是,由于 Angular 框架强迫单向数据流,所以这么做会导致ExpressionChangedAfterItHasBeenCheckedError 错误。想要了解导致这个错误的原因和解决方案可以参考 Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError

视图和服务层的单向数据流

你可能知道,大多数 web 程序会设计成有两层:视图层和服务层。

Layer

Web 环境下视图层主要通过 DOM 结构向用户展示相关程序数据,在 Angular 中这一层是通过组件做的。服务层主要是处理和保存数据,正如上图中展示的,这一层又被切分为状态管理和一些基础部分,如 rest 服务或可重用工具服务(helpers)。

上文中提到的单向数据流指的是视图层,因为它说的是组件,而组件是用来构建视图的:

State Management

然而,在随着实现了 redux 架构的 ngrx 引入后,这又让人迷惑了,因为 redux 文档上有 这么一句(译者注:为清晰理解,这句不翻译):

Redux architecture revolves around a strict unidirectional data flow.
This means that all data in an application follows the same lifecycle pattern, making the logic of your app more predictable and easier to understand…

实际上,这个严格单项数据流(strict unidirectional data flow)其实说的是服务层而不是视图层。但我有时会搞混服务层和视图层,会把 redux 的架构模式与 Angular 的结构模式联系起来,当然要避免搞混这两层嗷。Redux 说的单向数据流说的是服务层,而不是视图层嗷。 Redux 主要说的是状态管理模块,会把我们上文说的双向数据流转变为单向数据流。
把双向数据流

Two-way

转换为单向数据流:

One-way


lx1036
3.1k 声望923 粉丝

为五斗米折腰