angular的指令很像我们所说的组件,不过比较好玩的是,angular里面存在作用域继承。不过这篇文章不讲这个问题,关于angular作用域继承,可以阅读这篇文章。本文要讲的是另外一个话题,即指令中修改$scope的某个属性时,父级作用域没有接收到这个修改。
问题还原
在开发中,我使用了如下的一套方法:
// controllerA
module.exports = ['
s
c
o
p
e
′
,
f
u
n
c
t
i
o
n
(
scope', function(
scope′,function(scope) {
$scope.members = []
s
c
o
p
e
.
scope.
scope.watch(‘members’, newValue => console.log(newValue))
}]
// template
- {{m.name}}
angular的作用域
简单理解angular的作用域,即脚本
s
c
o
p
e
的
属
性
(
或
方
法
)
可
以
在
一
个
H
T
M
L
结
构
范
围
内
直
接
调
用
,
调
用
方
式
有
绑
定
(
如
n
g
−
c
l
i
c
k
=
"
c
l
o
s
e
(
)
"
中
的
c
l
o
s
e
(
)
)
和
插
值
(
如
t
e
x
t
)
两
种
。
怎
么
生
成
一
个
作
用
域
呢
?
最
常
用
的
就
是
在
标
签
上
加
n
g
−
a
p
p
和
n
g
−
c
o
n
t
r
o
l
l
e
r
指
令
,
另
外
还
有
n
g
−
r
e
p
e
a
t
、
n
g
−
i
n
c
l
u
d
e
、
n
g
−
s
w
i
t
c
h
、
n
g
−
v
i
e
w
可
以
创
建
作
用
域
。
当
某
些
标
签
使
用
了
上
述
指
令
之
后
,
这
个
标
签
范
围
内
的
H
T
M
L
结
构
,
就
可
以
调
用
指
令
对
应
脚
本
内
的
scope的属性(或方法)可以在一个HTML结构范围内直接调用,调用方式有绑定(如ng-click="close()"中的close())和插值(如{{text}})两种。怎么生成一个作用域呢?最常用的就是在标签上加ng-app和ng-controller指令,另外还有ng-repeat、ng-include、ng-switch、ng-view可以创建作用域。当某些标签使用了上述指令之后,这个标签范围内的HTML结构,就可以调用指令对应脚本内的
scope的属性(或方法)可以在一个HTML结构范围内直接调用,调用方式有绑定(如ng−click="close()"中的close())和插值(如text)两种。怎么生成一个作用域呢?最常用的就是在标签上加ng−app和ng−controller指令,另外还有ng−repeat、ng−include、ng−switch、ng−view可以创建作用域。当某些标签使用了上述指令之后,这个标签范围内的HTML结构,就可以调用指令对应脚本内的scope属性,一般一个应用内ng-app对应的作用域变量是
r
o
o
t
S
c
o
p
e
,
也
就
是
最
顶
层
的
作
用
域
,
在
n
g
−
a
p
p
内
部
创
建
的
任
何
作
用
域
都
可
以
继
承
rootScope,也就是最顶层的作用域,在ng-app内部创建的任何作用域都可以继承
rootScope,也就是最顶层的作用域,在ng−app内部创建的任何作用域都可以继承rootScope。
作用域继承
有关angular作用域继承问题的文章非常多,文章开头也给了一个链接。这里,我想着重强调“继承”的本质和angular的规律。
首先,我们讲的继承,实际上是表达“可以直接用”这个想法。所谓可以直接用,就是不需要你在脚本的$scope上加一个父级作用域已经声明过的值,直接在HTML结构范围内使用即可。例如:
其次,作用域继承的本质,是子作用域创建或实例化时,对父作用域里面$scope的引用,就和JavaScript的prototype继承一摸一样,当在子作用域里面定义了一个和父作用域同名的值时,它就覆盖了父作用域该同名值的引用,也就脱离了父级作用域的控制。这样说还是太抽象,我们用为代码来解释:
$parent.name = ‘david’
$parent.age = 13
s
c
o
p
e
.
n
a
m
e
=
′
t
o
m
′
/
/
此
时
scope.name = 'tom' // 此时
scope.name=′tom′//此时scope.age自动获得13,这个过程相当于:
// $scope.prototype = $parent // 这是伪代码
//
s
c
o
p
e
.
n
a
m
e
=
′
t
o
m
′
/
/
因
此
,
当
你
改
了
scope.name = 'tom' // 因此,当你改了
scope.name=′tom′//因此,当你改了parent.age的时候,
s
c
o
p
e
.
a
g
e
也
变
了
,
但
当
你
改
scope.age也变了,但当你改
scope.age也变了,但当你改parent.name的时候,对$scope没有影响
总之,抛开一些特殊情况(上面提到的特殊内置指令),angular的普通作用域继承是非常简单且容易理解的,甚至都不需要思考,而且在HTML里面写作用域的值(或调用)非常自然,你不需要思考或顾虑太多,因为你可以非常容易的觉察到哪些值是可以用的,哪些是不可以的。
指令的作用域
angular出色的扩展功能非指令莫属。但是指令的作用域又有点复杂。
首先,我们要知道指令的作用域到底是什么?因为对于HTML文档中而言,一个ng指令其实就是一个标签或属性,它不是一个完整的HTML结构范围,何来作用域之说呢?但是它的脚本里面又有 s c o p e , 那 么 这 个 scope,那么这个 scope,那么这个scope到底是用在什么地方的呢?实际上,指令的脚本里面的$scope不是对使用指令的HTML文档而言,而是指令自己内部的templete内可用。比如说:
// controller.html
// my-directive.html {{text}} 我们创建一个my-directive指令,而my-directive内使用templateUrl使用my-directive.html作为模板,因此,在my-directive.js脚本里面的$scope对my-directive.html内的插值或调用有效。其次,指令作用域的继承问题。
在创建一个指令的时候,我们在指令的创建函数返回值中,有一个scope选项:
module.exports = function() {
return {
restrict: ‘A’,
scope: true,
}
}
这里的scope选项就是用来确定,指令外部的作用域和指令内部的作用域之间的关系。默认情况下,scope:false,表示共享父作用域,也就是说指令内部的
s
c
o
p
e
直
接
是
指
令
所
依
附
的
那
个
标
签
所
在
的
作
用
域
的
scope直接是指令所依附的那个标签所在的作用域的
scope直接是指令所依附的那个标签所在的作用域的scope(即JavaScript里面的object引用),因此,无论是外部的
s
c
o
p
e
发
生
变
化
,
还
是
指
令
内
部
的
scope发生变化,还是指令内部的
scope发生变化,还是指令内部的scope值发生变化,两边都是同步的,也就会引起界面的变化,不过这种共享作用域常常遇到一些不可预料的问题,所以很少直接用。而当scope:true,表示继承父作用域,继承模式也是原型继承,也就是说,在指令内的
s
c
o
p
e
默
认
拥
有
指
令
依
附
的
那
个
标
签
所
在
的
作
用
域
的
scope默认拥有指令依附的那个标签所在的作用域的
scope默认拥有指令依附的那个标签所在的作用域的scope值,当然,如果你在指令内部修改了$scope的某个属性,那么这个属性值将覆盖外部作用域对应的属性值。
当思路到这里的时候,一个自然而然的问题浮现了。如果在 s c o p e 默 认 没 有 值 的 情 况 下 , 会 自 动 继 承 父 级 scope默认没有值的情况下,会自动继承父级 scope默认没有值的情况下,会自动继承父级scope的值,但是如果我新设了一个属性值覆盖了父级 s c o p e 的 属 性 值 , 父 级 scope的属性值,父级 scope的属性值,父级scope对应属性值再变时,会不会影响当前这个$scope被设置的新值?
这个答案是显而易见的:在你没有设那个属性值之前,修改父$scope对应属性,子作用域继承了父作用域,那个属性值是跟着变的,界面也随着变化。但是,一旦当你设置了新值的那一刻,继承关系就断了,这个属性将不再相互影响,子作用域当然不会影响父作用域,同时,父作用域也不会影响子作用域。实际上,理解这一点,用JavaScript的原型链继承是再适合不过的,这就考你的基础功了。
除了把指令的scope选项设置为true|false之外,还可以设置为一个{},例如:
return {
restrict: ‘A’,
scope: {
name: ‘@name’,
age: ‘=age’,
sing: ‘&sing’,
father: ‘@’,
mother: ‘@?’,
},
}
上面5种情况分别代表了5种表达方式,但表达与外部作用域关系的,只有3种,现在一一解释。
'@name’表示这个指令所依附的标签接受一个name属性,这个name属性传入的值,是一个字符串,字符串将作为当前指令里面的$scope的某属性值,在当前作用域修改这个值,不会影响外部作用域的对应值,举个例子:
// 外部
// 即使你的$scope.myName = 'tomy',directive内部接收到的也是myName这个字符串 // 你的$scope.myName = 'lucy', 那么directive内部接收到的就是lucy '=age'表示接受age属性,并且把它的值当作一个变量,实现穿透(继承),在当前作用域修改这个变量,会影响外部作用域对应的变量值,例如:// 外部
// directive内部
{
template: ‘xxx’,
scope: {
value: ‘=passValue’,
},
link($scope) {
$scope.onClick = function() {
KaTeX parse error: Expected 'EOF', got '}' at position 31: …e = 'lucy' }̲ }, } 当点击指令内的…scope.value = { name: ‘lucy’ },则不会产生任何结果,并且,这样passValue和
s
c
o
p
e
.
v
a
l
u
e
的
引
用
关
系
段
了
,
对
scope.value的引用关系段了,对
scope.value的引用关系段了,对scope.value的任何修改都不会再影响外部作用域都myValue,因此要特别注意这一点。
'&sing’表示接受sing属性,但是sing属性的值必须是一个函数,而且和=一样,它是一个继承的值。
'@'表示接受一个father属性,之所以在@后面省略内容,是因为这个属性值跟前面的father是一模一样的,而前面那些@name, =age,都可以不一样,比如可以是@my-name, =my-age,外部标签的属性名和内部$scope的属性名不同,但具有对应关系。
'@?'表示可以接受一个mother属性,也可以不传,?的意思就是可选。
如上所述,实际上,指令内部的$scope属性通过3个特殊的符号和外部作用域上的值进行绑定,你只需要分清楚@传进来的一定是字符串即可。
脏检查和双向绑定
我们经常听到angular的脏检查机制和数据的双向绑定,这两个词似乎已经是它的代名词了。那么从编程层面,这到底是什么鬼?
当 s c o p e 的 一 个 属 性 被 改 变 时 , 界 面 可 能 会 更 新 。 那 么 为 什 么 a n g u l a r 里 面 , 修 改 scope的一个属性被改变时,界面可能会更新。那么为什么angular里面,修改 scope的一个属性被改变时,界面可能会更新。那么为什么angular里面,修改scope上的一个属性,可以引起界面的变化呢?这是angular的数据响应机制决定的。在angular里面就是脏检查机制。而脏检查,和双向绑定离不开。
这里插句题外话,JavaScript里面非常有意思的一种接口,当你修改(或新增)一个对象的某个属性时,会触发该对象里面的setter。如果你对这块不是很了解,可以先学一下Object.defineProperty,包括这两年超级火的vuejs也是通过这个接口实现的。它是一个ES5的标准接口,据说IE8以下的浏览器都可能支持。
我们可以设计一种实现,当你修改或赋值 s c o p e 的 某 个 属 性 时 , 就 触 发 了 scope的某个属性时,就触发了 scope的某个属性时,就触发了scope这个js对象的setter,我们可以自定义这个setter,它是一个函数,在这个函数内部,调用某些逻辑去更新界面。同时,为了确保新塞进来的对象也可以被监听到变化,在你赋值时,还要把赋值进来的对象也进行改造,改造为可以被监听的对象。
双向绑定顾名思义是两个过程,一个是将 s c o p e 属 性 值 绑 定 到 H T M L 结 构 中 , 当 scope属性值绑定到HTML结构中,当 scope属性值绑定到HTML结构中,当scope属性值发生变化的时候界面也发生变化;另一个是,当用户在界面上进行操作,例如点击、输入、选择时,自动触发 s c o p e 属 性 的 变 化 ( 界 面 也 可 能 跟 着 变 ) 。 而 脏 检 查 的 作 用 是 “ 在 当 scope属性的变化(界面也可能跟着变)。而脏检查的作用是“在当 scope属性的变化(界面也可能跟着变)。而脏检查的作用是“在当scope属性值发生变化的时候促使界面发生变化”。
angular的数据响应机制
那么,在代码层面,angular是怎么做到监听数据变动然后更新界面的呢?答案是,angular根本不监听数据的变动,而是在恰当的时机从
r
o
o
t
S
c
o
p
e
开
始
遍
历
所
有
rootScope开始遍历所有
rootScope开始遍历所有scope,检查它们上面的属性值是否有变化,如果有变化,就用一个变量dirty记录为true,再次进行遍历,如此往复,直到某一个遍历完成时,这些$scope的属性值都没有变化时,结束遍历。由于使用了一个dirty变量作为记录,因此被称为脏检查机制。
这里面有三个问题:
“恰当的时机”是什么时候?
如何做到知道属性值是否有变化?
这个遍历循环是怎么实现的?
要解决这三个问题,我们需要深入了解angular的$watch, $apply, $digest。
w a t c h 绑 定 要 检 查 的 值 简 单 的 说 , 当 一 个 作 用 域 创 建 的 时 候 , a n g u l a r 会 去 解 析 模 板 中 当 前 作 用 域 下 的 模 板 结 构 , 并 且 自 动 将 那 些 插 值 ( 如 t e x t ) 或 调 用 ( 如 n g − m o d e l = " a g e " ) 找 出 来 , 并 利 用 watch绑定要检查的值 简单的说,当一个作用域创建的时候,angular会去解析模板中当前作用域下的模板结构,并且自动将那些插值(如{{text}})或调用(如ng-model="age")找出来,并利用 watch绑定要检查的值简单的说,当一个作用域创建的时候,angular会去解析模板中当前作用域下的模板结构,并且自动将那些插值(如text)或调用(如ng−model="age")找出来,并利用watch建立绑定,它的回调函数用于决定如果新值和旧值不同时(或相同时)要干什么事。当然,你也可以手动在脚本里面使用 s c o p e . scope. scope.watch对某个属性进行绑定。它的使用方法如下:
s
c
o
p
e
.
scope.
scope.watch(string|function, listener, objectEquality, prettyPrintExpression)
第一个参数是一个字符串或函数,如果是函数,需要运行后得到一个字符串,这个字符串用于确定将绑定$scope上的哪个属性。listener则是回调函数,表示当这个属性的值发生变化时,执行该函数。objectEquality是一个boolean,为true的时候,会对object进行深检查(懂什么叫深拷贝的话就懂深检查)。第四个参数是如何解析第一个参数的表达式,使用比较复杂,一般不传。
d i g e s t 遍 历 递 归 当 使 用 digest遍历递归 当使用 digest遍历递归当使用watch绑定了要检查的属性之后,当这个属性发生变化,就会执行回调函数。但是前面已经说过了,angular里面没有监听这么一说。它没有用object的setter机制(angular2之后改进并采用了这个机制),而是脏检查机制。脏检查的核心,就是 d i g e s t 循 环 。 当 用 户 执 行 了 某 些 操 作 之 后 , a n g u l a r 内 部 会 调 用 digest循环。当用户执行了某些操作之后,angular内部会调用 digest循环。当用户执行了某些操作之后,angular内部会调用digest(),最终导致界面重新渲染。那么它究竟是怎么一回事呢?
调用 w a t c h 之 后 , 对 应 的 信 息 被 绑 定 到 a n g u l a r 内 部 的 一 个 watch之后,对应的信息被绑定到angular内部的一个 watch之后,对应的信息被绑定到angular内部的一个 w a t c h e r s 中 , 它 是 一 个 队 列 ( 数 组 ) , 而 当 watchers中,它是一个队列(数组),而当 watchers中,它是一个队列(数组),而当digest被触发时,angular就会去遍历这个数组,并且用一个dirty变量记录$ w a t c h e r s 里 面 记 录 的 那 些 watchers里面记录的那些 watchers里面记录的那些scope属性是否有变化,当有变化的时候,dirty被设置为true,在$digest执行结束的时候,它会再检查dirty,如果dirty为true,它会再调用自己,直到dirty为true。但是为了防止死循环,angular规定,当递归发生了10次或以上时,直接抛出一个错误,并跳出循环。
递归流程如下:
判断dirty是否为true,如果为false,则不进行
d
i
g
e
s
t
递
归
。
(
d
i
r
t
y
默
认
为
t
r
u
e
)
遍
历
digest递归。(dirty默认为true) 遍历
digest递归。(dirty默认为true)遍历
w
a
t
c
h
e
r
s
,
取
出
对
应
的
属
性
值
的
老
值
和
新
值
根
据
o
b
j
e
c
t
E
q
u
a
l
i
t
y
进
行
新
老
值
的
对
比
。
如
果
两
个
值
不
同
,
则
继
续
往
下
执
行
。
如
果
两
个
值
相
同
,
则
设
置
d
i
r
t
y
为
f
a
l
s
e
,
跳
出
递
归
。
设
置
d
i
r
t
y
为
t
r
u
e
用
新
值
代
替
老
值
,
这
样
,
在
下
一
轮
递
归
的
时
候
,
老
值
就
是
这
一
轮
的
新
值
再
次
调
用
watchers,取出对应的属性值的老值和新值 根据objectEquality进行新老值的对比。 如果两个值不同,则继续往下执行。如果两个值相同,则设置dirty为false,跳出递归。 设置dirty为true 用新值代替老值,这样,在下一轮递归的时候,老值就是这一轮的新值 再次调用
watchers,取出对应的属性值的老值和新值根据objectEquality进行新老值的对比。如果两个值不同,则继续往下执行。如果两个值相同,则设置dirty为false,跳出递归。设置dirty为true用新值代替老值,这样,在下一轮递归的时候,老值就是这一轮的新值再次调用digest
当递归流程结束之后,$digest还要执行:
将变化后的 s c o p e 重 新 渲 染 到 界 面 当 一 个 作 用 域 创 建 完 之 后 , scope重新渲染到界面 当一个作用域创建完之后, scope重新渲染到界面当一个作用域创建完之后,scope. d i g e s t 会 被 运 行 一 次 。 d i r t y 的 默 认 值 被 设 定 为 t r u e , 因 此 , 如 果 你 在 c o n t r o l l e r 里 面 使 用 了 digest会被运行一次。dirty的默认值被设定为true,因此,如果你在controller里面使用了 digest会被运行一次。dirty的默认值被设定为true,因此,如果你在controller里面使用了watch,并且进行了属性赋值,往往刷新页面就可以看到 w a t c h 的 回 调 函 数 被 执 行 了 。 但 是 , 现 在 问 题 来 了 , 上 面 说 的 “ a n g u l a r 内 部 会 调 用 watch的回调函数被执行了。但是,现在问题来了,上面说的“angular内部会调用 watch的回调函数被执行了。但是,现在问题来了,上面说的“angular内部会调用digest()”,这个内部是怎么实现的?
a
p
p
l
y
触
发
apply触发
apply触发digest
在我们自己编程时,并不直接使用
d
i
g
e
s
t
,
而
是
调
用
digest,而是调用
digest,而是调用scope.
a
p
p
l
y
(
)
,
apply(),
apply(),apply内部会触发
d
i
g
e
s
t
递
归
遍
历
。
同
时
,
你
可
以
给
digest递归遍历。同时,你可以给
digest递归遍历。同时,你可以给apply传一个参数,是个函数,这个函数会在
d
i
g
e
s
t
开
始
之
前
执
行
。
现
在
回
到
上
面
的
问
题
,
a
n
g
u
l
a
r
内
部
怎
么
触
发
digest开始之前执行。现在回到上面的问题,angular内部怎么触发
digest开始之前执行。现在回到上面的问题,angular内部怎么触发digest?实际上,angular里面要求你通过ng-click, ng-modal, ng-keyup等来进行数据的双向绑定,为什么,因为这些angular的内部指令封装了
a
p
p
l
y
,
比
如
n
g
−
c
l
i
c
k
,
它
其
实
包
含
了
d
o
c
u
m
e
n
t
.
a
d
d
E
v
e
n
t
L
i
s
t
e
n
e
r
(
′
c
l
i
c
k
′
)
和
apply,比如ng-click,它其实包含了document.addEventListener('click')和
apply,比如ng−click,它其实包含了document.addEventListener(′click′)和scope.
a
p
p
l
y
(
)
两
个
过
程
。
所
谓
的
数
据
双
向
绑
定
,
其
实
也
就
是
n
g
−
i
n
p
u
t
和
apply()两个过程。所谓的数据双向绑定,其实也就是ng-input和
apply()两个过程。所谓的数据双向绑定,其实也就是ng−input和scope.$apply的同时运行。
当用户在模板里面使用ng-click时,如下:
手动调用 a p p l y 但 是 有 些 情 况 下 , 我 们 不 可 能 直 接 使 用 a n g u l a r 内 部 指 令 , 有 两 种 情 况 我 们 需 要 手 动 调 用 apply 但是有些情况下,我们不可能直接使用angular内部指令,有两种情况我们需要手动调用 apply但是有些情况下,我们不可能直接使用angular内部指令,有两种情况我们需要手动调用apply,一种是调用angular内置的语法糖,比如$http, t i m e o u t , 另 一 种 是 我 们 没 有 使 用 a n g u l a r 内 部 机 制 去 更 新 了 timeout,另一种是我们没有使用angular内部机制去更新了 timeout,另一种是我们没有使用angular内部机制去更新了scope,比如我们用$element.on(‘click’, () => s c o p e . n a m e = ′ l u c y ′ ) 。 也 就 是 说 “ 异 步 ” 和 “ 机 制 外 ” 修 改 scope.name = 'lucy')。也就是说“异步”和“机制外”修改 scope.name=′lucy′)。也就是说“异步”和“机制外”修改scope属性值之后,我们都要手动调用 a p p l y , 虽 然 我 们 在 调 用 apply,虽然我们在调用 apply,虽然我们在调用timeout的时候,没有手写 a p p l y , 但 实 际 上 它 内 部 确 实 调 用 了 apply,但实际上它内部确实调用了 apply,但实际上它内部确实调用了apply:
function($timeout) {
// 当我们通过on(‘click’)的方式触发某些更新的时候,可以这样做
$timeout(() => {
$scope.name = ‘lily’
})
// 也可以这样做
$element.on(‘click’, () => {
$scope.name = ‘david’
s
c
o
p
e
.
scope.
scope.apply()
})
}
但是,一定要注意,在递归过程中,绝对不能手动调用
a
p
p
l
y
,
比
如
在
n
g
−
c
l
i
c
k
的
函
数
中
,
比
如
在
apply,比如在ng-click的函数中,比如在
apply,比如在ng−click的函数中,比如在watch的回调函数中。
伪代码实现
通过上面的讲解,你可能已经对angular里面的脏检查已经了解了,但是我们还是希望更深入,用代码来把事情说清楚。我这里不去抄写angular的源码,而是自己写一段伪代码,这样更有助于理解整个机制。
import { isEqual } from ‘lodash’
class Scope {
constructor() {
this.
d
i
r
t
y
=
t
r
u
e
t
h
i
s
.
dirty = true this.
dirty=truethis.count = 0
this.KaTeX parse error: Expected 'EOF', got '}' at position 17: …atchers = [] }̲ $watch(prope…watchers.push(watcher)
}
KaTeX parse error: Expected '}', got 'EOF' at end of input: …{ if (this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …row new Error('digest超过10次’)
}
this.$$watchers.forEach(watcher => {
let newValue = eval('return this.' + watcher.property)
let oldValue = watcher.oldValue
if (watcher.deepEqual && isEqual(newValue, oldValue)) {
watcher.dirty = false
}
else if (newValue === oldValue) {
watcher.dirty = false
}
else {
watcher.dirty = true
eval('this.' + watcher.property + ' = ' newValue)
watcher.listener(newValue, oldValue) // 注意,listener是在newValue赋值给$scope之后执行的
watcher.oldValue = newValue
}
// 这里的实现和angular逻辑里面有一点不同,angular里面,当newValue和oldValue都为undefined时,listener会被调用,可能是angular里面在$watch的时候,会自动给$scope加上原本没有的属性,因此认为是一次变动
})
this.$$count ++
this.$$dirty = false
for (let watcher of this.$$watchers) {
if (watcher.dirty) {
this.$$dirty = true
break
}
}
if (this.$$dirty) {
this.$digest()
}
else {
this.$patch()
this.$$dirty = true
this.$$count = 0
}
}
KaTeX parse error: Expected '}', got 'EOF' at end of input: …{ if (this.KaTeX parse error: Expected '}', got 'EOF' at end of input: … return // 当digest执行的过程中,不能触发KaTeX parse error: Expected 'EOF', got '}' at position 11: apply }̲ this.
d
i
r
t
y
=
t
r
u
e
t
h
i
s
.
dirty = true this.
dirty=truethis.
c
o
u
n
t
=
0
t
h
i
s
.
count = 0 this.
count=0this.digest()
}
$patch() {
// 重绘界面
}
}
function ControllerRegister(controllerTemplate, controllerFunction) {
let $scope = new Scope()
$paser(controllerTemplate,
s
c
o
p
e
)
/
/
解
析
c
o
n
t
r
o
l
l
e
r
的
模
板
,
把
模
板
中
的
属
性
全
部
都
解
析
出
来
,
并
且
把
这
些
属
性
赋
值
给
scope) // 解析controller的模板,把模板中的属性全部都解析出来,并且把这些属性赋值给
scope)//解析controller的模板,把模板中的属性全部都解析出来,并且把这些属性赋值给scope
controllerFunction(
s
c
o
p
e
)
/
/
在
c
o
n
t
r
o
l
l
e
r
F
u
n
c
t
i
o
n
内
部
可
能
又
给
scope) // 在controllerFunction内部可能又给
scope)//在controllerFunction内部可能又给scope添加了一些属性,注意,不能在运行controllerFunction的时候调用
s
c
o
p
e
.
scope.
scope.apply()
let properties = Object.keys(
s
c
o
p
e
)
/
/
找
出
scope) // 找出
scope)//找出scope上的所有属性
// 要把
s
c
o
p
e
上
的
一
些
内
置
属
性
排
除
掉
p
r
o
p
e
r
t
i
e
s
=
p
r
o
p
e
r
t
i
e
s
.
f
i
l
t
e
r
(
i
t
e
m
=
>
i
t
e
m
.
i
n
d
e
x
O
f
(
′
scope上的一些内置属性排除掉 properties = properties.filter(item => item.indexOf('
scope上的一些内置属性排除掉properties=properties.filter(item=>item.indexOf(′’) !== 0) // 当然,这种排除方法只能保证在用户不使用$作为属性开头的时候有用
properties.forEach(property => {
s
c
o
p
e
.
scope.
scope.watch(property, () => {}, true)
})
s
c
o
p
e
.
scope.
scope.digest()
}
上面就是用伪代码实现了angular内部的机制,不能作为真实的引擎去使用,但是体现了整个脏检查的实现思路。
问题的解决
我们回到文章开头的问题,那段代码的问题究竟在哪里呢?通过本文的解释,你应该非常容易知道,代码中使用了on(‘click’)而没有调用$apply,正确的做法是使用ng-click。但是,出了这个问题,还有另外一个问题,就是作用域的继承问题。
在directiveB中,直接使用$scope.members = newValue,虽然看上去父级作用域里面的members应该也要等于新的值,但是我们从js的角度出发来看下:
directiveB.
s
c
o
p
e
.
m
e
m
b
e
r
s
=
c
o
n
t
r
o
l
l
e
r
A
.
scope.members = controllerA.
scope.members=controllerA.scope.members
directiveB.
s
c
o
p
e
.
m
e
m
b
e
r
s
=
n
e
w
M
e
m
b
e
r
s
c
o
n
t
r
o
l
l
e
r
A
.
scope.members = newMembers controllerA.
scope.members=newMemberscontrollerA.scope.members = ?
显然,这是一个超出预期的结果,一切都是js里面object是引用型数据的锅。那么要保证修改没问题,要怎么办呢?
directiveB.
s
c
o
p
e
.
m
e
m
b
e
r
s
=
c
o
n
t
r
o
l
l
e
r
A
.
scope.members = controllerA.
scope.members=controllerA.scope.members
for (let i = 0; i < newMembers.length; i ++) {
directiveB.KaTeX parse error: Expected 'EOF', got '}' at position 43: …newMembers[i]) }̲ controllerA.scope.members = ?
当使用数组的时候,要格外小心,因为数组是特殊的object,它比普通的object更加复杂。
好了,关于angular里面的一些用法就讲到这里。肯定有很多不足的地方,欢迎在下方留言和我交流。