Angular 原理分析之 scope

Scope

  $scope.aProperty = 1
  $scope.$watch('aProperty', (newValue, oldValue) => {
    // ...
  })
  $scope.$apply(function () {
    $scope.bProperty = 1
  })
  $scope.$on('event', function () {})

作用:
- 监听数据的变化
- 广播并监听事件
- 绑定模板中的数据

其中“监听数据变化”是 Angular 中最具争议也是最大的卖点——脏检查。
将从以下几个功能点来介绍 scope 的实现。

  • 脏检查的核心实现,包含 $watch 和 $digest
  • 不同的触发 digest 循环的方法:$eval, $apply, $evalAsync, $applyAsync, $watchGroup
  • scope 继承机制
  • 脏检查对于数组、对象的优化
  • 事件系统:$on, $emit & $broadcast

scope 对象是一个普通 JS 对象。
js function Scope () {} var scope = new Scope()

脏检查的实现

$watch 与 $digest

  1. $watch 给 scope 绑定一个 watcher。watcher 用来通知 scope 数据变化。$watch 接收2个参数:
    • watch 函数:指定要监听的数据
    • listener 函数:当监听的数据发生变化时,被调用的函数
  2. $digest 遍历绑定在 scope 上的全部 watcher,并执行他们对应的 watch 和 listener

实现思路

定义一个 $$watchers 数组,每调用 $watch 一次,就在数组中添加一项;
调用 $digest 时,遍历数组,就执行对应的 listener 函数。

  // 定义部分
  function Scope() {
    this.$$watchers = []
  }
  Scope.prototype.$watch = function(watchFn, listenerFn) {
    var watcher = {
      watchFn,
      listenerFn
    }
    this.$$watchers.push(watcher)
  }
  Scope.prototype.$digest = function() {
    _.forEach(this.$$watchers, function(watcher) {
      watcher.listenerFn()
    })
  }
  // 使用部分
  var scope = new Scope()
  scope.$watch(
    function (scope) { return scope.someValue },
    function (newValue, oldValue, scope) { }
  )
  scope.$digest()

但是这个版本,没有判断 watch 的值是否发生变化了。我们应该在发生变化(脏)的情况下才执行 listener 函数。这就叫做脏检查

  /**
  * 1. 对每个 watcher,比较 watch 函数的返回值和之前存储的 last 属性。
  * 2. 如果值是不同的,就调用 listener 函数,传递新、旧值和 scope 对象本身。
  * 3. 最后把 last 属性设为新返回的值,以便在下次比较中使用。
  */
  Scope.prototype.$digest = function() {
    var self = this
    var newValue, oldValue
    _.forEach(this.$$watchers, function(watcher) {
      newValue = watcher.watchFn(self)
      oldValue = watcher.last
      if (newValue !== oldValue) {
        watcher.last = newValue
        watcher.listenerFn(newValue, oldValue, self)
      }
    })
  }

一点启示

  • 在 scope 上绑定属性是不会直接造成性能降低的。如果一个属性没有被 watcher 监听,它是否绑在 scope 上就不是很关键。Angular 不会遍历 scope 的属性,而是遍历所有的 watchers。
  • 每次调用 $digest 函数,所有的 watch 函数都会被调用。所以,我们应该关注 watch 函数的数量,以及每个 watch 函数或表达式的性能。

