翻譯:深刻理解Angular 1.5 中的生命週期鉤子

文章翻譯自:https://toddmotto.com/angular-1-5-lifecycle-hooks
討論能夠訪問issue:https://github.com/Jocs/jocs.github.io/issues/3javascript

生命週期鉤子是一些簡單的函數,這些函數會在Angular應用組件特定生命週期被調用。生命週期鉤子在Angular 1.5版本被引入,一般與.component()方法一塊兒使用,並在接下來的幾個版本中演變,幷包含了更多有用的鉤子函數(受Angular 2的啓發)。讓咱們深刻研究這些鉤子函數並實際使用它們吧。這些鉤子函數所帶來的做用以及爲何咱們須要使用它們,對於咱們深刻理解經過組件架構的應用具備重要的意義。html

在Angular v1.3.0+版本,我本身實現了.component() 方法,該方法深入得洞悉了怎麼去使用這些生命週期函數以及這些函數在組件中的做用,讓咱們開始研究它吧。java

$onInit

什麼是$onInit ?首先,他是Angular組件(譯註:經過.component() 方法定義的組件)控制器中暴露出來的一個屬性,咱們能夠把一個函數賦值給該屬性:git

var myComponent = {
  bindings: {},
  controller: function () {
    this.$onInit = function() {

    };
  }
};

angular
  .module('app')
  .component('myComponent', myComponent);
Using $onInit

$onInit 生命週期鉤子用做控制器的初始化工做,下面舉個經常使用例子:angularjs

var myComponent = {
  ...
  controller: function () {
    this.foo = 'bar';
    this.bar = 'foo';
    this.fooBar = function () {

    };
  }
};

注意上面的代碼,咱們把全部的屬性直接賦值到了this上面,它們就像「浮在」控制器的各個角落。如今,讓咱們經過$onInit 來重寫上面代碼:github

var myComponent = {
  ...
  controller: function () {
    this.$onInit = function () {
      this.foo = 'bar';
      this.bar = 'foo';
    };
    this.fooBar = function () {
      console.log(this.foo); // 'bar'
    };
  }
};

上面的數據明顯地經過硬編碼寫入的,可是在實際的應用中,咱們一般是經過bindings: {} 對象來把咱們須要的數據傳遞到組件中,咱們使用$onInit 來進行一些初始化工做,這樣就把之前那些「浮在」控制器各處的初始化變量都集中起來了,$onInit 就像是控制器中的constructor ,包含了一些初始化信息。json

對於this.fooBar函數呢?不要着急,該函數放在$onInit外面是徹底可以訪問到初始化數據的,好比當你調用this.fooBar的時候,函數會打印出this.foo的值,也就是在$onInit函數中定義的'bar'。所以全部你初始化的數據都正確地綁定到了控制器的this 上下文中。數組

$onInit + 「require」

由於這些生命週期鉤子定義得如此優雅(不一樣的生命週期鉤子都在組件的不一樣生命週期被調用),一個組件也能夠從另外的組件中繼承方法,甚至繼承的方法在$onInit 鉤子中就能夠直接使用。架構

首先咱們須要思考的是如何使用require,我寫過另一篇深刻介紹$onInit 和 require的文章,可是在此我依然會簡要介紹一些require的基本用法,隨後將提供一個完整的實例。app

讓咱們來看看myComponent的例子,在這兒require後面緊跟的是一個對象(只在.component()方法中require字段後面接對象),當require.directive()結合使用的時候,require字段後面也能夠跟數組或者字符串語法形式。

var myComponent = {
  ...
  require: {
    parent: '^^anotherComponent'
  },
  controller: function () {
    this.$onInit = function () {
      this.foo = 'bar';
      this.bar = 'foo';
    };
    this.fooBar = function () {
      console.log(this.foo); // 'bar'
    };
  }
};

如上面的例子,require被設置爲^^anotherComponentrequire值前面^^表示自會在當前組件的父組件中搜尋anotherComponent控制器,(若是require值前面是^那麼首先會在當前組件搜尋是否有該控制器,若是沒有再在其父組件中搜尋)這樣咱們就能夠在$onInit中使用任何當定在父組件中的方法了。

