Объясните принципы отзывчивости Vue простыми словами (полная версия)

JavaScript Vue.js

предисловие

Одной из самых уникальных особенностей Vue является его ненавязчивая реактивная система. Модели данных — это обычные объекты JavaScript. И когда вы их изменяете, представление обновляется. Это делает управление состоянием очень простым, но не менее важно понимать, как оно работает, чтобы избежать некоторых распространенных проблем. ---- Официальная документация Эта статья даст подробное введение в принцип отзывчивости и поможет вам реализовать базовую версию адаптивной системы. Пожалуйста, нажмите на код этой статьиБлог на гитхабе

что отвечает

Сначала рассмотрим пример:

<div id="app">
    <div>Price :¥{{ price }}</div>
    <div>Total:¥{{ price * quantity }}</div>
    <div>Taxes: ¥{{ totalPriceWithTax }}</div>
    <button @click="changePrice">改变价格</button>
</div>
var app = new Vue({
  el: '#app',
  data() {
    return {
      price: 5.0,
      quantity: 2
    };
  },
  computed: {
    totalPriceWithTax() {
      return this.price * this.quantity * 1.03;
    }
  },
  methods: {
    changePrice() {
      this.price = 10;
    }
  }
})

响应式.gif

В приведенном выше примере, когда цена меняется, Vue знает, что ему нужно сделать три вещи:

  • Обновить значение цены на странице
  • Рассчитать значение выражения цена*количество, обновить страницу
  • Вызовите функцию totalPriceWithTax, чтобы обновить страницу.

После изменения данных страница будет перерисована.Это отзывчивость Vue, так как же все это работает?

Для завершения этого процесса нам потребуется:

  • Обнаружение изменений в данных
  • На какие данные опирается представление коллекции
  • При изменении данных автоматически «уведомлять» ту часть представления, которую необходимо обновить, и обновлять ее.

Соответствующие профессиональные термины:

  • Перехват данных/брокерство данных
  • Коллекция зависимостей
  • модель публикации-подписки

Как обнаружить изменения в данных

В первую очередь возникает вопрос, как в Javascript обнаружить изменение объекта? На самом деле есть два способа обнаружить изменения: использоватьObject.definePropertyи ES6Proxy, что является перехватом данных или проксированием данных. Эта часть кода в основном относится к классу архитектуры Эвереста.

Метод 1. Реализация Object.defineProperty

Vue отслеживает изменения данных, устанавливая методы установки/получения свойств объекта, и собирает зависимости с помощью средств получения, и каждый метод установки является наблюдателем, который уведомляет подписчиков об обновлении представлений при изменении данных.

function render () {
  console.log('模拟视图渲染')
}
let data = {
  name: '浪里行舟',
  location: { x: 100, y: 100 }
}
observe(data)
function observe (obj) { // 我们来用它使对象变成可观察的
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    // 递归子属性
    observe(value)
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal) //如果赋值是一个对象,也要递归子属性
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render()
          value = newVal
        }
      }
    })
  }
}
data.location = {
  x: 1000,
  y: 1000
} //set {x: 1000,y: 1000} 模拟视图渲染
data.name // get 浪里行舟

Основная функция приведенного выше кода:observeЭта функция проходит черезobj(Объект, который нужно отслеживать на наличие изменений), путем обхода всех свойств передается каждое свойство объектаdefineReactiveОбработка, чтобы добиться обнаружения изменений объекта. Примечательно,observeВыполняется рекурсивный вызов. Итак, как мы обнаруживаем VuedataДанные на самом деле очень просты:

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}

Таким образом, пока мы создаем новый объект Vue, он будетdataИзменения отслеживаются в данных. Тем не менее, есть несколько замечаний по поводу этого подхода:

  • Невозможно обнаружить добавление или удаление свойств объекта(Такие какdata.location.a=1).

Это потому, что Vue проходитObject.definePropertyпреобразовать ключ объекта вgetter/setterформы для отслеживания изменений, ноgetter/setterОн может только отслеживать, были ли изменены данные, но не может отслеживать новые атрибуты и удаленные атрибуты. Если нужно удалить атрибут, мы можем использоватьvm.$deleteЕсли он реализован, что мне делать, если это новый атрибут? 1) можно использоватьVue.set(location, a, 1)метод добавления реактивных свойств к вложенным объектам; 2) Вы также можете переназначить этот объект, напримерdata.location = {...data.location,a:1}

  • Object.definePropertyВы не можете отслеживать изменения в массиве, вам нужно переписать метод массива, конкретный код выглядит следующим образом:
