AngularJS 控制器 (Controller) 学习笔记

控制器是 AngularJS 应用的核心组件之一,负责初始化应用状态、定义行为逻辑,并作为视图(HTML)和模型(Scope)之间的桥梁。


一、控制器的基本概念

1. 什么是控制器?
  • 定义:JavaScript 构造函数,用于初始化 $scope 对象。
  • 作用
    • 设置初始状态(数据)。
    • 定义行为(方法)。
    • 响应视图中的用户交互。
  • 生命周期:当 AngularJS 遇到 ng-controller 指令时创建,当 DOM 元素被销毁时销毁。
2. 定义控制器的两种方式

方式 A:全局注册(推荐用于简单应用)

var app = angular.module('myApp', []);

// 定义控制器
app.controller('MyController', function($scope, $http) {
    $scope.message = "Hello World";
    $scope.users = [];

    // 方法
    $scope.addUser = function(name) {
        $scope.users.push({ name: name });
    };
});

方式 B:模块内匿名定义(不推荐,难以测试)

angular.module('myApp', [])
    .controller('MyController', function($scope) {
        $scope.data = "Test";
    });

二、控制器语法风格

1. $scope 语法(传统)
app.controller('UserCtrl', function($scope, $http) {
    $scope.user = { name: "John", age: 25 };
    
    $scope.save = function() {
        $http.post('/api/save', $scope.user);
    };
});
  • 优点:简单直观,适合小型应用。
  • 缺点:容易混淆 $scopethis,在嵌套作用域中容易出错。
2. ControllerAs 语法(推荐,AngularJS 1.3+)
app.controller('UserCtrl', function($http) {
    var vm = this; // 使用 vm (ViewModel) 作为 this 的别名
    
    vm.user = { name: "John", age: 25 };
    
    vm.save = function() {
        $http.post('/api/save', vm.user);
    };
});
<!-- 视图中使用 -->
<div ng-controller="UserCtrl as userCtrl">
    <input ng-model="userCtrl.user.name">
    <button ng-click="userCtrl.save()">保存</button>
</div>
  • 优点
    • 避免原型继承陷阱。
    • 代码更清晰,易于阅读和维护。
    • 便于单元测试(不依赖 $scope)。
    • 支持 ES6 Class 语法。
3. 混合使用(不推荐)
// ❌ 避免混用
app.controller('BadCtrl', function($scope) {
    this.data = "Bad";
    $scope.other = "Worse";
});

三、依赖注入 (Dependency Injection)

控制器通过依赖注入获取服务(Services)、过滤器(Filters)等。

1. 隐式注入(不推荐,压缩后失效)
app.controller('MyCtrl', function($scope, $http) { ... });
// 压缩后变成 function(a, b) { ... },Angular 无法识别参数名
2. 数组注解法(推荐,生产环境标准)
app.controller('MyCtrl', ['$scope', '$http', function($scope, $http) {
    // 逻辑代码
}]);
3. 显式 $inject 属性(推荐,代码整洁)
function MyCtrl($scope, $http) {
    // 逻辑代码
}
MyCtrl.$inject = ['$scope', '$http'];
app.controller('MyCtrl', MyCtrl);

四、控制器的作用域与继承

1. 作用域链
  • 控制器创建一个新的 $scope 对象。
  • 子控制器继承父控制器的 $scope(原型链继承)。
  • 注意:在子 Scope 中修改对象属性是安全的,但修改原始类型(字符串、数字)会创建新属性,破坏继承链。
// 父控制器
app.controller('ParentCtrl', function($scope) {
    $scope.data = { value: 10 }; // 对象
    $scope.count = 5;            // 原始类型
});

// 子控制器
app.controller('ChildCtrl', function($scope) {
    $scope.data.value = 20; // ✅ 修改对象属性,影响父级
    $scope.count = 10;      // ❌ 创建新属性,不影响父级
});
2. 隔离作用域(指令中)
  • 指令可以使用 scope: {} 创建隔离作用域,不继承父控制器。
  • 控制器通常不直接创建隔离作用域,而是通过指令实现。

五、控制器生命周期钩子

AngularJS 控制器没有像 Vue 那样的显式生命周期钩子,但可以通过以下方式模拟:

1. 初始化逻辑

直接在构造函数中执行。