var myComponent = {
  ...
  require: {
    parent: '^^anotherComponent'
  },
  controller: function () {
    this.$onInit = function () {
      this.foo = 'bar';
      this.bar = 'foo';
      this.parent.sayHi();
    };
    this.fooBar = function () {
      console.log(this.foo); // 'bar'
    };
  }
};

注意,在Angular 1.5.6版本(見 CHANGELOG)中,若是require對象中屬性名和require的控制器同名,那麼就能夠省略控制器名。這一特性並無帶來給功能帶來很大的改變,咱們能夠以下使用它:

var myComponent = {
  ...
  require: {
    parent: '^^'
  },
  controller: function () {
    ...
  }
};

正如你所見,咱們徹底省略了須要requre的控制器名而直接使用^^替代。完整寫法^^parent就被省略爲^^。須要謹記,在前面的一個例子中,咱們只能使用parent: '^^anotherComponent'來表示咱們須要使用另一個組件中控制器中的方法(譯者注:做者以上就是控制器和requre的屬性名不相同時,不可以省略),最後,咱們只需記住一點,若是咱們想使用該條特性,那麼被requre的控制器名必須和require的屬性名同名。

Real world $onInit + require

讓咱們使用$onInitrequire來實現一個tabs組件,首先咱們實現的組件大概如以下使用:

<tabs>
  <tab label="Tab 1">
    Tab 1 contents!
   </tab>
   <tab label="Tab 2">
    Tab 2 contents!
   </tab>
   <tab label="Tab 3">
    Tab 3 contents!
   </tab>
</tabs>

這意味着咱們須要兩個組件,tabtabs。咱們將transclude全部的tabs子元素(就是全部tab模板中的tabs元素)而後經過bindings綁定的對象來獲取label值。

首先,組件定義了每一個組件都必須使用的一些屬性:

var tab = {
  bindings: {},
  require: {},
  transclude: true,
  template: ``,
  controller: function () {}
};

var tabs = {
  transclude: true,
  template: ``,
  controller: function () {}
};

angular
  .module('app', [])
  .component('tab', tab)
  .component('tabs', tabs);

tab組件須要經過bindings綁定一些數據,同時在該組件中,咱們使用了require,transclude和一個template ,最後是一個控制器controller

tabs組件首先會transclude全部的元素到模板中,而後經過controller來對tabs進行管理。

讓咱們來實現tab組件的模板吧:

var tab = {
  ...
  template: `
    <div class="tabs__content" ng-if="$ctrl.tab.selected">
      <div ng-transclude></div>
    </div>
  `,
  ...
};

對於tab組件而言,咱們只在$ctrl.tab.selectedtrue的時候顯示該組件,所以咱們須要一些在控制器中添加一些邏輯來處理該需求。隨後咱們經過transclude來對tab組件中的內容填充。(這些內容就是展現在不一樣tab內的)

var tabs = {
  ...
  template: `
    <div class="tabs">
      <ul class="tabs__list">
        <li ng-repeat="tab in $ctrl.tabs">
          <a href=""
            ng-bind="tab.label"
            ng-click="$ctrl.selectTab($index);"></a>
        </li>
      </ul>
      <div class="tabs__content" ng-transclude></div>
    </div>
  `,
  ...
};

對於tabs組件,咱們建立一個數組來展現$ctrl.tabs內容,並對每個tab選項卡綁定click事件處理函數$ctrl.selectTab(),在調用該方法是傳入當前$index。同時咱們transclude全部的子節點(全部的<tab>元素)到.tabs_content容器中。

接下來讓咱們來處理tab組件的控制器,咱們將建立一個this.tab屬性,固然初始化該屬性應該放在$onInit鉤子函數中:

var tab = {
  bindings: {
    label: '@'
  },
  ...
  template: `
    <div class="tabs__content" ng-if="$ctrl.tab.selected">
      <div ng-transclude></div>
    </div>
  `,
  controller: function () {
    this.$onInit = function () {
      this.tab = {
        label: this.label,
        selected: false
      };
    };
  }
  ...
};

你能夠看到我在控制器中使用了this.label,由於咱們在組件中添加了bindings: {label: '@'},這樣咱們就可使用this.label來獲取綁定到<tab>組件label屬性上面的值了(字符串形式)。經過這樣的綁定形式咱們就能夠把不一樣的值映射到不一樣的tab組件上。

接下來讓咱們來看看tabs組件控制器中的邏輯,這可能稍微有點複雜:

