AngularJS 事件处理学习笔记(详细版 v1.x)
📌核心区分:AngularJS 中有两类完全不同的“事件”
- DOM 事件:用户交互触发(点击、输入、键盘等),通过
ng-*指令绑定。- 作用域事件(Scope Events):组件/控制器间通信机制,通过
$emit/$broadcast/$on实现。⚠️现代 AngularJS (1.5+) 建议:优先使用组件绑定(
&回调 /<单向数据)替代作用域事件。作用域事件易导致“事件面条代码”,仅建议在遗留系统或特殊跨级通信中使用。
🖱️ 一、DOM 事件绑定(内置指令)
1. 常用事件指令
| 指令 | 触发时机 | 备注 |
|---|---|---|
ng-click/ng-dblclick | 鼠标单击/双击 | 最常用 |
ng-mousedown/ng-mouseup/ng-mouseenter/ng-mouseleave | 鼠标按下/抬起/进入/离开 | 替代 jQueryhover |
ng-keydown/ng-keyup/ng-keypress | 键盘按键 | 推荐ng-keyup处理输入逻辑 |
ng-change | 模型值改变 | 必须配合ng-model,不监听原生change事件 |
ng-blur/ng-focus | 失去/获得焦点 | 表单验证常用 |
ng-submit | 表单提交 | 绑定在<form>上,自动阻止默认提交 |
ng-copy/ng-cut/ng-paste | 剪贴板操作 | 需配合$event获取数据 |
2. 语法与传参
<!-- 基础绑定 --><buttonng-click="vm.save()">保存</button><!-- 传递参数 + 事件对象 --><buttonng-click="vm.delete(item.id, $event)">删除</button><!-- 结合表达式(不推荐复杂逻辑) --><inputng-keyup="vm.searchText && vm.doSearch()">vm.delete=function(id,$event){$event.preventDefault();// 阻止默认行为$event.stopPropagation();// 阻止冒泡// 业务逻辑...};3.ng-change与防抖优化
ng-change默认在每次输入时触发,频繁触发会导致性能问题。
<!-- 推荐:使用 ng-model-options 防抖 --><inputng-model="vm.keyword"ng-change="vm.search()"ng-model-options="{ debounce: 300, updateOn: 'default blur' }">✅debounce: 300:停止输入 300ms 后才触发ng-change
✅updateOn: 'blur':仅在失焦时更新模型(适合表单验证)
📡 二、作用域事件通信($emit/$broadcast/$on)
1. 传播方向对比
| 方法 | 传播方向 | 适用场景 | 性能影响 |
|---|---|---|---|
$scope.$emit('name', data) | 子 → 父 → $rootScope(向上冒泡) | 子组件通知父组件 | 低(仅遍历祖先) |
$scope.$broadcast('name', data) | 父 → 子 → 所有后代(向下广播) | 父组件通知多个子组件 | 高(遍历整棵子树) |
$rootScope.$broadcast() | 全局广播 | 跨模块通信(不推荐) | 极高(遍历所有 scope) |
2. 监听事件:$on
// 监听事件varderegister=$scope.$on('user:updated',function(event,data){console.log('收到数据:',data);console.log('事件名:',event.name);console.log('触发源 scope:',event.targetScope);console.log('当前 scope:',event.currentScope);// 控制传播event.stopPropagation();// 仅对 $emit 有效event.preventDefault();// 标记 event.defaultPrevented = true});// ⚠️ 重要:手动注销(尤其绑定在 $rootScope 时)$scope.$on('$destroy',deregister);3. DOM$eventvs Scope$event
| 属性/方法 | DOM 事件 (ng-click) | 作用域事件 ($on) |
|---|---|---|
| 来源 | 浏览器原生事件(jqLite/jQuery 包装) | Angular 内部构造的事件对象 |
event.target | 触发事件的 DOM 节点 | ❌ 不存在 |
event.preventDefault() | 阻止浏览器默认行为 | 仅设置defaultPrevented = true |
event.stopPropagation() | 阻止 DOM 冒泡 | 阻止 Scope 事件继续传播 |
| 传参方式 | ng-click="fn($event)" | $emit('name', arg1, arg2)→$on('name', (e, arg1, arg2)=>{}) |
⚡ 三、与原生/第三方事件的集成
Angular 只能自动检测自身上下文内的变化。外部事件需手动触发$digest。
1. 安全同步方案
// ❌ 危险:可能报 $digest already in progresselement.addEventListener('click',function(){$scope.$apply(()=>vm.count++);});// ✅ 推荐 1:$applyAsync(合并到下一次 digest,防报错)element.addEventListener('click',function(){$scope.$applyAsync(()=>vm.count++);});// ✅ 推荐 2:$timeout(自动 $apply,异步安全)$timeout(()=>vm.count++);// ✅ 推荐 3:$evalAsync(在当前 digest 周期末尾执行)$scope.$evalAsync(()=>vm.count++);2. 第三方库集成模板(如 ECharts、Swiper)
link:function(scope,element){varchart=echarts.init(element[0]);chart.on('click',function(params){// 第三方回调不在 Angular 上下文scope.$applyAsync(function(){scope.vm.selectedData=params.data;});});// 销毁时清理scope.$on('$destroy',function(){chart.dispose();});}🛑 四、常见坑与避坑指南
| 现象 | 原因 | 解决方案 |
|---|---|---|
$digest already in progress | 在 Angular 上下文中重复调用$apply() | 改用$applyAsync()/$timeout()/$evalAsync() |
$rootScope.$on导致内存泄漏 | $rootScope永不销毁,监听器不会自动清理 | 必须保存返回值并在$destroy时调用注销函数 |
ng-change不触发 | 未绑定ng-model,或值未真正改变 | 检查ng-model;对象引用未变时不会触发 |
$broadcast页面卡顿 | 广播遍历整个作用域树,Watcher 过多 | 改用 Service 共享状态 / 组件&回调 / RxJS Subject |
| 事件绑定后重复触发 | 未解绑 DOM 事件或$on,指令重复编译 | scope.$on('$destroy', () => element.off())+ 注销$on |
$event.stopPropagation()无效 | 在$broadcast中使用(仅对$emit有效) | 广播无法中途停止,需改用条件判断或重构通信方式 |
📊 五、性能优化与最佳实践
✅ 推荐做法
优先使用组件绑定替代作用域事件
// 父组件<child-component on-save="vm.handleSave(data)"></child-component>// 子组件定义bindings:{onSave:'&'},controller:function(){this.submit=function(){this.onSave({data:this.formData});// 传递命名参数};}全局通信使用 Service + 发布订阅
app.factory('EventBus',function($rootScope){varbus={};bus.on=function(event,fn){return$rootScope.$on(event,fn);// 返回注销函数};bus.emit=function(event,data){$rootScope.$emit(event,data);// 用 $emit 替代 $broadcast 提升性能};returnbus;});高频事件防抖/节流
- 输入搜索:
ng-model-options="{ debounce: 300 }" - 滚动/resize:使用
lodash.throttle+$applyAsync
- 输入搜索:
严格清理事件监听
varunbindDom=element.on('scroll',handler);varunbindScope=$scope.$on('custom:event',handler);$scope.$on('$destroy',function(){unbindDom();unbindScope();});
🚫 反模式(Avoid)
- ❌ 在 Controller 中直接
document.addEventListener - ❌ 滥用
$rootScope.$broadcast做全局状态管理 - ❌ 在
ng-repeat内部绑定大量独立事件(使用事件委托) - ❌ 用
$watch替代事件通信(性能更差)
📦 六、完整示例:安全的事件通信模式
// parent.component.jsangular.module('app').component('parentPanel',{template:`<h3>父组件</h3> <p>收到消息: {{ $ctrl.message }}</p> <child-item on-notify="$ctrl.handleNotify(msg)"></child-item>`,controller:function(){varvm=this;vm.message='无';vm.handleNotify=function(msg){vm.message=msg;};}});// child.component.jsangular.module('app').component('childItem',{template:`<button ng-click="$ctrl.send()">通知父级</button>`,bindings:{onNotify:'&'},controller:function(){varvm=this;vm.send=function(){// 使用 & 绑定回调,替代 $emitvm.onNotify({msg:'来自子组件: '+Date.now()});};}});💡 此模式完全避免
$scope事件,符合 AngularJS 1.5+ 组件化规范,易于测试与维护。
🔄 七、向 Angular (2+) 迁移提示
| AngularJS (1.x) | Angular (2+) |
|---|---|
ng-click="fn()" | (click)="fn()" |
$scope.$emit / $broadcast | @Output() event = new EventEmitter()+ RxJSSubject |
$on('$destroy') | ngOnDestroy()生命周期钩子 |
$applyAsync() | 变更检测自动处理(Zone.js) |
ng-model-options debounce | RxJS debounceTime()+ 响应式表单 |
📚 延伸学习
- 📘 官方文档:ngClick | [r o o t S c o p e . S c o p e ] ( h t t p s : / / d o c s . a n g u l a r j s . o r g / a p i / n g / t y p e / rootScope.Scope](https://docs.angularjs.org/api/ng/type/rootScope.Scope](https://docs.angularjs.org/api/ng/type/rootScope.Scope) | ngModelOptions
- 🔍 调试工具:ChromeAngularJS Batarang→ 查看 Scope 树与事件传播路径
- 📖 推荐阅读:《AngularJS 权威教程》第 8 章(事件与作用域通信)