Принцип грязной проверки Angular и реализация псевдокода

внешний интерфейс JavaScript Angular.js

Мы часто слышим грязный механизм проверки angular и двустороннюю привязку данных, и эти два слова кажутся синонимами. Итак, с уровня программирования, что это за чертовщина?

Интерфейс может быть обновлен при изменении свойства $scope. Так почему же в angular изменение свойства $scope может привести к изменениям в интерфейсе? Это определяется механизмом ответа угловых данных. В Angular это механизм грязной проверки. Грязная проверка неотделима от двустороннего связывания.

Вот отступление, очень интересный интерфейс в JavaScript. При изменении (или добавлении) свойство объекта, установка в объекте будет запускается. Если вы не знаете много об этом, вы можете узнать его первымObject.defineProperty, включая суперпопулярный за последние два года vuejs, также реализуется через этот интерфейс. Это стандартный интерфейс ES5.

Мы можем разработать реализацию. Когда вы изменяете или назначаете свойство $scope, срабатывает установщик js-объекта $scope. Мы можем настроить этот установщик и вызвать некоторую логику внутри функции установки для обновления интерфейса. В то же время, чтобы убедиться, что вновь вставленный объект также можно отслеживать на предмет изменений, при присвоении значения вы также должны преобразовать присвоенный объект в объект, который можно отслеживать.

Двусторонняя привязка, как следует из названия, представляет собой два процесса, один из которых предназначен для привязки значения атрибута $scope к структуре HTML, а интерфейс изменяется при изменении значения атрибута $scope; другой — при изменении значения атрибута $scope. пользователь выполняет операции с интерфейсом, например, при щелчке, вводе и выборе автоматически запускается изменение свойства $scope (соответственно может измениться и интерфейс). Роль грязной проверки заключается в том, чтобы «заставить интерфейс измениться при изменении значения свойства $scope».

Механизм ответа данных Angular

Итак, на уровне кода, как меняются данные углового монитора, а затем обновляется интерфейс? Ответ заключается в том, что angular вообще не отслеживает изменения данных, а обходит все $scopes, начиная с $rootScope, в нужный момент, чтобы проверить, изменились ли значения свойств на них.Если есть изменение, используйте переменную dirty для запишите его как true, и снова Выполняется обход, и так далее, пока не завершится определенный обход, и обход завершается, когда значения свойств этих $scopes не изменились. Поскольку в качестве записи используется грязная переменная, она называется механизмом грязной проверки.

Здесь есть три вопроса:

  1. Когда "подходящее время"?
  2. Как узнать, изменилось ли значение свойства?
  3. Как реализован этот цикл обхода?

Чтобы решить эти три проблемы, нам нужно глубоко понять $watch, $apply, $digest в angular.

$watch связывает значение для проверки

Проще говоря, при создании области angular будет анализировать структуру шаблона в текущей области действия в шаблоне и автоматически вставлять эти значения (например, {{текст}}) или вызовы (например, ng-click="update " ), чтобы выяснить это, и использовать $watch для установки привязки, его функция обратного вызова используется, чтобы решить, что делать, если новое значение и старое значение отличаются (или совпадают). Конечно, вы также можете вручную использовать $scope.$watch для привязки свойства в скрипте. Он используется следующим образом:

$scope.$watch(string|function, listener, objectEquality, prettyPrintExpression)

Первый параметр — это строка или функция. Если это функция, она должна получить строку после запуска. Эта строка используется для определения того, какое свойство в $scope будет привязано. Слушатель — это функция обратного вызова, что означает, что при изменении значения этого свойства функция выполняется. objectEquality — логическое значение, когда оно равно true, выполняется глубокая проверка объекта (если вы знаете, что такое глубокая копия, вы знаете, как ее проверить). Четвертый параметр определяет, как анализировать выражение первого параметра, который сложнее в использовании и обычно не передается.

$digest пройти рекурсивно

При использовании $watch для привязки проверяемого свойства при изменении свойства будет выполняться функция обратного вызова. Но, как я уже говорил, в angular нет мониторинга, так как же его обратно вызвать? Он использует не механизм установки объекта, а механизм грязной проверки. Ядром грязной проверки является цикл $digest. Когда пользователь выполняет какое-либо действие, $digest() вызывается внутри angular, что в конечном итоге приводит к повторному рендерингу интерфейса. Так что же это такое?

После вызова $watch соответствующая информация привязывается к ?watchers внутри angular, который является очередью (массивом), и когда $digest срабатывает, angular будет проходить массив и записывать его с грязной переменной? , записанные в наблюдателях, изменились. Когда есть изменение, dirty устанавливается в true. Когда выполнение $digest заканчивается, оно снова проверяет грязный. Если грязный является истинным, он снова вызывает себя, пока грязный не станет истинным. Но чтобы предотвратить бесконечный цикл, Angular предусматривает, что когда рекурсия происходит 10 и более раз, сразу возникает ошибка и цикл прерывается.