var tabs = {
  ...
  template: `
    <div class="tabs">
      <ul class="tabs__list">
        <li ng-repeat="tab in $ctrl.tabs">
          <a href=""
            ng-bind="tab.label"
            ng-click="$ctrl.selectTab($index);"></a>
        </li>
      </ul>
      <div class="tabs__content" ng-transclude></div>
    </div>
  `,
  controller: function () {
    this.$onInit = function () {
      this.tabs = [];
    };
    this.addTab = function addTab(tab) {
      this.tabs.push(tab);
    };
    this.selectTab = function selectTab(index) {
      for (var i = 0; i < this.tabs.length; i++) {
        this.tabs[i].selected = false;
      }
      this.tabs[index].selected = true;
    };
  },
  ...
};

咱們在$onInit鉤子處理函數中初始化this.tabs = [],咱們已經知道$onInit用來初始化屬性值,接下來咱們定義了兩個函數,addTabselectTabaddTab函數咱們會經過require傳遞到每個子組件中,經過這種形式來告訴父組件子組件的存在,同時保存一份對每一個tab的引用,這樣咱們就能夠經過ng-repeat來遍歷全部的tab選項卡,而且能夠點擊(經過selectTab)選擇不一樣的選項卡。

接下來咱們經過tab組件的require來將addTab方法委派到tab組件中使用。

var tab = {
  ...
  require: {
    tabs: '^^'
  },
  ...
};

正如咱們在文章關於$onInitrequire部分提到,咱們經過^^來只requre父組件控制器中的邏輯而不在自身組件中尋找這些方法。除此以外,當咱們require的控制器名和requre對象中的屬性名相同時咱們還能夠省略requre的控制器名字,這是版本1.5.6新增長的一個特性。關於這一新特性準備好了嗎?在下面代碼中,咱們使用tabs: '^^',咱們有一個和require控制器同名的屬性名{tabs: ...},這樣咱們就能夠在$onInit中使用this.tabs來調用父組件控制器中的方法了。

var tab = {
  ...
  require: {
    tabs: '^^'
  },
  controller: function () {
    this.$onInit = function () {
      this.tab = {
        label: this.label,
        selected: false
      };
      // this.tabs === require: { tabs: '^^' }
      this.tabs.addTab(this.tab);
    };
  }
  ...
};

把全部代碼放一塊兒:

var tab = {
  bindings: {
    label: '@'
  },
  require: {
    tabs: '^^'
  },
  transclude: true,
  template: `
    <div class="tabs__content" ng-if="$ctrl.tab.selected">
      <div ng-transclude></div>
    </div>
  `,
  controller: function () {
    this.$onInit = function () {
      this.tab = {
        label: this.label,
        selected: false
      };
      this.tabs.addTab(this.tab);
    };
  }
};

var tabs = {
  transclude: true,
  controller: function () {
    this.$onInit = function () {
      this.tabs = [];
    };
    this.addTab = function addTab(tab) {
      this.tabs.push(tab);
    };
    this.selectTab = function selectTab(index) {
      for (var i = 0; i < this.tabs.length; i++) {
        this.tabs[i].selected = false;
      }
      this.tabs[index].selected = true;
    };
  },
  template: `
    <div class="tabs">
      <ul class="tabs__list">
        <li ng-repeat="tab in $ctrl.tabs">
          <a href=""
            ng-bind="tab.label"
            ng-click="$ctrl.selectTab($index);"></a>
        </li>
      </ul>
      <div class="tabs__content" ng-transclude></div>
    </div>
  `
};

點擊選項卡相應內容就會呈現出來,當時,咱們並無設置一個初始化的展現的選項卡?這就是接下來$postLink要介紹的內容。

$postLink

咱們已經知道,compile函數會返回一個prepost‘連接函數’,如以下形式:

function myDirective() {
  restrict: 'E',
  scope: { foo: '=' },
  compile: function compile($element, $attrs) {
    return {
      pre: function preLink($scope, $element, $attrs) {
        // access to child elements that are NOT linked
      },
      post: function postLink($scope, $element, $attrs) {
        // access to child elements that are linked
      }
    };
  }
}

你也可能知道以下:

function myDirective() {
  restrict: 'E',
  scope: { foo: '=' },
  link: function postLink($scope, $element, $attrs) {
    // access to child elements that are linked
  }
}

