Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

angular的数据驱动与浏览器的 event loop #27

Open
fnjoe opened this issue Aug 11, 2016 · 4 comments
Open

angular的数据驱动与浏览器的 event loop #27

fnjoe opened this issue Aug 11, 2016 · 4 comments

Comments

@fnjoe
Copy link

fnjoe commented Aug 11, 2016

一、开始

how Angular interacts with the browser's event loop?.

  • Angular 如何与浏览器的 event loop 相互作用?
  • 再说,为什么要与浏览器的 event loop 相互作用?

image
Integration with the browser event loop

1.wait(等待触发 )

The browser's event-loop waits for an event to arrive. An event is a user interaction, timer event, or network event (response from a server).

上图的左侧上部分,浏览器的事件机制等待事件的触发来响应操作。
这里的事件包含用户的交互操作(click,mousemove等)、js内部通过浏览器的定时操作(setInterval,setTimeout)以及与服务器的交互(ajax);

2.emits(事件触发)&& callback executes(执行回调)

The event's callback gets executed. This enters the JavaScript context. The callback can modify the DOM structure.

此时浏览器的事件机制被触发,注册在对应事件上的listener被执行,此时进入js的上下文执行环境,执行我们预设的操作。在angular的项目中,会在此时通过调用$apply(fn)方法进入自身的数据检查模式$digest,就是图中的右侧部分。

这里的fn通常是一些会引起模型变化的动作,如:DOM事件,XHR响应回调,浏览器location变化,计时器的回调等,这也就是angular为何要与浏览器的event loop相互作用的原因。

$apply方法会先执行fn,然后进入$rootScope的脏检查来保证数据的一致性。

Only operations which are applied in the Angular execution context will benefit(受益) from Angular data-binding(数据绑定), exception handling, property watching, etc...

通常ng已经将上述浏览器的动作加入了$apply中来实现数据绑定,这就是我们并没有明确的在代码中调用$apply却实现了数据绑定的原因,当然,你也可以手动调用$apply来明确的引起一轮angular的数据检查。

现在我们进入了$digest loop,如上图所示,这里面又内涵了两个小的loop。

$evalAsyncQueue loop

The $evalAsync queue is used to schedule work(安排任务) which needs to occur outside of current stack frame, but before the browser's view render. This is usually done with setTimeout(0), but the setTimeout(0) approach suffers from slowness and may cause view flickering since the browser renders the view after each event.

目前只理解到这个队列中的任务会在$digest期间确保执行,下文提到的$evalAsync方法正是这个队列的入口。
当我们进行了一些操作,并引起了数据的变化,需要借助$digest来同步数据,如果此时已经存在一个$digest,我们便可以使用$evalAsync方法来该操作添加到已经存在的循环中的evalAsyncQueue,达到同步数据的目的。

$watchList loop

The $watch list is a set of expressions which may have changed since last iteration. If a change is detected then the $watch function is called which typically updates the DOM with the new value.

$watch list 是一系列表达式的集合,ng中将模型展示在view中的方式便是对模型的值添加一些watch,如ng-model、{{}}插值表达式,以及指令的赋值等,也包括在js中在scope上调用$watch方法来实现数据的监听,这些所有便构成了$watch list。

$digest done && before DOM render(脏检查结束,DOM渲染之前)

The $digest loop keeps iterating until the model stabilizes(模型稳定), which means that the $evalAsync queue is empty and the $watch list does not detect any changes.

$digest loop终止的条件便是$evalAsync queue为空和$watch不再变化,至此我们便可以重新渲染DOM。

3.browser re-rendering(DOM重新渲染)

至此,数据的改变便驱动了view层的变化。

二、深入

1.ng如何根据作用域解析并执行表达式,或执行函数

表达式的解析与执行,函数的执行必须与作用域联系起来,并且该动作可能会引起数据的变化。

$eval

Executes the expression on the current scope and returns the result.

在当前作用域中执行表达式并返回结果。
考虑到执行的动作可能会改变数据,所以需要配合别的动作来进入digest循环,来确保数据的一致性。

$evalAsync