function render() {
  console.log('模拟视图渲染')
}
let obj = [1, 2, 3]
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
// 先获取到原来的原型上的方法
let arrayProto = Array.prototype
// 创建一个自己的原型 并且重写methods这些方法
let proto = Object.create(arrayProto)
methods.forEach(method => {
  proto[method] = function() {
    // AOP
    arrayProto[method].call(this, ...arguments)
    render()
  }
})
function observer(obj) {
  // 把所有的属性定义成set/get的方式
  if (Array.isArray(obj)) {
    obj.__proto__ = proto
    return
  }
  if (typeof obj == 'object') {
    for (let key in obj) {
      defineReactive(obj, key, obj[key])
    }
  }
}
function defineReactive(data, key, value) {
  observer(value)
  Object.defineProperty(data, key, {
    get() {
      return value
    },
    set(newValue) {
      observer(newValue)
      if (newValue !== value) {
        render()
        value = newValue
      }
    }
  })
}
observer(obj)
function $set(data, key, value) {
  defineReactive(data, key, value)
}
obj.push(123, 55)
console.log(obj) //[1, 2, 3, 123,  55]

Этот метод перезаписывает общие методы массива, а затем перезаписывает собственный метод массива.Переписанный метод массива должен быть перехвачен. Однако некоторые массивы не могут быть перехвачены при работе с Vue, и, конечно же, нет возможности ответить, например:

obj.length-- // 不支持数组的长度变化
obj[0]=1  // 修改数组中第一个元素,也无法侦测数组的变化

ES6 предоставляет возможность метапрограммирования, поэтому у него есть возможность перехвата.Vue3.0 может использовать прокси в ES6 как основной способ реализации прокси данных.

Способ 2. Реализация прокси

Proxy— это новая функция JavaScript 2015.ProxyПрокси для всего объекта, а не для свойства объекта, так отличается отObject.definePropertyдолжен перебирать каждое свойство объекта,ProxyВам нужно только сделать слой прокси, чтобы отслеживать все изменения атрибутов в рамках одной и той же структуры уровня.Конечно, для глубоких структур все еще необходимо выполнять рекурсию. также**ProxyПоддержка изменения массива прокси. **

function render() {
  console.log('模拟视图的更新')
}
let obj = {
  name: '前端工匠',
  age: { age: 100 },
  arr: [1, 2, 3]
}
let handler = {
  get(target, key) {
    // 如果取的值是对象就在对这个对象进行数据劫持
    if (typeof target[key] == 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    if (key === 'length') return true
    render()
    return Reflect.set(target, key, value)
  }
}

let proxy = new Proxy(obj, handler)
proxy.age.name = '浪里行舟' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化
console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
proxy.arr.length-- // 无效

Приведенный выше код не только упрощен, но и реализует набор кода, который применим как для обнаружения объектов, так и для обнаружения массивов. ноProxyСовместимость не очень!

Зачем собирать зависимости

Цель наблюдения за данными — уведомить те места, которые использовали данные, об изменении их свойств. Например, в первом примере данные о цене используются в шаблоне, и при их изменении отправляется уведомление в место, где они используются. Затем, если несколько экземпляров Vue совместно используют переменную, как в следующем примере:

let globalData = {
    text: '浪里行舟'
};
let test1 = new Vue({
    template:
        `<div>
            <span>{{text}}</span> 
        <div>`,
    data: globalData
});
let test2 = new Vue({
    template:
        `<div>
            <span>{{text}}</span> 
        <div>`,
    data: globalData
});

Если мы выполним следующий оператор:

globalData.text = '前端工匠';

На этом этапе нам нужно уведомить два экземпляра Vue test1 и test2, чтобы обновить представление.Только собирая зависимости, мы можем знать, какие места зависят от моих данных, и отправлять обновления при обновлении данных. Как реализован сбор зависимостей? Основная идея — «модель публикации и подписки на события». Далее мы сначала вводим две важные роли — подписчик Dep и наблюдатель Watcher, а затем объясняем, как собирать зависимости.

Отдел подписки

1. Зачем вводить Деп

Сбор зависимостей требует поиска места для хранения зависимостей для зависимостей, поэтому мы создали Dep, который используется для сбора зависимостей, удаления зависимостей и отправки сообщений зависимостям.

Итак, давайте сначала реализуем класс подписчика Dep, который используется для отделения набора зависимостей атрибутов и диспетчеризации операций обновления.Чтобы быть более конкретным, его основная функция заключается в хранении объекта-наблюдателя Watcher. мы можем поставитьНаблюдатель понимается как посредническая роль, уведомляющая об изменении данных, а затем уведомляющая другие места.

2. Простая реализация Dep

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }
    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