當咱們只須要使用postLink函數的時候,上面兩種形式效果是同樣的。注意咱們使用的post: function)() {...} - 這就是咱們的主角。我已經在上面的代碼中添加了一行註釋「能夠獲取到已經連接的子元素」,上面的註釋意味着在父指令的post 函數中,子元素的模板已經被編譯而且已經被連接到特定的scope上。而經過compilepre函數咱們是沒法獲取到已經編譯、連接後的子元素的。所以咱們有一個生命週期鉤子來幫我咱們在編譯的最後階段(子元素已經被編譯和連接)來處理一些相應邏輯。

Using $postLink

$postLink給予了咱們處理如上需求的可能,咱們不需使用一些hack的範式就能夠像以下形式同樣使用$postLink鉤子函數。

var myComponent = {
  ...
  controller: function () {
    this.$postLink = function () {
      // fire away...
    };
  }
};

咱們已經知道,$postLink是在全部的子元素被連接後觸發,接下來讓咱們來實現咱們的tabs組件。

Real world $postLink

咱們能夠經過$postLink函數來給咱們的選項卡組件一個初始的選項卡。首先咱們須要調整一下模板:

<tabs selected="0">
  <tab label="Tab 1">...</tab>
  <tab label="Tab 2">...</tab>
  <tab label="Tab 3">...</tab>
</tabs>

如今咱們就能夠經過bindings獲取到selected特性的值,而後用以初始化:

var tabs = {
  bindings: {
    selected: '@'
  },
  ...
  controller: function () {
    this.$onInit = function () {
      this.tabs = [];
    };
    this.addTab = function addTab(tab) {
      this.tabs.push(tab);
    };
    this.selectTab = function selectTab(index) {
      for (var i = 0; i < this.tabs.length; i++) {
        this.tabs[i].selected = false;
      }
      this.tabs[index].selected = true;
    };
    this.$postLink = function () {
      // use `this.selected` passed down from bindings: {}
      // a safer option would be to parseInt(this.selected, 10)
      // to coerce to a Number to lookup the Array index, however
      // this works just fine for the demo :)
      this.selectTab(this.selected || 0);
    };
  },
  ...
};

如今咱們已經有一個生動的實例,經過selected屬性來預先選擇某一模板,在上面的例子中咱們使用selected=2來預先選擇第三個選項卡做爲初始值。

What $postLink is not

$postLink函數中並非一個好的地方用以處理DOM操做。在Angular生態圈外經過原生的事件綁定來爲HTML/template擴展行爲,Directive依然是最佳選擇。不要僅僅將Directive(沒有模板的指令)重寫爲component組件,這些都是不推薦的作法。

那麼$psotLint存在的意義何在?你可能想在$postLink函數中進行DOM操做或者自定義的事件。其實,DOM操做和綁定事件最好使用一個帶模板的指令來進行封裝。正確地使用$postLink,你能夠把你的疑問寫在下面的評論中,我會很樂意的回覆你的疑問。

$onChanges

這是一個很大的部分(也是最重要的部分),$onChanges將和Angular 1.5.x中的組件架構及單向數據流一塊兒討論。一條金玉良言:$onChanges在自身組件被改變可是卻在父組件中發生的改變(譯者注:其實做者這兒說得比較含糊,$onChange就是在單向數據綁定後,父組件向子組件傳遞的數據發生改變後會被調用)。當父組件中的一些屬性發生改變後,經過bindings: {}就能夠把這種變化傳遞到子組件中,這就是$onChanges的祕密所在。

What calls $onChanges?

在如下狀況下$onChanges會被調用,首先,在組件初始化的時候,組件初始化時會傳遞最初的changes對象,這樣咱們就能夠直接獲取到咱們所需的數據了。第二種會被調用的場景就是隻當單向數據綁定<he @(用於獲取DOM特性值,這些值是經過父組件傳遞的)改變時會被調用。一旦$onChanges被調用,你將在$onChanges的參數中獲取到一個變化對象,咱們將在接下來的部分中詳細討論。

Using $onChanges

使用$onChanges至關簡單,可是該生命週期鉤子又一般被錯誤的使用或談論,所以咱們將在接下來的部分討論$onChanges的使用,首先,咱們聲明瞭一個childConpoment組件。

