weixin_39689428
weixin_39689428
2020-12-28 17:30

深入理解Angular 1.5的生命周期钩子

Posted on Jun 3, 2016 - Edit this page on GitHub

生命周期钩子是一些简单的函数,这些函数会在Angular应用组件特定生命周期被调用。生命周期钩子在Angular 1.5版本被引入,通常与.component()方法一起使用,并在接下来的几个版本中演变,并包含了更多有用的钩子函数(受Angular 2的启发)。让我们深入研究这些钩子函数并实际使用它们吧。这些钩子函数所带来的作用以及为什么我们需要使用它们,对于我们深入理解通过组件架构的应用具有重要的意义。

在Angular v1.3.0+版本,我自己实现了.component() 方法,该方法深刻得洞悉了怎么去使用这些生命周期函数以及这些函数在组件中的作用,让我们开始研究它吧。

**Table of contents

**$onInit

什么是$onInit ?首先,他是Angular组件(译注:通过.component() 方法定义的组件)控制器中暴露出来的一个属性,我们可以把一个函数赋值给该属性:


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

    };
  }
};

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

$onInit 生命周期钩子用作控制器的初始化工作,下面举个常用例子:

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

    };
  }
};

注意上面的代码,我们把所有的属性直接赋值到了this上面,它们就像“浮在”控制器的各个角落。现在,让我们通过$onInit 来重写上面代码:

 javascript
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 ,包含了一些初始化信息。

对于this.fooBar函数呢?不要着急,该函数放在$onInit外面是完全能够访问到初始化数据的,比如当你调用this.fooBar的时候,函数会打印出this.foo的值,也就是在$onInit函数中定义的'bar'。因此所有你初始化的数据都正确地绑定到了控制器的this 上下文中。

**$onInit + “require”

因为这些生命周期钩子定义得如此优雅(不同的生命周期钩子都在组件的不同生命周期被调用),一个组件也可以从另外的组件中继承方法,甚至继承的方法在$onInit 钩子中就可以直接使用。

首先我们需要思考的是如何使用require,我写过另外一篇深入介绍$onInit 和 require的文章,但是在此我依然会简要介绍一些require的基本用法,随后将提供一个完整的实例。

让我们来看看myComponent的例子,在这儿require后面紧跟的是一个对象(只在.component()方法中require字段后面接对象),当require.directive()结合使用的时候,require字段后面也可以跟数组或者字符串语法形式。

 javascript
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中使用任何当定在父组件中的方法了。

 javascript
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的控制器同名,那么就可以省略控制器名。这一特性并没有带来给功能带来很大的改变,我们可以如下使用它:

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

正如你所见,我们完全省略了需要requre的控制器名而直接使用^^替代。完整写法^^parent就被省略为^^。需要谨记,在前面的一个例子中,我们只能使用parent: '^^anotherComponent'来表示我们需要使用另外一个组件中控制器中的方法(译者注:作者以上就是控制器和requre的属性名不相同时,不能够省略),最后,我们只需记住一点,如果我们想使用该条特性,那么被requre的控制器名必须和require的属性名同名。

**Real world $onInit + require

让我们使用$onInitrequire来实现一个tabs组件,首先我们实现的组件大概如如下使用:

 html
<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值。

首先,组件定义了每个组件都必须使用的一些属性:

 javascript
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钩子函数中:

 javascript
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组件控制器中的逻辑,这可能稍微有点复杂:

 javascript
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组件中使用。

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

正如我们在文章关于$onInitrequire部分提到,我们通过^^来只requre父组件控制器中的逻辑而不在自身组件中寻找这些方法。除此之外,当我们require的控制器名和requre对象中的属性名相同时我们还可以省略requre的控制器名字,这是版本1.5.6新增加的一个特性。关于这一新特性准备好了吗?在下面代码中,我们使用tabs: '^^',我们有一个和require控制器同名的属性名{tabs: ...},这样我们就可以在$onInit中使用this.tabs来调用父组件控制器中的方法了。

 javascript
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);
    };
  }
  ...
};

把所有代码放一起:

 javascript
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‘链接函数’,如如下形式:

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

你也可能知道如下:

 javascript
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钩子函数。

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

我们已经知道,$postLink是在所有的子元素被链接后触发,接下来让我们来实现我们的tabs组件。

**Real world $postLink

我们可以通过$postLink函数来给我们的选项卡组件一个初始的选项卡。首先我们需要调整一下模板:

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

现在我们就可以通过bindings获取到selected特性的值,然后用以初始化:

 javascript
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组件。

 javascript
var childComponent = {
  bindings: { user: '

注意,这儿bindings对象包含了一个值为'<'user字段,该‘<’表示了单向数据流,这一点在我以前的 文章已经提到过,单向数据流会导致$onChanges钩子被调用。

但是,正如上面提到,我们需要一个parentComponent组件来完成我的实例:

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

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

需要注意的是:<child-compoent></component>组件在<parent-component></parent-component>组件中渲染,这就是为什么我们能够初始化一个带有数据的控制器,并且把这些数据传递给childComponent:

 javascript
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组件:

 javascript
var childComponent = {
  bindings: {
    user: '
      <pre>{{ $ctrl.user | json }}</pre>
    

该提问来源于开源项目:Jocs/jocs.github.io

`, controller: function () { this.$onChanges = function (changes) { this.user = changes; }; } };
  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

8条回答

  • weixin_39668571 weixin_39668571 3月前

    RIP AngularJS

    点赞 评论 复制链接分享
  • weixin_39689428 weixin_39689428 3月前

    哈哈,「愿灵安眠」

    点赞 评论 复制链接分享
  • weixin_39624094 weixin_39624094 3月前

    很抱歉我挖坟了……

    点赞 评论 复制链接分享
  • weixin_39689428 weixin_39689428 3月前

    没事,我帮你按着棺材盖。

    点赞 评论 复制链接分享
  • weixin_39658619 weixin_39658619 3月前

    这篇文章太TMD好了!!!深度好文

    点赞 评论 复制链接分享
  • weixin_39689428 weixin_39689428 3月前

    好就 star 下呗

    点赞 评论 复制链接分享
  • weixin_39658619 weixin_39658619 3月前

    已经star

    点赞 评论 复制链接分享
  • weixin_39658619 weixin_39658619 3月前

    已经star

    点赞 评论 复制链接分享