Приведенный выше код в основном делает две вещи:

  • Используйте метод addSub, чтобы добавить операцию подписки Watcher к текущему объекту Dep;
  • Используйте метод уведомления, чтобы уведомить все объекты Watcher в подпрограммах текущего объекта Dep, чтобы инициировать операцию обновления.

Поэтому вызывайте addSub, когда вам нужно собрать зависимости, и вызывайте уведомление, когда вам нужно отправлять обновления. Вызов также прост:

let dp = new Dep()
dp.addSub(() => {
    console.log('emit here')
})
dp.notify()

Наблюдатель Наблюдатель

1. Зачем вводить Watcher

Класс наблюдения определяется в Vue для представления зависимостей подписки на наблюдение. Что касается того, почему наблюдатель был введен, «Введение в Vue.js» дает хорошее объяснение:

При изменении атрибута нам нужно уведомить место использования данных, а мест использования данных много, и типы разные.Это может быть шаблон или часы, написанные пользователем.В это время , необходимо абстрагировать класс, который обрабатывает эти случаи централизованно. Затем мы собираем только экземпляры этого инкапсулированного класса на этапе сбора зависимостей и уведомляем только один из них, а затем он отвечает за уведомление других мест.

Целью сбора зависимостей является сохранение объекта Watcher наблюдателя в подпрограммах подписчика Dep в текущем закрытии.. Сформируйте такую ​​связь, как показано ниже (рисунок см. в разделе «Анализ внутреннего рабочего механизма Vue.js»).

image.png

2. Простая реализация Watcher

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}

Выше приведена простая реализация Watcher.При выполнении конструктораDep.targetУкажите на себя, чтобы соответствующий наблюдатель был собран, а соответствующий наблюдатель был удален при отправке обновления, а затем выполненupdateфункция.

Собрать зависимости

Так называемая зависимость на самом деле Watcher. Что касается того, как собирать зависимости, то это можно резюмировать в одном предложении, ** собирать зависимости в геттерах и запускать зависимости в сеттерах. **Сначала соберите зависимости, то есть соберите места, где используются данные, а затем инициируйте ранее собранный цикл зависимостей при изменении атрибутов.

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

Наконец, мы модифицировали функцию defineReactive и добавили код, связанный со сбором зависимостей и обновлениями распространения, в пользовательскую функцию, реализовав простой ответ данных.

function observe (obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    observe(value)  // 递归子属性
    let dp = new Dep() //新增
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
     // 将 Watcher 添加到订阅
       if (Dep.target) {
         dp.addSub(Dep.target) // 新增
       }
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal) //如果赋值是一个对象,也要递归子属性
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render()
          value = newVal
     // 执行 watcher 的 update 方法
          dp.notify() //新增
        }
      }
    })
  }
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

Когда функция рендеринга отрисовывается, чтение значения требуемого объекта вызовет функцию reactiveGetter для сбора текущего объекта Watcher (хранящегося в Dep.target) в класс Dep. Позже, если значение объекта будет изменено, будет запущен метод reactiveSetter, и класс Dep будет уведомлен о вызове notify для запуска метода обновления всех объектов Watcher для обновления соответствующих представлений.

Суммировать

Наконец, согласно следующему рисунку (см. «Введение в vue.js»), давайте рассмотрим весь процесс:

image.png

  • существуетnew Vue(), Vue позвонит _initИнициализация функции, то есть процесс init, в этом процессе Data преобразуется в форму getter/setter через Observer для отслеживания изменений в данных, когда set объект будет прочитан, он будет выполнен.getterфункция, которая выполняется при назначенииsetterфункция.
  • Когда функция рендеринга выполняется, поскольку значение требуемого объекта будет прочитано, будет запущена функция получения, чтобы добавить наблюдателя в зависимость для сбора зависимостей.
  • Когда значение объекта изменяется, соответствующийsetter,setterдо уведомленияКоллекция зависимостейКаждый наблюдатель в полученном Dep сообщает им, что их значение изменилось и представление необходимо перерендерить. В это время эти Наблюдатели начнут звонитьupdateобновить представление.

Статьи серии Vue CC Gate

Порекомендуйте полезный инструмент мониторинга ошибок для всехFundebug, добро пожаловать, чтобы попробовать это бесплатно!

Справочные статьи и книги