var childComponent = {
  bindings: { user: '<' },
  controller: function () {
    this.$onChanges = function (changes) {
      // `changes` is a special instance of a constructor Object,
      // it contains a hash of a change Object and
      // also contains a function called `isFirstChange()`
      // it's implemented in the source code using a constructor Object
      // and prototype method to create the function `isFirstChange()`
    };
  }
};

angular
  .module('app')
  .component('childComponent', childComponent);

注意,這兒bindings對象包含了一個值爲'<'user字段,該‘<’表示了單向數據流,這一點在我之前的 文章已經提到過,單向數據流會致使$onChanges鉤子被調用。

可是,正如上面提到,咱們須要一個parentComponent組件來完成個人實例:

var parentComponent = {
  template: `
    <div>
      <child-component></child-component>
    </div>
  `
};

angular
  .module('app')
  .component('parentComponent', parentComponent);

須要注意的是:<child-compoent></component>組件在<parent-component></parent-component>組件中渲染,這就是爲何咱們可以初始化一個帶有數據的控制器,而且把這些數據傳遞給childComponent:

var parentComponent = {
  template: `
    <div>
      <a href="" ng-click="$ctrl.changeUser();">
        Change user (this will call $onChanges in child)
      </a>
      <child-component
          user="$ctrl.user">
      </child-component>
    </div>
  `,
  controller: function () {
    this.$onInit = function () {
        this.user = {
          name: 'Todd Motto',
        location: 'England, UK'
      };
    };
    this.changeUser = function () {
        this.user = {
          name: 'Tom Delonge',
        location: 'California, USA'
      };
    };
  }
};

再次,咱們使用$onInit來定義一些初始化數據,把一個對象賦值給this.user。同時咱們有this.changeUser函數,用來更新this.user的值,這個改變發生在父組件,可是會觸發子組件中的$onChange鉤子函數被調用,父組件的改變經過$onChanges來通知子組件,這就是$onChanges的做用。

如今,讓咱們來看看childComponent組件:

var childComponent = {
  bindings: {
    user: '<'
  },
  template: `
    <div>
      <pre>{{ $ctrl.user | json }}</pre>
    </div>
  `,
  controller: function () {
    this.$onChanges = function (changes) {
      this.user = changes;
    };
  }
};

這兒,咱們使用binding: {user: '<'},意味着咱們能夠經過user來接收來自父組件經過單向數據綁定傳遞的數據,咱們在模板中經過this.user來展現數據的變化,(我經過使用| json過濾器來展現整個對象)

點擊按鈕來觀察childCompoent經過$onChanges來傳播的變化:「我並無獲取到變化??」像上面的代碼,我永遠也獲取不到,由於咱們把整個變化對象都賦值給了this.user,讓咱們修改下上面的代碼:

var childComponent = {
  ...
  controller: function () {
    this.$onChanges = function (changes) {
      this.user = changes.user.currentValue;
    };
  }
};

如今咱們可使用user屬性來獲取到從父組件傳遞下來的數據,經過curentValue來引用到該數據,也就是change對象上面的curentChange屬性,嘗試下上面的代碼:

Cloning 「change」 hashes for 「immutable」 bindings

如今咱們已經從組件中獲取到從單向數據綁定的數據,咱們能夠在深刻的思考。雖然單項數據綁定並無被Angular所$watch,可是咱們是經過引用傳遞。這意味着子組件對象(特別注意,簡單數據類型不是傳遞引用)屬性的改變依然會影響到父組件的相同對象,這就和雙向數據綁定的做用同樣了,固然這是無心義的。這就是,咱們能夠經過設計。聰明的經過深拷貝來處理單向數據流傳遞下來的對象,來使得該對象成爲「不可變對象」,也就是說傳遞下來的對象不會在子組件中被更改。

這個是一個fiddle例子(注意user | json)過濾器移到了父組件中(注意,父組件中的對象也隨之更新了)

做爲替換,咱們可使用 angular.cocy()來克隆傳遞下來的對象,這樣就打破了JavaScript對象的「引用傳遞「:

var childComponent = {
  ...
  controller: function () {
    this.$onChanges = function (changes) {
      this.user = angular.copy(changes.user.currentValue);
    };
  }
};