app.controller('InitCtrl', function($scope, $http) {
    // 初始化
    $scope.loadData = function() {
        $http.get('/api/data').then(function(response) {
            $scope.data = response.data;
        });
    };
    
    // 立即执行
    $scope.loadData();
});
2. 销毁钩子

监听 $destroy 事件。

app.controller('DestroyCtrl', function($scope) {
    $scope.$on('$destroy', function() {
        // 清理定时器、事件监听器等
        console.log('控制器销毁');
    });
});

六、控制器通信方式

1. 父子控制器通信
  • 父传子:通过 $scope 属性绑定。
  • 子传父:通过 $scope.$emit 或回调函数。
// 父控制器
app.controller('ParentCtrl', function($scope) {
    $scope.parentData = "Parent";
    $scope.onChildEvent = function(data) {
        console.log("收到子级事件:", data);
    };
});

// 子控制器
app.controller('ChildCtrl', function($scope) {
    $scope.sendToParent = function() {
        $scope.$emit('childEvent', "Hello Parent");
    };
    
    $scope.$on('childEvent', function(event, data) {
        $scope.parentData = data; // 注意:这里需要处理作用域
    });
});
2. 兄弟控制器通信
  • 通过 $rootScope 或共享服务(Service)。
  • 推荐:使用 Service 作为单例共享数据。
// 共享服务
app.service('SharedService', function() {
    this.data = "Shared Data";
    this.updateData = function(newData) {
        this.data = newData;
    };
});

// 控制器 A
app.controller('CtrlA', function($scope, SharedService) {
    $scope.update = function() {
        SharedService.updateData("New Data");
    };
});

// 控制器 B
app.controller('CtrlB', function($scope, SharedService) {
    $scope.$watch(function() {
        return SharedService.data;
    }, function(newVal) {
        $scope.sharedData = newVal;
    });
});

七、最佳实践

1. 保持控制器轻量
  • 控制器只负责视图逻辑。
  • 业务逻辑、数据访问放入 Service
  • DOM 操作放入 Directive
// ❌ 糟糕的控制器
app.controller('BadCtrl', function($scope, $http) {
    $scope.fetchData = function() {
        // 复杂的业务逻辑
        // 直接操作 DOM
        document.getElementById('div').style.color = 'red';
    };
});

// ✅ 优秀的控制器
app.controller('GoodCtrl', function($scope, DataService) {
    $scope.data = [];
    $scope.fetchData = function() {
        DataService.getData().then(function(result) {
            $scope.data = result;
        });
    };
});
2. 使用 ControllerAs 语法
  • 提高代码可读性。
  • 避免 $scope 陷阱。
  • 便于测试。
3. 避免在控制器中直接操作 DOM
  • 使用 ng-class, ng-style, ng-show 等指令。
  • 复杂 DOM 操作使用 Directive
4. 错误处理
app.controller('ErrorCtrl', function($scope, $http) {
    $scope.fetchData = function() {
        $http.get('/api/data')
            .then(function(response) {
                $scope.data = response.data;
            })
            .catch(function(error) {
                $scope.error = "加载失败: " + error.message;
            });
    };
});
5. 内存泄漏预防
  • $destroy 事件中清理定时器、事件监听器。
$scope.$on('$destroy', function() {
    clearInterval($scope.timer);
    $scope.$off('customEvent');
});

八、常见陷阱与解决方案

问题 原因 解决方案
数据不更新 异步回调中未触发 $apply 使用 $timeout$scope.$apply()
原型继承污染 直接修改原始类型属性 使用对象包裹数据 ($scope.data = {})
控制器重复初始化 指令重复使用 ng-controller 检查 HTML 结构,避免嵌套重复
内存泄漏 未清理定时器或事件监听 $destroy 事件中清理
测试困难 依赖 $scope 隐式注入 使用 ControllerAs + 数组注解法

九、总结

AngularJS 控制器的核心要点:

  1. 职责单一:只处理视图逻辑,业务逻辑交给 Service。
  2. 语法选择:优先使用 ControllerAs 语法,避免 $scope 陷阱。
  3. 依赖注入:使用数组注解法或 $inject 属性,确保代码可压缩。
  4. 通信机制:父子通过 $scope,兄弟通过 Service 或 $rootScope
  5. 生命周期:利用 $destroy 事件清理资源,防止内存泄漏。
  6. 测试友好:保持控制器轻量,便于单元测试。

通过遵循这些最佳实践,可以构建出结构清晰、易于维护的 AngularJS 应用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