优化

  1. 【TIPS】对 last 属性赋初值。链接

      function initWatchVal() { }
      Scope.prototype.$watch = function(watchFn, listenerFn) {
        var watcher = {
          // ...
          last: initWatchVal
        }
      }
      Scope.prototype.$digest = function() {
        _.forEach(this.$$watchers, function(watcher) {
          // ...
          if (newValue !== oldValue) {
            watcher.last = newValue;
            watcher.listenerFn(newValue,
              (oldValue === initWatchVal ? newValue : oldValue),
              self);
          }
        })
      }
    

    理由:若不赋初值,默认值会是 undefined,当使用时如果第一个合法值也是 undefined。listener 函数就不会执行。

    做法:此处对 last 属性赋了一个空的函数。因为一个函数不会全等于除它自身外的任何值。(当 oldValue 是初始值函数时,listener 函数的第二个参数会传 newValue)

  2. 当一次 digest 循环结束,scope 仍然为“脏”的状态时,继续进行 digest 循环,直到所有 watch 值均不再更新。

    理由:如果某一 listener 函数修改了 scope 属性,并且另一个 watcher 监听着这个属性,那么就会得到错误的值。
    js Scope.prototype.$digest = function () { var dirty do { dirty = false _.forEach(this.$$watchers, function (watcher) { // get value... if (newValue !== oldValue) { // listenerFn... dirty = true } }) } while(dirty) } 
    启示:watch 函数应该尽量没有副作用。特别是不要在这里调用 Ajax 请求,因为无法确保 Ajax 请求会被发送几次。

  3. 当一次 $digest 导致 10 次脏检查的循环时,抛出异常

    理由:解决 watcher 循环依赖的问题。

      Scope.prototype.$digest = function () {
        var ttl = 10  // Time to Live
        do {
          // loop...
          if (dirty && !(ttl--)) {
            throw '10 $digest() iterations reached. Aborting!'
          }
        } while(dirty)
      }
    
  4. 【TIPS】记录一次循环中的最后一个 dirty watch,下次遍历到该 watch 时,如果没有发生变化,则不再继续向下遍历。(当新增 watch 时,需将记录清除)

      function Scope() {
        this.$$lastDirtyWatch = null
      }
      Scope.prototype.$digest = function () {
        this.$$lastDirtyWatch = null
        do {
          // loop...
          _.forEach(this.$$watchers, function(watcher) {
            if (newValue !== oldValue) {
              self.$$lastDirtyWatch = watcher
            } else if (self.$$lastDirtyWatch === watcher) {
              dirty = false
            }
          }
        } while(dirty)
      }
    
  5. watch 第三个参数如果传 TRUE,则进行深比较。(此时,对 last 属性进行赋值时,应采用深拷贝,否则无法发现改动)

      Scope.prototype.$watch = function(watchFn, listenerFn, objectEquality) {
        var watcher = {
          eq: !!objectEquality
        }
      }
    
  6. 判断 watch 的值是不是 NaN。

    理由:NaN 与自身不相等,将永远是“脏”的。

  7. 【TIPS】通过调用定义的 $watch 函数,来删除这个 watcher。实际就是从 watchers 数组中移除了这一项。

      Scope.prototype.$watch = function(watchFn, listenerFn, objectEquality) {
        var self = this
        // ...
        return function deregisterWatch() {
          var index = self.$$watchers.indexOf(watcher)
          if (index >= 0) {
            self.$$watchers.splice(index, 1)
          }
        }
      }
    

scope 方法:$eval, $apply, $evalAsync, $applyAsync, $watchGroup

$eval

$eval 函数提供了在 scope 上下文,执行 JS 代码的最简单的方法。

js Scope.prototype.$eval = function(expr, locals) { return expr(this, locals) }

$apply

$apply 函数利用 $eval 函数执行传入的函数,最后再触发一次 $digest 循环。

js Scope.prototype.$apply = function(expr) { try { return this.$eval(expr) } finally { this.$digest() } }

$apply 函数的目的是:我们可以运行一些 Angular 无法感知的却修改了 scope 中的属性的代码,以便 watcher 可以收集到数据的变化。

$evalAsync

$evalAsync 把一个函数排在正在执行的 digest 最后执行。(在每一次循环的最后执行,而不是每个 digest 的最后执行)

$applyAsync

$applyAsync 最初被设计处理 HTTP response。当请求密集时,可以做到合并 digest,避免资源浪费。
将多次 $apply 合并至一个队列中,在 digest 时统一执行。(通过 setTimeout、clearTimeout 实现)
【TIPS】当你预计你要在短时间调用多次 $apply 时,你可以用 $applyAsync 代替。

$watchGroup

Angular 1.3 加入 $watchGroup。针对多个 watch 指定一个 listener。
js // 基本原理 Scope.prototype.$watchGroup = function(watchFns, listenerFn) { var self = this _.forEach(watchFns, function(watchFn) { self.$watch(watchFn, function () { self.$evalAsync(listenerFn) }) }) }

scope inheritance - scope 等级机制,用来传递数据和事件

  // 这种方式是创建了一个 $rootScope。因为它没有 parent。
  var scope = new Scope()

Angular 的 scope 继承是基于 JavaScript 原生对象的继承实现的。
js Scope.prototype.$new = function () { var ChildScope = function () {} ChildScope.prototype = this var child = new ChildScope() return child }

当父级 digest 运行的时候,会运行子级的 watch,反之则不会。

  Scope.prototype.$new = function() {
    // prototype...
    this.$$children.push(child)
    child.$$watchers = []
    child.$$children = []
    return child
  }

$apply 会执行 $root.$digest,这会导致执行每个 scope 上的每个 watch。为了节约性能,可以用 $digest 代替 $apply。

$new(true) 创建一个隔离的 scope,此时无法访问父级 scope 的属性。此时直接 new Scope(),而不是 new ChildScope()。但 $root 依然指向实际的 $rootScope。

$new 第二个参数是 parent。指定一个特定的 scope,而不是原型链上的父级。
js Scope.prototype.$new = function(isolated, parent) { var child parent = parent || this if (isolated) { child = new Scope() child.$root = parent.$root } parent.$$children.push(child) return child }

$destory 函数删除全部的 watcher 并且从父级的 $$chiildren 删除 scope 自身。

Scope.prototype.$destroy = function() {
  if (this.$parent) {
    var siblings = this.$parent.$$children
    var indexOfThis = siblings.indexOf(this)
    if (indexOfThis >= 0) {
      siblings.splice(indexOfThis, 1)
    }
  }
  this.$$watchers = null;
}

efficient dirty-checking for collections (arrays & objects)

$watchCollection 用来发现数组和对象的变化。当一项或者一个属性被添加、删除或覆盖。是对 $watch 值检查的优化,它仅做浅层的比较。

$scope.users = [{name: "Joe", age: 30},{name: "Jill", age: 29},{name: "Bob", age: 31} ];

Reference Watches
✔ $scope.users = newUsers;
✘ $scope.users.push(newUser);
✘ $scope.users[0].age = 31;

Collection Watches
✔ $scope.users = newUsers;
✔ $scope.users.push(newUser);
✘ $scope.users[0].age = 31;

Equality Watches
✔ $scope.users = newUsers;
✔ $scope.users.push(newUser);
✔ $scope.users[0].age = 31;

event system: $on, $emit & $broadcast

Angular 的 pub-sub 实现,与 jQuery 等常见实现大致相同,唯一的不同是与 scope 层级绑定。
事件向上层级传递时,当前 scope 和祖先 scope 会得到通知,这个操作叫 emit
向下传递时,当前 scope 和子孙 scope 会得到通知,这个操作叫 broadcast

$on 定义的事件,第一个参数是事件名;第二个参数是 listener 函数。
$$listeners 对象中会保存所有的事件, key 是事件名,value 是 listener 数组。

  Scope.prototype.$on = function(eventName, listener) {
    var listeners = this.$$listeners[eventName]
    if (!listeners) {
      this.$$listeners[eventName] = listeners = []
    }
    listeners.push(listener)
  }
  Scope.prototype.$emit|$broadcast = function(eventName) {
    var listeners = this.$$listeners[eventName] || []
    _.forEach(listeners, function(listener) {
      listener()
    })
    return function() {
      var index = listeners.indexOf(listener)
      if (index >= 0) {
        listeners.splice(index, 1)
      }
    }
  }

stopPropagation

  Scope.prototype.$emit = function(eventName) {
    var propagationStopped = false
    var event = {
      stopPropagation: function() {
        propagationStopped = true
      }
    }
    do {
      // event...
    } while (scope && !propagationStopped)
    return event
  }

destory

  // usage
  child.$on('$destroy', listener)
  scope.$destroy()

  // define
  Scope.prototype.$destroy = function() {
    this.$broadcast('$destroy');
    // destory...
  }

猜你喜欢

转载自www.cnblogs.com/maopengyu/p/9190834.html