作得更好,咱們添加了if語句來檢測對象的屬性是否存在,這是一個很好的實踐:

var childComponent = {
  ...
  controller: function () {
    this.$onChanges = function (changes) {
      if (changes.user) {
        this.user = angular.copy(changes.user.currentValue);
      }
    };
  }
};

甚至咱們還能夠再優化咱們的代碼,由於當父組件中數據發生變化,該變化會當即反應在this.user上面,隨後咱們經過深拷貝changes.user.currentValue對象,其實這兩個對象是相同的,下面兩種寫法實際上是在作同一件事。

this.$onChanges = function (changes) {
  if (changes.user) {
    this.user = angular.copy(this.user);
    this.user = angular.copy(changes.user.currentValue);
  }
};

我更偏向於的途徑(使用angular.copy(this.user))。

如今就開始嘗試,經過深拷貝開復制從父組件傳遞下來的對象,而後賦值給子組件控制器相應屬性。

感受還不錯吧?如今咱們使用拷貝對象,咱們能夠任意改變對象而不用擔憂會影響到父組件(對不起,雙向數據綁定真的不推薦了!)所以當咱們更新數據後,經過事件來通知父組件,單向數據流並非生命週期鉤子的一部分,可是這$onChanges鉤子被設計出來的意思所在。數據輸入和事件輸出(輸入 = 數據, 輸出 = 事件),讓咱們使用它吧。

One-way dataflow + events

上面咱們討論了bindings$onChanges已經覆蓋了單向數據流,如今咱們將添加事件來擴展這一單向數據流。

爲了使數據可以迴流到 parentComponent,咱們須要委託一個函數做爲事件的回調函數,然咱們添加一個叫updateUser的函數,該函數須要一個event最爲傳遞回來的參數,相信我,這樣作將會頗有意義。

var parentComponent = {
  ...
  controller: function () {
    ...
    this.updateUser = function (event) {
      this.user = event.user;
    };
  }
};

從這咱們能夠看出,咱們期待event是一個對象,而且帶有一個user的屬性,也就是從子組件傳遞回來的值,首先咱們須要把該事件回調函數傳遞到子組件中:

var parentComponent = {
  template: `
    <div>
      ...
      <child-component
        user="$ctrl.user"
        on-update="$ctrl.updateUser($event);">
      </child-component>
    </div>
  `,
  controller: function () {
    ...
    this.updateUser = function (event) {
      this.user = event.user;
    };
  }
};

注意我建立了一個帶有on-*前綴的特性,當咱們須要綁定一個事件(想一想 onclick/onblur)的時候,這是一個最佳實踐。

如今咱們已經將該函數傳遞給了<child-component>,咱們須要經過bindings來獲取這一綁定的函數。

var childComponent = {
  bindings: {
    user: '<',
    onUpdate: '&' // magic ingredients
  },
  ...
  controller: function () {
    this.$onChanges = function (changes) {
      if (changes.user) {
        this.user = angular.copy(this.user);
      }
    };
    // now we can access this.onUpdate();
  }
};

經過&,咱們能夠傳遞函數,因此咱們經過this.updateUser字面量來把該函數從父組件傳遞到子組件,在子組件中更新的數據(經過在$onChanges中深拷貝從bindings對象中的屬性)而後經過傳遞進來的回調函數來將更新後的數據傳遞回去,數據從父組件到子組件,而後經過事件回調將更新後的數據通知到父組件。

接下來,咱們須要擴展咱們的模板來時的用戶能夠更新深拷貝的數據:

var childComponent = {
  ...
  template: `
    <div>
      <input type="text" ng-model="$ctrl.user.name">
      <a href="" ng-click="$ctrl.saveUser();">Update</a>
    </div>
  `,
  ...
};

這意味着咱們須要在控制器中添加this.saveUser方法,讓咱們添加它:

var childComponent = {
  ...
  template: `
    <div>
      <input type="text" ng-model="$ctrl.user.name">
      <a href="" ng-click="$ctrl.saveUser();">Update</a>
    </div>
  `,
  controller: function () {
    ...
    this.saveUser = function () {

    };
  }
};

儘管,當咱們在子組件中"保存"的時候,這其實僅僅是父組件回調函數的一個封裝,所以咱們在子組件中直接調用父組件方法this.updateUser(該方法已經綁定到了子組件onUpdate屬性上)

var childComponent = {
  ...
  controller: function () {
    ...
    this.saveUser = function () {
      // function reference to "this.updateUser"
        this.onUpdate();
    };
  }
};

好的,相信我,咱們已經到了最後階段,這也會使得事情變得更加有趣。相反咱們並非直接把this.user傳遞到回調函數中,而是構建了一個$event對象,這就像Angular 2同樣(使用EventEmitter),這也提供了在模板中使用$ctrl.updateUser($event)來獲取數據的一致性,這也就能夠傳遞給子組件,$event參數在Angular中是真實存在的,你能夠經過ng-submit等指令來使用它,你是否還記得以下函數:(譯者注:上面這一段翻譯須要推敲)

this.updateUser = function (event) {
  this.user = event.user;
};

咱們期待event對象帶有一個user的屬性,好吧,那就讓咱們來在子組件中saveUser方法中添加該屬性:

var childComponent = {
  ...
  controller: function () {
    ...
    this.saveUser = function () {
      this.onUpdate({
        $event: {
          user: this.user
        }
      });
    };
  }
};

上面的代碼看上去有些怪異。也許有一點吧,可是他是始終一致的,當你使用十遍之後,你就不再會中止使用它了。必要的咱們須要在子組件中建立this.saveUser,而後在該方法中調用從父組件中經過bindings傳遞進來的this.updateUsser,接着咱們傳遞給它event對象,來把咱們更新後的數據返回給父組件:

嘗試如上方式寫代碼吧:

這兒也有一個免費的教學視頻,是我關於$onChanges和單向數據流教程的一部分,你能夠從這獲取到 check it out here.

Is two-way binding through 「=」 syntax dead?

是的,單向數據綁定已經被認爲是數據流的最佳方式,React,Angular 2 以及其餘的類庫都是用單向數據流,如今輪到Angualr 1了,雖然Angular 1加入單向數據流有些晚,可是依然很強大並將改變Angular 1.x應用開發方式。

img

Using isFirstChange()

$onChanges還有一個特性,在changeshash對象中,該對象實際上是SimpleChange構造函數的一個實例,該構造函數原型對象上有一個isFirstChange方法。

function SimpleChange(previous, current) {
  this.previousValue = previous;
  this.currentValue = current;
}
SimpleChange.prototype.isFirstChange = function () {
  // don't worry what _UNINITIALIZED_VALUE is :)
  return this.previousValue === _UNINITIALIZED_VALUE;
};

這就是變化對象根據不一樣的綁定策略怎麼被創造出來(經過 new關鍵字)(我之前實現過單向數據綁定,並享受這一過程)

你爲何會想着使用該方法呢?上面咱們已經提到過,$onChanges會在組件的某給生命週期階段被調用,不只在父組件的數據改變時,(譯者注:也在數據初始化的時候也會被調用)所以咱們能夠經過該方法(isFirstChange)來判斷是非須要跳過初始化階段,咱們能夠經過在改變對象的某屬性上面調用isFirstChange方法來判斷$onChanges是不是第一次被調用。

this.$onChanges = function (changes) {
  if (changes.user.isFirstChange()) {
    console.log('First change...', changes);
    return; // Maybe? Do what you like.
  }
  if (changes.user) {
    this.user = angular.copy(this.user);
  }
};

Here’s a JSFiddle if you want to check the console.

$onDestroy

咱們最後來討論下最簡單的一個生命週期鉤子,$onDestroy

function SomeController($scope) {
  $scope.$on('$destroy', function () {
    // destroy event
  });
}
Using $onDestroy

你能夠猜測到該生命週期鉤子怎麼使用:

var childComponent = {
  bindings: {
    user: '<'
  },
  controller: function () {
    this.$onDestroy = function () {
      // component scope is destroyed
    };
  }
};

angular
  .module('app')
  .component('childComponent', childComponent);

若是你使用了$postLink來設置了DOM事件監聽函數或者其餘非Angular原生的邏輯,在$onDestroy中你能夠把這些事件監聽或者非原生邏輯清理乾淨。

Conclusion

Angular 1.x 應用開發者的的開發模式也隨着單向數據流,生命週期事件及生命週期鉤子函數的出現而改變,不久未來我將發佈更多關於組件架構的文章。

相關文章
相關標籤/搜索