当前位置: 首页 > news >正文

@angular/forms 源码解析之双向绑定

我们知道,Angular 的 @angular/forms 包提供了 NgModel 指令,来实现双向绑定,即把一个 JS 变量(假设为 name)与一个 DOM 元素(假设为 input 元素)进行绑定,这样 name 的值发生变化,input 元素 的 value 也会自动变化;input 元素的 value 发生变化,name 的值也会自动变化。如下代码,展示一个最简单的双向绑定(也可见 stackblitz demo):

@Component({
  selector: 'my-app',
  template: `
    <input [ngModel]="name" (ngModelChange)="this.name=$event">
    <button (click)="this.name = this.name + ' , apple';">ChangeName</button>
    <p>{{name}}</p>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  name = 'banana';
}

上面代码使用了 NgModel 指令来把变量 nameinput DOM 元素双向绑定到了一起,这里为了更清晰理解 NgModel 的本质,没有使用 [(ngModel)] 语法糖。实际上,在模板里写 [(xxx)] 这种 'BANANA_BOX' 语法,@angular/compiler 的 Template Parser 会把这种语法拆解为为 [xxx](xxxChange),可看 L448-L453L501-L505,所以 [(xxx)] 仅仅是为了省事的简单写法。

查看 stackblitz demo 可以看到,如果修改 input 里的值,name 变量的值也自动发生变化了,这点可从与 name 绑定的 p 标签值自动变化看出;如果点击 button 修改了 name 的值,input 输入框内的 value 值也发生变化了,这点可从 input 框内的值变化可看到。那 NgModel 指令是如何做到双向绑定的呢?

在理解 NgModel 指令双向绑定原理之前,可以先看看双向绑定最简单形式:

<input [value]="country" (input)="country = $event.target.value">
<button (click)="country = country + ' , China';">ChangeCountry</button>
<p>Hello {{country}}!</p>

点击 button 修改 model 时,就会自动修改 input 的 value 值,即自动修改 view,数据流方向就是 model -> view;更新 input 框内值时,就会自动修改 country 这个 model 值,这点可从绑定的 p 标签看到,这时数据流方向就是 view -> model。当然,这是最简单且最不可扩展的一个双向绑定实例,如果去设计一个指令,不仅仅需要考虑 view 的不同类型,而且还需要考虑数据校验问题。尽管如此,这个简单实例与 NgModel 指令本质是类似的。

如果自己设计这样一个双向绑定指令,那它的输入必然是绑定的变量 name,该指令接收 name 后再去更新 input 元素的 value 值(还得支持 textarea,select 等 DOM 元素,甚至组件等自定义 DOM 元素),这样 name 发生变化,input 的 value 也会自动变化,即 model -> view;输出的必然是 input 元素的 value 值,然后赋值给 name,这样 input 元素的值变化,name 值也自动变化,即 view -> model。这里的最难点是该指令得能够写 DOM 元素(不管原生或者自定义 DOM 元素)的值,并且能够监听 DOM 元素的值变化,读取变化的值。 所以,为了支持原生 DOM 元素或自定义 DOM 元素,为了有个好的设计模式,必然会抽象出一个接口,来帮助指令去写入和监听读取 DOM 元素值,有了这个接口,事情就简单很多了。

现在,我们需要搞明白两个问题:name 值发生变化时,input 的 value 如何自动变化;input 的 value 变化,name 值如何自动变化?

绑定到 input 上的 NgModel 指令在实例化时,其 构造函数 会首先查找出 ControlValueAccessor 对象,这个 ControlValueAccessor 就是上文提到的抽象出来的对象,该对象会具体负责更新和监听读取 DOM 元素的值。上文模板中的 input 元素不仅仅绑定了 NgModel 指令,实际上还绑定了 DefaultValueAccessor 指令,这点可以从该指令的选择器知道,如果 input 模板是这么写的:

<input [ngModel]="name" (ngModelChange)="this.name=$event" type="number">

那不仅仅绑定了 DefaultValueAccessor 指令,还绑定了 NumberValueAccessor 指令。

由于 DefaultValueAccessor 的 providers 属性提供了 NG_VALUE_ACCESSOR 令牌,并且该令牌指向的对象就是 DefaultValueAccessor,所以 NgModel 构造函数中注入的 NG_VALUE_ACCESSOR 令牌包含的 ControlValueAccessor 对象数组只有 DefaultValueAccessor 一个。如果是 type="number" 的 input,则 valueAccessors 包含 NumberValueAccessorDefaultValueAccessor 这两个对象。构造函数中的 selectValueAccessor() 方法会依次遍历 NG_VALUE_ACCESSOR 令牌提供的 ControlValueAccessor 对象数组,如果是自定义的 ControlValueAccessor 优先选择自定义的,如果是 @angular/forms 内置的 ControlValueAccessor 就选择内置的(内置的也就 6 个),否则最后选择默认的 ControlValueAccessorDefaultValueAccessor 对象。对于本文 demo,那就是默认的 DefaultValueAccessor 对象。注意的一点是,注入的 NG_VALUE_ACCESSOR 令牌有装饰器 @Self,所以只能从自身去查找这个依赖,自身的意思是 NgModel 指令自己,和它一起挂载到 input 元素的其他指令。另外,input 上没有绑定任何 validators 指令,所以注入的 NG_VALIDATORS 和 NG_ASYNC_VALIDATORS 令牌解析的值为空,并且 input 单独使用,没有放在 form 元素内,或 FormGroup 绑定的元素内,所以不存在宿主控件容器 ControlContainer,即 parent 也为空。

NgModel 指令在首次实例化时,运行 _setUpControl() 方法,利用 ControlValueAccessor(本 demo 即 DefaultValueAccessor 对象) 把 NgModel 指令内部的 FormControl 对象与 DOM 元素绑定。由于本 demo 中,NgModel 指令绑定的 input 没有父控件容器,所以会调用 _setUpStandalone 方法,核心方法就是 setUpControl(),该方法主要包含两点:第一点,通过调用 setUpViewChangePipeline()DefaultValueAccessor 对象内注册一个回调函数,这样当 input 值发生变化时,就触发 input 事件 时,会执行这个回调函数,而这个回调函数的逻辑 一是更新 FormControl 的 value,二是让 NgModel 指令抛出 ngModelChange 事件,该事件包含的值就是当前 input 变化的新值,所以,setUpViewChangePipeline() 方法的作用就是搭建了 view -> model 的管道,这样 view (这里是 input) 值发生变化时,会同步 FormControl 对象的 value 值,并让 NgModel 指令把这个新值输出出去;第二点,通过调用 setUpModelChangePipeline 方法向 FormControl 对象内注册 一个回调,这个回调逻辑是当 FormControl 的 value 值发生变化时(本 demo 中就是 [ngModel]="name" 时,name 值发生变化,也就是属性值改变,这样 isPropertyUpdated(changes, this.viewModel) 就为 true,这样就会需要更新 FormControl 的 value 值 FormControl.setValue(value),从而会 触发 上文说的 FormControl 对象内的回调函数),通过调用 ControlValueAccessor.writeValue() 方法去修改 view (这里是 input) 的 value 值(本 demo 中使用的是 DefaultValueAccessor.writeValue(value)),然后让 NgModel 指令抛出 ngModelChange 事件,该事件包含的值就是当前 FormControl 对象 变化的新值,所以,setUpModelChangePipeline() 方法的作用就是搭建了 model -> view 的管道,这样 FormControl 对象值发生改变时,会同步更新 view 的 value,并让 NgModel 指令把这个新值输出出去。

通过以上的解释,就能理解 name 值发生变化时,input 的 value 是如何自动变化的;input 的 value 发生变化时,name 值是如何自动变化的。(最好能一个个点击链接查看源码,效率更高。) 一句话解释就是:NgModel 指令初始化时先安装了两个回调(一个是 view 变化时更新 FormControl 对象 value 值的回调,另一个是 FormControl 对象 value 值变化时更新 view 值的回调),数据流方向从 view -> model 时,更新 FormControl 对象并抛出携带该值的 ngModelChange 事件,数据流方向从 model -> view 时,利用 ControlValueAccessor 去更新 view 值,同时也抛出携带该值的 ngModelChange 事件。抛出的 ngModelChange 事件包含新值,模板中的 $event 会被 @angular/compiler 特殊处理,为 ngModelChange 事件抛出的值。

当然,本文没有考虑存在 Validators 的情况,如果 input 模板修改为如下代码:

<input [ngModel]="name" (ngModelChange)="this.name=$event" required>

那该模板除了绑定 NgModel 指令外,还绑定了 RequiredValidator 指令,这样不管数据流方向是 view -> model 还是 model -> view,在数据流动之前,还需要运行验证器,验证数据的有效性。这样 NgModel 的构造函数里就会包含 一个 RequiredValidator 对象,然后 把这个 Validator 传给 FormControl 对象,最后注册 validatorChange 回调,这样以后 FormControl 值更新时就会 运行 Validators

总之,NgModel 指令来管理 model <-> view 的数据流,内部存在一个 FormControl 对象,用来读取存储值和验证有效性,从 FormControl 读取的值会赋值给外界传进来的 model,view 是借助 ControlValueAccessor 来读写值。整个 @angular/forms 包的设计也是按照这种数据流形式,并不复杂。

也可阅读 @angular/forms 相关文章了解如何写一个自定义的 ControlValueAccessor:译 别再对 Angular 表单的 ControlValueAccessor 感到迷惑

相关文章:

  • C# 获取电脑的网络连接状态
  • leetcode 有效的字母异位词 java 版本
  • memset函数,strcpy函數,memcp函數
  • 老司机 iOS 周报 #30 | 2018-08-06
  • 机器学习 -- 机器学习是什么?
  • TCP三次握手四次挥手手动实践
  • 初识 Spring(05)---(Annotation注解)
  • win7系统已经够用为什么还要开发win10系统
  • python3 简单爬虫
  • HAP将新的页面加入到菜单
  • 常用的CSS各类属性
  • 从计算机知识到落地能力,你欠缺了什么?
  • 阿里云王牌架构师杨曦:N多环境N多应用个性配置管理如何从混乱到简单?
  • 小程序开发中的那些坑
  • Intellij idea 快捷键持续更新
  • 时间复杂度分析经典问题——最大子序列和
  • AHK 中 = 和 == 等比较运算符的用法
  • CoolViewPager:即刻刷新,自定义边缘效果颜色,双向自动循环,内置垂直切换效果,想要的都在这里...
  • Fundebug计费标准解释:事件数是如何定义的?
  • javascript从右向左截取指定位数字符的3种方法
  • JAVA并发编程--1.基础概念
  • Linux链接文件
  • Puppeteer:浏览器控制器
  • Python中eval与exec的使用及区别
  • Sass Day-01
  • Swoft 源码剖析 - 代码自动更新机制
  • tab.js分享及浏览器兼容性问题汇总
  • vue-cli在webpack的配置文件探究
  • 二维平面内的碰撞检测【一】
  • 思否第一天
  • 为视图添加丝滑的水波纹
  • 想写好前端,先练好内功
  • 写给高年级小学生看的《Bash 指南》
  • 鱼骨图 - 如何绘制?
  • 智能网联汽车信息安全
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • ​HTTP与HTTPS:网络通信的安全卫士
  • #Z0458. 树的中心2
  • #在 README.md 中生成项目目录结构
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (bean配置类的注解开发)学习Spring的第十三天
  • (C语言)二分查找 超详细
  • (k8s中)docker netty OOM问题记录
  • (附源码)计算机毕业设计高校学生选课系统
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (四)【Jmeter】 JMeter的界面布局与组件概述
  • (一)【Jmeter】JDK及Jmeter的安装部署及简单配置
  • (一)80c52学习之旅-起始篇
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .
  • *上位机的定义
  • ... 是什么 ?... 有什么用处?
  • .class文件转换.java_从一个class文件深入理解Java字节码结构
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)...
  • .Net转Java自学之路—基础巩固篇十三(集合)
  • @Autowired标签与 @Resource标签 的区别