Executes the expression on the current scope at a later point in time.
“稍后但很及时的”在当前作用域中执行表达式,ng在这个动作中整合了保证数据一致性的动作。

  1. 判断当前是否处在一个脏检查中,如果存在,就会将该表达式添加到evalAsyncQueue这个对列中,所以,这个动作便会整合进入已经存在的脏检查,被执行并保证了数据的一致。
  2. 如果不存在,ng会该表达式添加到evalAsyncQueue这个对列中,并主动发起$rootScope.$digest()来确保数据的一致性。

2.ng中如何检查数据。

$digest是数据绑定的核心,可分为

  1. 声明当前处于一个digest状态中。
  2. 当$rootScope.$digest()时,判断applyAsyncId是否存在,如果存在,取消对应的$apply动作,并且执行flushApplyAsync。这个步骤的用意是,当多个$apply的请求存在时,ng会在这个时间点来取消注册了的$apply,flushApplyAsync会执行所有的applyAsyncQueue队列动作,然后统一开始$digest循环。
  3. 进行evalAsyncQueue队列的遍历并执行,执行完后重置evalAsyncQueue。这个步骤的用意是,当回引起数据变化的动作存在时,依次执行完所有动作,再统一去遍历数据。
  4. 进行当前作用域中$watch list的遍历,比较新旧值,判断是否执行$watch list队列中的watcher。并且这个遍历是深度优先遍历。
  5. 根据遍历结果(dirty)与evalAsyncQueue是否为空来确定是否进入下一次循环。
  6. 最后,清除当前digest状态的声明。
  7. 执行postDigestQueue队列中的内容。

3.与浏览器eventLoop的结合。

$apply用于响应eventloop的事件,并进入$rootScope.$digest()

  1. 声明当前处于一个$apply的循环状态。
  2. 在当前作用域上解析并执行fn。
  3. 最终主动发起$rootScope.$digest()

$applyAsync用于处理多个$apply请求。

  1. 如果参数存在,将执行该参数的动作添加到applyAsyncQueue这队列中,此时并没有执行该动作。
  2. 然后判断,如果applyAsyncId不存在,异步的发起$rootScope.$apply(flushApplyAsync),并给applyAsyncId赋值来注册$apply的状态。

关于$applyAsync方法以及applyAsyncQueue队列的用意,目前没有很好的理解,$digest循环中也会进行applyAsyncId的判断以及applyAsyncQueue队列的flush(冲刷);

三、总结

MVVM模式的核心在于数据驱动,所以,一切对应用状态的操作的出发点应该是操作数据,再通过框架对数据的处理,最后映射到应用的状态,就是页面中的DOM状态。而这个处理数据的核心操作便是$digest循环。
在项目中使用angular作为框架时,能整理出驱动整个应用状态流转的数据变化,那么,与angular的合作便很顺利了。

@hjzheng
Copy link
Member

hjzheng commented Aug 11, 2016

@kuitos 关于这个 $applyAsync 一直没有弄清楚怎么用,存在的意义。

@kuitos
Copy link

kuitos commented Aug 11, 2016

$applyAsync 跟 $evalAsync 对于上层框架使用者而言,基本上就是用来做性能优化的。

$applyAsync 就是把可能会多个触发的digest检查合并成一个,提升性能。angular框架里面用来优化http性能,做法就是:

$httpProvider.useApplyAsync(true);

基本上是用于并发的异步场景优化。
除了多个 http 请求场景,其他场景我只想到一个,比如页面上存在多个定时器 timer,直接 调用 $timeout 会每个定时触发一个digest(不一定都会执行)

$timeout(() => {
    this.aTimer++;
}, 1000);

$timeout(() => {
    this.bTimer++;
}, 1000);

可以通过 $applyAsync 优化

setTimeout(() => {
    $scope.$applyAsync(() => {
        this.aTimer++;
    });
}, 1000);

setTimeout(() => {
    $scope.$applyAsync(() => {
        this.bTimer++;
    });
}, 1000);

对开发者而言,这两个 api 的使用频率都比较低。

@kuitos
Copy link

kuitos commented Aug 11, 2016

西安的这个problem-collection很棒啊,fork到shuyunff2e吧,已经给你开了 owner 权限 @hjzheng

@zonebond
Copy link

zonebond commented Sep 6, 2017

Good!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants