Сравнение основного кода адаптивной реализации Vue2.x и Vue3

Vue.js
Сравнение основного кода адаптивной реализации Vue2.x и Vue3

Object.defineProperty реализует отзывчивый

Сначала нужно знать, чтоObject.definePropertyможет только слушать объекты, и этот объектнетотносится к типам объектов (массивы также являются типами объектов), ноObjectОбъект-конструктор, т.е.{}.

структураДелится на три части:

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

слушающий объект

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

//设置简单的函数表示视图更新
function updateView() {
  console.log("视图更新")
}

// 分解对象属性函数
function observer(target) {
  if (typeof target !== "object" || target === null) {
    // 不是对象或数组
    return target
  }

  // 监听target每一个属性
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  // 深度监听:如果value是对象就继续分解
  observer(value)

  // 核心API:Object.defineProperty()
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        // 设置新值也要监听是否是对象和数组
        observer(newValue)
        // 设置新值
        // 注意value一直在闭包中,此处设置完后再get时也是会获取到value的值
        value = newValue

        // 触发更新视图
        updateView()
      }
    },
  })
}

из-заJavaScriptпределы,VueДанные в нем не могут динамически добавлять реактивные свойства корневого уровня. Другими словами, все реактивные свойства корневого уровня должны быть объявлены до инициализации экземпляра, даже если это просто пустое значение, иначе обновление представления не будет запущено, даже если значение изменилось.

大家可以通过上面的代码运行一下。 👆

слушать массив

Чтобы иметь возможность прослушивать массив, Vue2 переписывает некоторые методы массива для реализации обновлений представления, но эта часть иObject.definePropertyЭто не имеет значения.

👇Наверное реализовано так:

//设置简单的函数表示视图更新
function updateView() {
  console.log("视图更新")
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
//这样新增方法也不会影响到Array原型
const arrPrototype = Object.create(oldArrayProperty)

// 假设添加了这些方法,那么如果数组调用这些方法就会触发视图更新
;["push", "pop", "unshift", "shift", "splice"].forEach(
  (method) =>
    (arrPrototype[method] = function () {
      // 如果调用以上的方法就触发视图更新
      updateView()
      oldArrayProperty[method].call(this, ...arguments)
    })
)

// 分解对象属性函数
function observer(target) {
  if (typeof target !== "object" || target === null) {
    // 不是对象或数组
    return target
  }

  // 为了不污染全局Array原型:需要重新定义数组原型
  if (Array.isArray(target)) {
    target.__proto__ = arrPrototype
  }

  // 重新定义各个属性
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

потому чтоObject.definePropertyРабота с массивами в Vue2 очень ограничена.

Vue не может обнаружить изменения в следующих массивах:

  1. Когда вы напрямую устанавливаете элемент массива по индексу, например:vm.items[indexOfItem] = newValue
  2. Когда вы изменяете длину массива, например:vm.items.length = newLength

Официальная рекомендация - лучшаяspliceМетод добавляет и удаляет массив, поскольку он перезаписывается при внутренней перезаписи метода.spliceметод. или использоватьVue.set()|vm.$set()Принудительно перевести данные в реактивный режим.

Proxy и Reflect реализуют отзывчивость

Я должен поговорить об этом, прежде чем вставить кодProxy, похоже, что многие люди правыProxyнеправильно понятый,ProxyИспользуется только синтаксис ES6 вместо синтаксиса ES5.Object.definePropertyсинтаксический сахар, эта идея абсолютно неверна.Proxyа такжеObject.definePropertyдваполностью отличаетсяс вещами. такObject.definePropertyи не может заменитьProxy.

Proxy

Прокси можно понимать так: уровень «перехвата» устанавливается перед целевым объектом, и доступ к объекту из внешнего мира должен сначала пройти через этот уровень перехвата, поэтому он обеспечивает механизм фильтрации и перезаписи доступа. во внешний мир. Первоначальное значение слова Proxy — прокси, и здесь оно используется для обозначения того, что оно используется для «прокси» некоторых операций, что можно перевести как «прокси».

ES6 изначально предоставляет конструктор Proxy для создания экземпляров Proxy.

Давайте начнем с кода, чтобы увидеть основную операцию👇

var arr = [1,2]

var arrProxy = new Proxy(
  arr,
  {
    get(target, propKey) {
      console.log(`getting ${propKey}!`)
    },
    set(target, propKey, value) {
      console.log(`setting ${propKey}!`)
    },
  }
)

//设置值
arrProxy[0] = 'change'   //setting 0!
//读取值
arrProxy[1]  //getting 1!

Код выше правильныйarrМассив устанавливает слой перехвата и переопределяет чтение свойств (get) и настройки (set)поведение.

как конструктор,ProxyПринимает два параметра:

  • Первый параметр — это целевой объект для проксирования (вышеприведенный пример — этоarrобъекта), т.е. если нетProxyВмешательство операции, исходная операция доступа - этоarrобъект. объект здесьдаСсылается на тип объекта (массивы также являются типами объектов).
  • Второй параметр — объект конфигурацииhandler, для каждой операции прокси необходимо предоставить соответствующую функцию-обработчик, которая будет перехватывать соответствующую операцию. Например, в приведенном выше коде объект конфигурации имеетgetметод для перехвата запросов доступа к свойствам целевого объекта.getДвумя параметрами метода являются целевой объект и свойство, к которому необходимо получить доступ.

Примечание. Чтобы прокси работал, вы должны работать с экземпляром прокси (объект arrProxy в приведенном выше примере), а не с целевым объектом (объект arr в приведенном выше примере).

Ниже приведен список операций перехвата, поддерживаемых прокси, всего 13 типов.

  • get(target, propKey, receiver): перехватывать чтение свойств объекта, таких какproxy.fooа такжеproxy['foo'].
  • set(target, propKey, value, receiver): перехватить настройку свойств объекта, таких какproxy.foo = vилиproxy['foo'] = v, который возвращает логическое значение.
  • has(target, propKey): перехватpropKey in proxyОперация, возвращающая логическое значение.
  • deleteProperty(target, propKey): перехватdelete proxy[propKey]Операция, возвращающая логическое значение.
  • ownKeys(target): перехватObject.getOwnPropertyNames(proxy),Object.getOwnPropertySymbols(proxy),Object.keys(proxy),for...inЦикл, возврат массива. Этот метод возвращает имена свойств всех собственных свойств целевого объекта, иObject.keys()Возвращаемый результат включает только проходимые свойства самого целевого объекта.
  • getOwnPropertyDescriptor(target, propKey): перехватObject.getOwnPropertyDescriptor(proxy, propKey), который возвращает объект описания для свойства.
  • defineProperty(target, propKey, propDesc): перехватObject.defineProperty(proxy, propKey, propDesc),Object.defineProperties(proxy, propDescs), который возвращает логическое значение.
  • preventExtensions(target): перехватObject.preventExtensions(proxy), который возвращает логическое значение.
  • getPrototypeOf(target): перехватObject.getPrototypeOf(proxy), который возвращает объект.
  • isExtensible(target): перехватObject.isExtensible(proxy), который возвращает логическое значение.
  • setPrototypeOf(target, proto): перехватObject.setPrototypeOf(proxy, proto), который возвращает логическое значение. Если целевой объект является функцией, есть две дополнительные операции, которые можно перехватить.
  • apply(target, object, args): Перехватывать операцию экземпляра Proxy как вызов функции, напримерproxy(...args),proxy.call(object, ...args),proxy.apply(...).
  • construct(target, args): перехватывать операцию экземпляра Proxy как вызов конструктора, напримерnew proxy(...args).

Как можно заметитьProxyне только можетObject.definePropertiesфункция, и другие операции также могут быть перехвачены.

Я обеспокоенProxyСодержание в основном является ссылкойУчебник Ruan Yifeng по ES6, это очень хорошо, каждый может посмотреть.

Reflect

законченныйProxyдолжен сказатьReflectЭто новый API ES6.Reflectобъект иProxyОбъекты также используются для управления объектами, ноReflectЦель, для которой создается объект, имеет большое значение.

  1. БудуObjectМетоды объекта, которые явно являются внутренними для языка (например,Object.defineProperty), вставитьReflectна объекте. На этом этапе некоторые методыObjectа такжеReflectобъект, будущие новые методы будут развернуты только наReflectна объекте. То есть изReflectОбъекты имеют доступ к внутренним методам языка. Преимущество этого в том, чтоObjectкласс более чистый,JavaScriptбольше похоже на язык,ObjectБольше похоже на класс, а не на набор методовObjectна вилке.

  2. изменить некоторыеObjectВозвращаемый результат метода делает его более разумным. Например,Object.defineProperty(obj, name, desc)Ошибка выдается, когда свойство не может быть определено, иReflect.defineProperty(obj, name, desc)вернусьfalse.

    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }
    
  3. ПозволятьObjectОперации становятся функциональным поведением. немногоObjectОперации обязательны, напримерname in objа такжеdelete obj[name],а такжеReflect.has(obj, name)а такжеReflect.deleteProperty(obj, name)Сделайте их функциональным поведением.

    // 老写法
    'assign' in Object // true
    
    // 新写法
    Reflect.has(Object, 'assign') // true
    
  4. (основной)Reflectметоды объекта иProxyМетоды объекта соответствуют один к одному, пока онProxyметоды объекта, вы можетеReflectНайдите соответствующий метод на объекте. Это делаетProxyОбъект может легко вызвать соответствующийReflectметод, который завершает поведение по умолчанию, в качестве основы для изменения поведения. То есть независимо отProxyКак изменить поведение по умолчанию, вы всегда можетеReflectчтобы получить поведение по умолчанию.

ReflectОбъект имеет в общей сложности 13 статических методов.

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

Эффекты вышеперечисленных методов в основном связаны сObjectМетод объекта с тем же именем имеет тот же эффект, и он такой же, какProxyМетод объектапереписка один на одиниз.

Прежде чем мы перепишемproxyпример 🌰, плюсReflect:

var arr = [1,2]

var arrProxy = new Proxy(
  arr,
  {
    get(target, propKey, receiver) {
      console.log(`getting ${propKey}!`)
      return Reflect.get(target, propKey, receiver)
    },
    set(target, propKey, value, receiver) {
      console.log(`setting ${propKey}!`)
      return Reflect.set(target, propKey, receiver)
    },
  }
)

//设置值
arrProxy[0] = 'change'   //setting 0! change
//读取值
arrProxy[1]  //getting 1! 2

В приведенном выше коде каждыйProxyОперация перехвата объекта (get,set), внутренне вызывает соответствующийReflectметод, чтобы убедиться, что собственное поведение может выполняться нормально.ReflectсуществуетProxyЕще одна важная причина для вызоваrecevierпараметр.

Приходите, посмотрите 🌰, чтобы проиллюстрироватьrecevierФункция параметра👇

let p = {
  a: "a",
}

let handler = {
  set(target, key, value, receiver) {
    console.log("set")
    Reflect.set(target, key, value, receiver)
  },
  defineProperty(target, key, attribute) {
    console.log("defineProperty")
    Reflect.defineProperty(target, key, attribute)
  },
}

let obj = new Proxy(p, handler)
obj.a = "A"
// set
// defineProperty

В приведенном выше кодеProxy.setиспользуется для перехватаReflect.set, и прошел вreceiver, вызывая триггерProxy.definePropertyперехват.

Это потому чтоProxy.setизreceiverПараметр всегда указывает на текущийProxyэкземпляр (то естьobj),а такжеReflect.setОднажды прошел вreceiver, свойство будет присвоеноreceiverвыше (т.е.obj), вызывая триггерdefinePropertyперехват. еслиReflect.setнет входящихreceiver, то не сработаетdefinePropertyперехват.

такreceiverЭффект, чтобы позволитьProxyОбъектные операции в нем все указывают на текущийProxyнапример, чтобы вы могли перехватитьвсеОперации над экземплярами. (ну очень строго и безупречно :+1:

Прокси против Object.defineProperty

Увидев это, я думаю, все понялиObject.definePropertiesа такжеProxyразница. Но я собираюсь сказать еще одно словоProxyнетObject.definePropertiesсинтаксический сахар! ! ! !

babelпройти через@babel/polyfill(corejsа такжеre-generator) переводит синтаксис ES6 в синтаксис ES5, поддерживаемый большинством браузеров. Принцип просто в том, что некоторые функции ES6 могут быть заменены ES5, но писать на ES5 будет громоздко или неинтуитивно.

Например, давайте посмотрим на кусок кода👇:

// ES6定义类
class Person{
  constructor(name){
    this.name = name
  }
  sayName(){
    console.log(this.name)
  }
}

Чтобы этот метод определения класса распознавался большинством браузеров, мы обычно используемbabelПеревести его:

// 通过babel的polyfill后
function Person(name){
  this.name = name
}
Person.prototype.sayName = function(){
  console.log(this.name)
}

Перенесенный код ES5 имеет те же функции, что и код предыдущей версии ES6. Мы действительно можем сказатьclassСпособ определения класса заключается вfunctionСинтаксический сахар для определения классов (конструкторов).

ноProxyне могу пройтиbabelТранспилируется, потому что в ES5 абсолютно нет синтаксиса для имитацииProxyхарактеристики. следовательноVue3.xЭта версия не может работать с некоторыми браузерами более ранних версий.

реактивный код

function reactive(target = {}) {
  if (typeof target !== "object" || target == null) {
    return target
  }

  // 代理配置
  const proxyConf = {
    get(target, key, receiver) {
      //只监听对象本身(非原型)属性
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        //如果是本身的属性就监听,如果是对象原型的属性就不监听
        console.log("get", key)
      }
        
      const result = Reflect.get(target, key, receiver)
      //(惰性)深度监听-->提升性能
      return reactive(result)
    },
    set(target, key, val, receiver) {
      // 重复的数据不处理
      if (val === target[key]) {
        return true
      }

      // 监听是否是新增的key
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log("已有的key", key)
      } else {
        console.log("新增的key", key)
      }

      const result = Reflect.set(target, key, val, receiver)
      console.log("set", key, val)
      return result //通过return的值可以看出是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log("delete property", key)
      return result //是否删除成功
    },
  }

  // 生成代理对象
  const observed = new Proxy(target, proxyConf)
  return observed
}

Как шаг за шагом строится отзывчивая логика Vue3, я поместил в другой пост в блоге.Логика адаптивной реализации Vue3.

Эпилог

Хорошо, в конце я хочу кое-что сказать.

Есть много людей, которые считают, что отказ Vue3 от некоторых браузеров ради производительности более чем стоит потерь. Однако я так не думаю.

Прежде всего, Vue3 в настоящее время все ещеraПредполагается, что до фактического релиза пройдет некоторое время. И дело не в том, что вышел Vue3, мы должны использовать 3 для проектов, и должен быть долгий переходный период с Vue2.x на 3. Я считаю, что со временем поддержки браузеров будет все больше и больше.Proxyсвойства, мы также отказываемся от совместимости со старыми и старыми версиями браузеров.

Во-вторых, улучшение производительности Vue нельзя игнорировать из-за несовместимости некоторых браузеров в настоящее время, продвижение технологии происходит медленно. Если бы все руководствовались статус-кво, Интернет не развивался бы так быстро.

Ну, так тому и быть.