Рекурсивный процесс выглядит следующим образом:

  1. Определите, является ли dirty истинным, если ложным, не выполняйте рекурсию $digest. (грязные значения по умолчанию равны истине)
  2. Traverse — наблюдатели, вынимают старое значение и новое значение соответствующего значения атрибута
  3. Сравните старые и новые значения по objectEquality.
  4. Если два значения отличаются, продолжайте выполнение. Если два значения совпадают, установите для dirty значение false.
  5. После проверки всех наблюдателей, если dirty все еще верно (для этого нужно прочитать мой псевдокод ниже)
  6. установить грязное в истинное
  7. Замените старое значение новым значением, чтобы в следующем раунде рекурсии старое значение было новым значением этого раунда.
  8. снова вызвать $digest

Когда рекурсивный процесс завершается, $digest также выполняется:

  1. Повторно визуализировать измененный $scope в интерфейсе

После создания области $scope.$digest запускается один раз. По умолчанию для dirty установлено значение true, поэтому, если вы используете $watch в контроллере и назначаете свойства, вы часто можете увидеть, как функция обратного вызова $watch выполняется при обновлении страницы. Однако теперь возникает вопрос: вышеупомянутый «angular вызовет $digest()» внутри, как это реализовано внутри?

$apply запускает $digest

В нашем собственном программировании мы не используем $digest напрямую, а вызываем $scope.$apply(), что запускает рекурсивный обход $digest внутри $apply. В то же время вы можете передать аргумент $apply, функции, которая будет выполняться до запуска $digest. Теперь вернемся к предыдущему вопросу: как angular запускает $digest внутри? На самом деле angular требует, чтобы вы выполняли двустороннюю привязку данных через ng-click, ng-modal, ng-keyup и т. д. Зачем, потому что эти внутренние директивы angular инкапсулируют $apply, например ng-click, который на самом деле содержит документ .addEventListener('щелчок') и $scope.$apply().

Когда пользователь использует ng-click внутри шаблона, следующим образом:

<div ng-click="update()">change</div>
$scope.update = function() {
  $scope.name = 'tom'
}

На самом деле, когда пользователь щелкает, $scope.$apply() также выполняется внутри angular, что запускает рекурсию обхода $digest и, наконец, вызывает перерисовку интерфейса.

Ручной вызов $apply

Но в некоторых случаях мы не можем напрямую использовать внутренние директивы angular.Есть два случая, когда нам нужно вызвать $apply вручную, один — вызвать встроенный синтаксический сахар angular, такой как $http, $timeout, другой заключается в том, что мы не используем механизм angular Internal для обновления $scope, например, мы используем $element.on('click', () => $scope.name = 'lucy'). То есть, после изменения значения атрибута $scope «асинхронно» и «вне механизма», мы должны вручную вызвать $apply.Хотя мы не написали $apply вручную при вызове $timeout, он фактически вызывает $ применять внутрь. :

function($timeout) {
  // 当我们通过on('click')的方式触发某些更新的时候,可以这样做
  $timeout(() => {
    $scope.name = 'lily'
  })
  // 也可以这样做
  $element.on('click', () => {
    $scope.name = 'david'
    $scope.$apply()
  })
}

Однако следует отметить, что $apply нельзя вызывать вручную во время рекурсивного процесса, например, в функции ng-click, например, в функции обратного вызова $watch.

Реализация псевдокода

Благодаря приведенному выше объяснению вы, возможно, уже имеете представление о грязной проверке в angular, но мы все еще надеемся углубиться и использовать код, чтобы прояснить ситуацию. Я не буду копировать сюда исходный код angular, а сам напишу псевдокод, который более полезен для понимания всего механизма.

import { isEqual } from 'lodash'

class Scope {
  constructor() {
    this.$$dirty = true
    this.$$count = 0
    this.$$watchers = []
  }
  $watch(property, listener, deepEqual) {
    let watcher = {
      property,
      listener,
      deepEqual,
    }
    this.$$watchers.push(watcher)
  }
  $digest() {
    if (this.$$count >= 10) {
      throw 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
    }
  }
  $apply() {
    if (this.$$count) {
      return // 当$digest执行的过程中,不能触发$apply
    }
    this.$$dirty = true
    this.$$count = 0
    this.$digest()
  }
  $patch() {
    // 重绘界面
  }
}
function ControllerRegister(controllerTemplate, controllerFunction) {
  let $scope = new Scope()
  $paser(controllerTemplate, $scope) // 解析controller的模板,把模板中的属性全部都解析出来,并且把这些属性赋值给$scope
  controllerFunction($scope) // 在controllerFunction内部可能又给$scope添加了一些属性,注意,不能在运行controllerFunction的时候调用$scope.$apply()

  let properties = Object.keys($scope) // 找出$scope上的所有属性
  // 要把$scope上的一些内置属性排除掉  
  properties = properties.filter(item => item.indexOf('$') !== 0) // 当然,这种排除方法只能保证在用户不使用$作为属性开头的时候有用

  properties.forEach(property => {
    $scope.$watch(property, () => {}, true)
  })

  $scope.$digest()
}

Вышесказанное заключается в использовании псевдокода для реализации внутреннего механизма angular, который не может использоваться как настоящий движок, но отражает идею реализации всей грязной проверки.


Статья из моего блога: https://www.tangshuang.net/5435.html