[Внешний движок] Углубленный принцип отклика Vue, анализ исходного кода

внешний интерфейс Vue.js
[Внешний движок] Углубленный принцип отклика Vue, анализ исходного кода

предисловие

В качестве одного из обязательных вопросов на собеседовании Vue принцип отзывчивости Vue не должен быть незнаком студентам, которые использовали Vue.Официальная документация VueВопросы, на которые следует обратить внимание в ответ, также подробно разъясняются.

Но для тех, кто в этом новичок или мало знает об этом, они все еще могут быть сбиты с толку: почему не может быть обнаружено добавление или удаление свойств объекта? Почему не поддерживается установка элементов массива по индексу? Я верю, что после прочтения этой статьи вы станете просветленным.

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

Репозиторий статей и исходный код находятся в🍹🍰 fe-код, добро пожаловать звезда.

После того, как большой парень напомнил вам, что Vue — это не совсем модель MVVM, пожалуйста, внимательно прочитайте ее.

Не полностью следуя модели MVVM, дизайн Vue также был вдохновлен ею. Поэтому имя переменной vm (сокращение от ViewModel) часто используется в документации для представления экземпляра Vue. —Официальный сайт Вью

Официальная адаптивная схема таунхауса Vue.

считать

Перед входом в тему давайте подумаем над следующим кодом.

<template>
    <div>
        <ul>
            <li v-for="(v, i) in list" :key="i">{{v.text}}</li>
        </ul>
    </div>
</template>
<script>
    export default{
        name: 'responsive',
        data() {
            return {
                list: []
            }
        },
        mounted() {
            setTimeout(_ => {
                this.list = [{text: 666}, {text: 666}, {text: 666}];
            },1000);
            setTimeout(_ => {
                this.list.forEach((v, i) => { v.text = i; });
            },2000)
        }
    }
</script>

Мы знаем, что в Vue черезObject.definePropertyИспользуйте атрибуты, определенные в данных как перехват данных, для поддержки публикации и подписки на связанные операции. В нашем примере только список определяется как пустой массив данных, поэтому Vue перехватит его и добавит соответствующий геттер/сеттер.

Итак, в 1 с, поthis.list = [{text: 666}, {text: 666}, {text: 666}]Переназначение списка вызовет установщик, а затем уведомит соответствующего наблюдателя (здесь наблюдатель является компиляцией шаблона) для обновления.

Через 2 с мы снова просматриваем массив, меняем свойство text каждого члена списка, и представление снова обновляется. Это место требует нашего внимания, если мы используем его непосредственно в теле циклаthis.list[i] = {text: i}Чтобы выполнить операцию обновления данных, данные могут быть обновлены в обычном режиме, но представление не будет. Это также упоминалось ранее и не поддерживает установку элементов массива по индексу.

но мы используемv.text = iТаким образом, вид может нормально обновляться, зачем это? Согласно тому, что я сказал ранее, Vue перехватит атрибуты в данных, но атрибуты внутренних элементов списка, очевидно, не будут перехвачены, так почему же представление может быть обновлено?

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

Немного изменим код:

// 视图增加了 v-if 的条件判断
<ul>
    <li v-for="(v, i) in list" :key="i" v-if="v.status === '1'">{{v.text}}</li>
</ul>

// 2 s 时,新增状态属性。
mounted() {
    setTimeout(_ => {
        this.list = [{text: 666}, {text: 666}, {text: 666}];
    },1000);
    setTimeout(_ => {
        this.list.forEach((v, i) => {
            v.text = i;
            v.status = '1'; // 新增状态
        });
    },2000)
}

Как и выше, мы добавили оценку состояния v-if в представление и установили состояние через 2 с. Но это имело неприятные последствия, представление не показывало 0, 1, 2 прямо через 2 с, как мы ожидали, а всегда было пустым.

Это ошибка, которую совершают многие новички, потому что часто бывают похожие потребности. Это также тот факт, о котором мы упоминали ранее, что Vue не может обнаружить добавление или удаление свойств объекта. Что, если мы хотим добиться желаемого эффекта? Это просто:

// 在 1 s 进行赋值操作时,预置 status 属性。
setTimeout(_ => {
    this.list = [{text: 666, status: '0'}, {text: 666, status: '0'}, {text: 666, status: '0'}];
},1000);

Конечно, Vue также предоставляетvm.$set( target, key, value )метод для решения операции добавления атрибутов в конкретных случаях, но мы здесь неприменимы.

Принципы отзывчивости Vue

Ранее мы говорили о двух конкретных примерах, приводя легкие ошибки и решения, но мы пока знаем только то, что мы должны это делать, а не зачем мы это делаем.

Перехват данных Vue зависит отObject.defineProperty, так что именно из-за некоторых его характеристик возникает эта проблема. Для учащихся, которые не понимают этот атрибут, см. здесьMDN.

Базовая реализация Object.defineProperty

Метод Object.defineProperty() определяет новое свойство непосредственно в объекте или изменяет существующее свойство объекта и возвращает объект. — МДН

Взгляните на базовый каштан для захвата данных, который также является наиболее фундаментальной зависимостью скорости реагирования.

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true, // 可枚举
        configurable: true,
        get: function() {
            console.log('get');
            return val;
        },
        set: function(newVal) {
            // 设置时,可以添加相应的操作
            console.log('set');
            val += newVal;
        }
    });
}
let obj = {name: '成龙大哥', say: ':其实我之前是拒绝拍这个游戏广告的,'};
Object.keys(obj).forEach(k => {
    defineReactive(obj, k, obj[k]);
});
obj.say = '后来我试玩了一下,哇,好热血,蛮好玩的';
console.log(obj.name + obj.say);
// 成龙大哥:其实我之前是拒绝拍这个游戏广告的,后来我试玩了一下,哇,好热血,蛮好玩的
obj.eat = '香蕉'; // ** 没有响应

можно увидеть,Object.definePropertyЭто операция захвата существующих свойств, поэтому Vue требует, чтобы используемые данные были определены в данных заранее, и не может реагировать на добавление и удаление свойств объекта. Захваченные свойства будут иметь соответствующие методы get и set.

Кроме того,Официальная документация VueВверху: из-за ограничений JavaScript Vue не поддерживает установку элементов массива по индексу. Для этого, собственно, и можно захватить массив напрямую по подписке.

let arr = [1,2,3,4,5];
arr.forEach((v, i) => { // 通过下标进行劫持
    defineReactive(arr, i, v);
});
arr[0] = 'oh nanana'; // set

Так почему же Vue не справляется с этим таким образом? Официальный ответ Юды - проблема с производительностью. Для более детального анализа этого пункта можно перейтиПочему Vue не может обнаружить изменения массива?

Реализация исходного кода Vue

Следующая версия кода Vue: 2.6.10.

Observer

Мы знаем базовую реализацию перехвата данных, и, кстати, давайте посмотрим, как это делает исходный код Vue.

// observer/index.js
// Observer 前的预处理方法
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) { // 是否是对象或者虚拟dom
    return
  }
  let ob: Observer | void
  // 判断是否有 __ob__ 属性,有的话代表有 Observer 实例,直接返回,没有就创建 Observer
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if ( // 判断是否是单纯的对象
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value) // 创建Observer
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

// Observer 实例
export class Observer { 
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep() // 给 Observer 添加 Dep 实例,用于收集依赖,辅助 vm.$set/数组方法等
    this.vmCount = 0
    // 为被劫持的对象添加__ob__属性,指向自身 Observer 实例。作为是否 Observer 的唯一标识。
    def(value, '__ob__', this)
    if (Array.isArray(value)) { // 判断是否是数组
      if (hasProto) { // 判断是否支持__proto__属性,用来处理数组方法
        protoAugment(value, arrayMethods) // 继承
      } else {
        copyAugment(value, arrayMethods, arrayKeys) // 拷贝
      }
      this.observeArray(value) // 劫持数组成员
    } else {
      this.walk(value) // 劫持对象
    }
  }

  walk (obj: Object) { // 只有在值是 Object 的时候,才用此方法
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // 数据劫持方法
    }
  }

  observeArray (items: Array<any>) { // 如果是数组,则调用 observe 处理数组成员
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]) // 依次处理数组成员
    }
  }
}

Выше следует отметить, что__ob__свойства, чтобы избежать создания дубликатов,__ob__На нем есть атрибут dep, который используется как хранилище для коллекции зависимостей, которую нужно использовать в vm.$set, array push и других методах. Затем Vue обрабатывает объекты и массивы по отдельности, а массив тщательно отслеживает только члены объекта, что также является причиной того, что индексом нельзя напрямую манипулировать, как упоминалось ранее. Однако некоторые методы массива могут отвечать нормально, например push, pop и т. д. Это связано с тем, что описанная выше обработка выполняется при оценке того, является ли объект ответа массивом, давайте взглянем на конкретный код.

// observer/index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

// export function observe 省略部分代码
if (Array.isArray(value)) { // 判断是否是数组
  if (hasProto) { // 判断是否支持__proto__属性,用来处理数组方法
    protoAugment(value, arrayMethods) // 继承
  } else {
    copyAugment(value, arrayMethods, arrayKeys) // 拷贝
  }
  this.observeArray(value) // 劫持数组成员
}
// ···

// 直接继承 arrayMethods
function protoAugment (target, src: Object) { 
  target.__proto__ = src
}
// 依次拷贝数组方法
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

// util/lang.js  def 方法长这样,用来给对象添加属性
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

Видно, что ключевой моментarrayMethodsНа, давайте продолжим смотреть на:

// observer/array.js
import { def } from '../util/index'

const arrayProto = Array.prototype // 存储数组原型上的方法
export const arrayMethods = Object.create(arrayProto) // 创建一个新的对象,避免直接改变数组原型方法

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 重写上述数组方法
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) { // 
    const result = original.apply(this, args) // 执行指定方法
    const ob = this.__ob__ // 拿到该数组的 ob 实例
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2) // splice 接收的前两个参数是下标
        break
    }
    if (inserted) ob.observeArray(inserted) // 原数组的新增部分需要重新 observe
    // notify change
    ob.dep.notify() // 手动发布,利用__ob__ 的 dep 实例
    return result
  })
})

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

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep() // 实例一个 Dep 实例

  const property = Object.getOwnPropertyDescriptor(obj, key) // 获取对象自身属性
  if (property && property.configurable === false) { // 没有属性或者属性不可写就没必要劫持了
    return
  }

  // 兼容预定义的 getter/setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) { // 初始化 val
    val = obj[key]
  }
  // 默认监听子对象,从 observe 开始,返回 __ob__ 属性 即 Observer 实例
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // 执行预设的getter获取值
      if (Dep.target) { // 依赖收集的关键
        dep.depend() // 依赖收集,利用了函数闭包的特性
        if (childOb) { // 如果有子对象,则添加同样的依赖
          childOb.dep.depend() // 即 Observer时的 this.dep = new Dep();
          if (Array.isArray(value)) { // value 是数组的话调用数组的方法
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 原有值和新值比较,值一样则不做处理
      // newVal !== newVal && value !== value 这个比较有意思,但其实是为了处理 NaN
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) { // 执行预设setter
        setter.call(obj, newVal)
      } else { // 没有预设直接赋值
        val = newVal
      }
      childOb = !shallow && observe(newVal) // 是否要观察新设置的值
      dep.notify() // 发布,利用了函数闭包的特性
    }
  })
}
// 处理数组
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend() // 如果数组成员有 __ob__,则添加依赖
    if (Array.isArray(e)) { // 数组成员还是数组,递归调用
      dependArray(e)
    }
  }
}

Dep

В приведенном выше анализе мы поняли перехват данных Vue и переписывание методов массива, но у нас появились новые сомнения, что делает Деп? Dep является издателем и может быть подписан несколькими наблюдателями.

// observer/dep.js

let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++ // 唯一id
    this.subs = [] // 观察者集合
  }
 // 添加观察者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
 // 移除观察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  
  depend () { // 核心,如果存在 Dep.target,则进行依赖收集操作
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice() // 避免污染原来的集合
    // 如果不是异步执行,先进行排序,保证观察者执行顺序
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 发布执行
    }
  }
}

Dep.target = null // 核心,用于闭包时,保存特定的值
const targetStack = []
// 给 Dep.target 赋值当前Watcher,并添加进target栈
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
// 移除最后一个Watcher,并将剩余target栈的最后一个赋值给 Dep.target
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher

Глядя на одного Депа, может быть нелегко понять, давайте посмотрим на него вместе с Наблюдателем.

// observer/watcher.js

let uid = 0
export default class Watcher {
  // ...
  constructor (
    vm: Component, // 组件实例对象
    expOrFn: string | Function, // 要观察的表达式,函数,或者字符串,只要能触发取值操作
    cb: Function, // 被观察者发生变化后的回调
    options?: ?Object, // 参数
    isRenderWatcher?: boolean // 是否是渲染函数的观察者
  ) {
    this.vm = vm // Watcher有一个 vm 属性,表明它是属于哪个组件的
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this) // 给组件实例的_watchers属性添加观察者实例
    // options
    if (options) {
      this.deep = !!options.deep // 深度
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync // 同步执行
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb // 回调
    this.id = ++uid // uid for batching // 唯一标识
    this.active = true // 观察者实例是否激活
    this.dirty = this.lazy // for lazy watchers
    // 避免依赖重复收集的处理
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else { // 类似于 Obj.a 的字符串
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop // 空函数
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () { // 触发取值操作,进而触发属性的getter
    pushTarget(this) // Dep 中提到的:给 Dep.target 赋值
    let value
    const vm = this.vm
    try {
      // 核心,运行观察者表达式,进行取值,触发getter,从而在闭包中添加watcher
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) { // 如果要深度监测,再对 value 执行操作
        traverse(value)
      }
      // 清理依赖收集
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) { // 避免依赖重复收集
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this) // dep 添加订阅者
      }
    }
  }

  update () { // 更新
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run() // 同步直接运行
    } else { // 否则加入异步队列等待执行
      queueWatcher(this)
    }
  }
}

На этом этапе мы можем в общих чертах подытожить некоторые процессы всей отзывчивой системы, которые мы часто называемШаблон наблюдателя: Первый шаг, конечно же, перехватить данные через наблюдатель, а затем добавить наблюдателя в то место, на которое нужно подписаться (например, компиляцию шаблона), и сразу вызвать метод получения указанного свойства через значение операцию, тем самым добавляя наблюдатель Enter Dep (используя характеристики замыканий для сбора зависимостей), а затем уведомлять, когда срабатывает Setter, уведомлять всех наблюдателей и обновлять их соответствующим образом.

мы можем понятьШаблон наблюдателя: Dep подобен самородку, и у слепка много авторов (эквивалентно многим атрибутам данных). Естественно, все мы выступаем в роли наблюдателей и следим за интересующими нас авторами на Наггетс (Dep), такими как: Цзян Санмад, скажи ему, что Цзян Санмад обновился, и напомни мне посмотреть его. Затем, когда у Цзян Санмада будет новый контент, мы получим напоминание, подобное этому:江三疯发布了【2019 前端进阶之路 ***】, а потом мы можем пойти и посмотреть.

Однако каждый наблюдатель может подписаться на многих авторов, и каждый автор также обновляет статьи. Так получат ли пользователи, которые не подписаны на Jiang Sanmad, напоминание? Нет, он отправляет напоминания только тем пользователям, которые уже подписались, и только при обновлении Jiang Sanmad.Вы подписаны на Jiang Sanmad, но должен ли веб-мастер напоминать вам, что он был обновлен? Конечно, нет. Это то, что должны делать замыкания.

Proxy

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

Мы все знаем, что Vue 3.0 будет использоватьProxyзаменятьObject.defineProperty, так каковы преимущества этого?

Преимущества очевидны: например, можно решить две существующие проблемы Vue, который не может реагировать на добавление и удаление свойств объекта и не может напрямую манипулировать индексами массива. Есть, конечно, и плохие, то есть проблема совместимости, и эту проблему совместимости babel решить не может.

Основное использование

Мы используем прокси для простого захвата данных.

let obj = {};
// 代理 obj
let handler = {
    get: function(target, key, receiver) {
        console.log('get', key);
        return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
        console.log('set', key, value);
        return Reflect.set(target, key, value, receiver);
    },
    deleteProperty(target, key) {
        console.log('delete', key);
        delete target[key];
        return true;
    }
};
let data = new Proxy(obj, handler);
// 代理后只能使用代理对象 data,否则还用 obj 肯定没作用
console.log(data.name); // get name 、undefined
data.name = '尹天仇'; // set name 尹天仇
delete data.name; // delete name

В данном примере obj — это пустой объект.После проксирования через Proxy добавление и удаление свойств также может получить обратную связь. Давайте снова посмотрим на прокси массива:

let arr = ['尹天仇', '我是一个演员', '柳飘飘', '死跑龙套的'];
let array = new Proxy(arr, handler);
array[1] = '我养你啊'; // set 1 我养你啊
array[3] = '先管好你自己吧,傻瓜。'; // set 3 先管好你自己吧,傻瓜。

Настройка индекса массива тоже вполне держится.Конечно, полезность Proxy не только в этом, есть 13 видов операций, поддерживающих перехват. Заинтересованные студенты могут посетитьКнига Учителя Жуань Ифэн., здесь больше нет многословия.

Прокси реализует шаблон наблюдателя

Ранее мы проанализировали исходный код Vue, а также поняли основные принципы паттерна Observer. Как реализовать наблюдателя с прокси? Мы можем просто написать:

class Dep {
    constructor() {
        this.subs = new Set(); 
        // Set 类型,保证不会重复
    }
    addSub(sub) { // 添加订阅者
        this.subs.add(sub);
    }
    notify(key) { // 通知订阅者更新
        this.subs.forEach(sub => {
            sub.update();
        });
    }
}
class Watcher { // 观察者
    constructor(obj, key, cb) {
        this.obj = obj;
        this.key = key;
        this.cb = cb; // 回调
        this.value = this.get(); // 获取老数据
    }
    get() { // 取值触发闭包,将自身添加到dep中
        Dep.target = this; // 设置 Dep.target 为自身
        let value = this.obj[this.key];
        Dep.target = null; // 取值完后 设置为nul
        return value;
    }
    // 更新
    update() {
        let newVal = this.obj[this.key];
        if (this.value !== newVal) {
            this.cb(newVal);
            this.value = newVal;
        }
    }
}
function Observer(obj) {
    Object.keys(obj).forEach(key => { // 做深度监听
        if (typeof obj[key] === 'object') {
            obj[key] = Observer(obj[key]);
        }
    });
    let dep = new Dep();
    let handler = {
        get: function (target, key, receiver) {
            Dep.target && dep.addSub(Dep.target);
            // 存在 Dep.target,则将其添加到dep实例中
            return Reflect.get(target, key, receiver);
        },
        set: function (target, key, value, receiver) {
            let result = Reflect.set(target, key, value, receiver);
            dep.notify(); // 进行发布
            return result;
        }
    };
    return new Proxy(obj, handler)
}

Код относительно короткий, просто поместите его в один кусок. Общая идея аналогична Vue, но следует обратить внимание на среду закрытия во время операции получения, что делаетDep.target && dep.addSub(Dep.target)Можно гарантировать, что при срабатывании получателя каждого свойства это будет текущий экземпляр Watcher. Если замыкание сложно понять, вы можете сравнить пример вывода цикла for 1, 2, 3, 4, 5.

Еще раз взгляните на бегущий результат:

let data = {
    name: '渣渣辉'
};
function print1(data) {
    console.log('我系', data);
}
function print2(data) {
    console.log('我今年', data);
}
data = Observer(data);
new Watcher(data, 'name', print1);
data.name = '杨过'; // 我系 杨过

new Watcher(data, 'age', print2);
data.age = '24'; // 我今年 24

MVVM

Сказав это, пришло время потренироваться. Vue значительно повысил производительность внешнего интерфейса.На этот раз мы будем обращаться к Vue для реализации простой инфраструктуры Vue.

Часть реализации упоминается изРазбор принципа реализации Vue — как реализовать двухстороннюю привязку mvvm

Что такое МВВМ?

Краткое введение в MVVM, более полное объяснение вы можете посмотреть здесьШаблон MVVM. Полное название MVVM — Model-View-ViewModel, которое представляет собой архитектурный шаблон, впервые предложенный Microsoft, основанный на идеях MVC и других шаблонах.

ViewModel отвечает за синхронизацию данных модели с представлением для отображения, а также за синхронизацию изменений данных представления с моделью. Уровень модели, как и уровень данных, заботится только о самих данных, а не о том, как работать с данными и отображать их; представление — это уровень представления, отвечающий за преобразование модели данных в пользовательский интерфейс и отображение ее пользователю.

Изображение изШаблон MVVM

Как реализовать MVVM?

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

<body>
<div id="app">
    姓名:<input type="text" v-model="name"> <br>
    年龄:<input type="text" v-model="age"> <br>
    职业:<input type="text" v-model="profession"> <br>
    <p> 输出:{{info}} </p>
    <button v-on:click="clear">清空</button>
</div>
</body>
<script src="mvvm.js"></script>
<script>
    const app = new MVVM({
        el: '#app',
        data: {
            name: '',
            age: '',
            profession: ''
        },
        methods: {
            clear() {
                this.name = '';
                this.age =  '';
                this.profession = '';
            }
        },
        computed: {
            info() {
                return `我叫${this.name},今年${this.age},是一名${this.profession}`;
            }
        }
    })
</script>

текущий результат:

Ну, похоже, что он имитирует (плагиат) некоторые основные функции Vue, такие как двусторонняя привязка, вычисление, v-on и т. д. Для того, чтобы облегчить понимание, все же нарисуем принципиальную схему примерно.

Судя по картинке, что нам теперь нужно сделать? Перехват данных, посредничество в данных, компиляция шаблонов, публикация-подписка, подождите минутку, эти термины не кажутся вам знакомыми? Разве это не то, что мы делали, когда раньше анализировали исходный код Vue? (Да, да, но это просто копия Vue). Хорошо, мы знакомы с перехватом данных, публикацией и подпиской, но понятия не имеем о компиляции шаблонов. Не волнуйтесь, давайте начнем.

new MVVM()

Согласно идее принципиальной схемы, первым шагом являетсяnew MVVM(), то есть инициализация. Что делать при инициализации? Возможен захват данных и инициализация шаблонов (представлений).

class MVVM {
    constructor(options) { // 初始化
        this.$el = options.el;
        this.$data = options.data;
        if(this.$el){ // 如果有 el,才进行下一步
            new Observer(this.$data);
            new Compiler(this.$el, this);
        }
    }
}

Кажется, чего-то не хватает, с вычислениями и методами тоже надо разобраться, так что наверстать.

class MVVM {
    constructor(options) { // 初始化
        // ··· 接收参数
        let computed = options.computed;
        let methods = options.methods;
        let that = this;
        if(this.$el){ // 如果有 el,才进行下一步
        // 把 computed 的key值代理到 this 上,这样就可以直接访问 this.$data.info,取值的时候便直接运行 计算方法
        // 注意 computed 需要代理,不需要Observer
            for(let key in computed){
                Object.defineProperty(this.$data, key, {
                    enumerable: true,
                    configurable: true,
                    get() {
                        return computed[key].call(that);
                    }
                })
            }
        // 把 methods 的方法直接代理到 this 上,这样可以访问 this.clear
            for(let key in methods){
                Object.defineProperty(this, key, {
                    get(){
                        return methods[key];
                    }
                })
            }
        }
    }
}

В приведенном выше коде мы помещаем данные в this.$data, но подумайте об этом, мы обычно используем this.xxx для доступа. Следовательно, к данным, как и к вычисляемым свойствам, необходимо добавить слой прокси для легкого доступа. Для подробного процесса расчета свойств мы поговорим об этом при захвате данных.

class MVVM {
    constructor(options) { // 初始化
        if(this.$el){
            this.proxyData(this.$data);
            // ··· 省略
        }
    }
    proxyData(data) { // 数据代理
        for(let key in data){
           // 访问 this.name 实际是访问的 this.$data.name
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get(){
                    return data[key];
                },
                set(newVal){
                    data[key] = newVal;
                }
            })
        }
    }
}

Перехват данных, публикация и подписка

После инициализации нам осталось выполнить еще два шага.

new Observer(this.$data); // 数据劫持 + 发布订阅
new Compiler(this.$el, this); // 模板编译

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

class Dep { // 发布订阅
    constructor(){
        this.subs = []; // watcher 观察者集合
    }
    addSub(watcher){ // 添加 watcher
        this.subs.push(watcher);
    }
    notify(){ // 发布
        this.subs.forEach(w => w.update());
    }
}

class Watcher{ // 观察者
    constructor(vm, expr, cb){
        this.vm = vm; // 实例
        this.expr = expr; // 观察数据的表达式
        this.cb = cb; // 更新触发的回调
        this.value = this.get(); // 保存旧值
    }
    get(){ // 取值操作,触发数据 getter,添加订阅
        Dep.target = this; // 设置为自身
        let value = resolveFn.getValue(this.vm, this.expr); // 取值
        Dep.target = null; // 重置为 null
        return value;
    }
    update(){ // 更新
        let newValue = resolveFn.getValue(this.vm, this.expr);
        if(newValue !== this.value){
            this.cb(newValue);
            this.value = newValue;
        }
    }
}

class Observer{ // 数据劫持
    constructor(data){
        this.observe(data);
    }
    observe(data){
        if(data && typeof data === 'object') {
            if (Array.isArray(data)) { // 如果是数组,遍历观察数组的每个成员
                data.forEach(v => {
                    this.observe(v);
                });
                // Vue 在这里还进行了数组方法的重写等一些特殊处理
                return;
            }
            Object.keys(data).forEach(k => { // 观察对象的每个属性
                this.defineReactive(data, k, data[k]);
            });
        }
    }
    defineReactive(obj, key, value) {
        let that = this;
        this.observe(value); //对象属性的值,如果是对象或者数组,再次观察
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get(){ // 取值时,判断是否要添加 Watcher,收集依赖
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newVal){
                if(newVal !== value) {
                    that.observe(newVal); // 观察新设置的值
                    value = newVal;
                    dep.notify(); // 发布
                }
            }
        })
    }
}

Принимая значение, мы используемresolveFn.getValueТакой метод представляет собой набор инструментальных методов, и в последующих компиляциях их гораздо больше. Рассмотрим сначала этот метод более подробно.

resolveFn = { // 工具函数集
    getValue(vm, expr) { // 返回指定表达式的数据
        return expr.split('.').reduce((data, current)=>{
            return data[current]; // this[info]、this[obj][a]
        }, vm);
    }
}

Как мы упоминали в предыдущем анализе, выражение может быть строкой или функцией (например, функцией рендеринга), если оно может инициировать операцию значения. Здесь мы рассматриваем только форму строк, где могут быть такие выражения? Например{{info}},Напримерv-model="name"После = стоит выражение. это также может бытьobj.aформа. Итак, здесь используйте сокращение для достижения непрерывного эффекта значения.

Расчетное свойство вычислено

При инициализации осталась проблема, так как она включает в себя публикацию и подписку, здесь мы подробно проанализируем процесс срабатывания вычисляемых свойств, во время инициализации используется шаблон.{{info}}, затем, когда шаблон скомпилирован, вам нужно инициировать операцию значения this.info, чтобы получить реальное значение для замены{{info}}эта строка. Мы также добавим наблюдателя в этом месте.

    compileText(node, '{{info}}', '') // 假设编译方法长这样,初始值为空
    new Watcher(this, 'info', () => {do something}) // 我们紧跟着实例化一个观察者

Какое действие будет запущено в это время? мы знаемnew Watcher() , это вызовет значение. В соответствии с функцией стоимости только что, она будет принята в это время.this.info, и мы сделали прокси во время инициализации.

for(let key in computed){
    Object.defineProperty(this.$data, key, {
        get() {
            return computed[key].call(that);
        }
    })
}

Таким образом, в этот раз будет выполняться метод, определенный в calculated. Вы помните, как выглядит этот метод?

computed: {
    info() {
        return `我叫${this.name},今年${this.、age},是一名${this.profession}`;
    }
}

В результате операции со значениями имени, возраста и профессии будут запускаться одна за другой.

defineReactive(obj, key, value) {
    // ···
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get(){ // 取值时,判断是否要添加 Watcher,收集依赖
            Dep.target && dep.addSub(Dep.target);
            return value;
        }
        // ···
    })
}

Воспользуйтесь этим временем по полнойЗакрытиеСледует отметить, что он все еще находится в процессе приобретения значения информации, потому чтоСинхронизироватьметод, что означает, что теперь Dep.target существует и является Watcher, который наблюдает за свойством info. Следовательно, программа добавит наблюдателя информации в список имени, возраста и профессии соответственно, так что, если какое-либо значение из трех атрибутов изменится, она уведомит наблюдателя информации о переоценке и обновлении представления.

Распечатайте отчет в это время для простоты понимания.

Компиляция шаблона

На самом деле некоторые вещи, связанные с компиляцией шаблонов, уже упоминались ранее, главное в этой части — скомпилировать синтаксис шаблона на html в реальные данные, а инструкции преобразовать в соответствующие функции.

В процессе компиляции неизбежно манипулирование элементом Dom, поэтому здесь используется метод createDocumentFragment для создания фрагментов документа. Это фактически использует виртуальный дом в Vue и использует алгоритм сравнения, чтобы сделать это при обновлении.минимальная стоимость рендеринга.

Фрагменты документа живут в памяти, а не в дереве DOM, поэтому вставка дочерних элементов во фрагменты документа не вызывает переформатирование страницы (вычисление положения и геометрии элемента). Таким образом, использование фрагментов документа обычно приводит к повышению производительности. —MDN

class Compiler{
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el); // 获取app节点
        this.vm = vm;
        let fragment = this.createFragment(this.el); // 将 dom 转换为文档碎片
        this.compile(fragment); // 编译
        this.el.appendChild(fragment); // 变易完成后,重新放回 dom
    }
    createFragment(node) { // 将 dom 元素,转换成文档片段
        let fragment = document.createDocumentFragment();
        let firstChild;
        // 一直去第一个子节点并将其放进文档碎片,直到没有,取不到则停止循环
        while(firstChild = node.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
    isDirective(attrName) { // 是否是指令
        return attrName.startsWith('v-');
    }
    isElementNode(node) { // 是否是元素节点
        return node.nodeType === 1;
    }
    compile(node) { // 编译节点
        let childNodes = node.childNodes; // 获取所有子节点
        [...childNodes].forEach(child => {
            if(this.isElementNode(child)){ // 是否是元素节点
                this.compile(child); // 递归遍历子节点
                let attributes = child.attributes; 
                // 获取元素节点的所有属性 v-model class 等
                [...attributes].forEach(attr => { // 以  v-on:click="clear" 为例
                    let {name, value: exp} = attr; // 结构获取 "clear"
                    if(this.isDirective(name)) { // 判断是不是指令属性
                        let [, directive] = name.split('-'); // 结构获取指令部分 v-on:click
                        let [directiveName, eventName] = directive.split(':'); // on,click
                        resolveFn[directiveName](child, exp, this.vm, eventName); 
                        // 执行相应指令方法
                    }
                })
            }else{ // 编译文本
                let content = child.textContent; // 获取文本节点
                if(/\{\{(.+?)\}\}/.test(content)) { // 判断是否有模板语法 {{}}
                    resolveFn.text(child, content, this.vm); // 替换文本
                }
            }
        });
    }
}

// 替换文本的方法
resolveFn = { // 工具函数集
    text(node, exp, vm) {
        // 惰性匹配,避免连续多个模板时,会直接取到最后一个花括号
        // {{name}} {{age}} 不用惰性匹配 会一次取全 "{{name}} {{age}}"
        // 我们期望的是 ["{{name}}", "{{age}}"]
        let reg = /\{\{(.+?)\}\}/;
        let expr = exp.match(reg);
        node.textContent = this.getValue(vm, expr[1]); // 编译时触发更新视图
        new Watcher(vm, expr[1], () => { // setter 触发发布
            node.textContent = this.getValue(vm, expr[1]);
        });
    }
}

При компиляции узла элемента (this.compile(node)), мы оцениваем, является ли атрибут элемента инструкцией, и вызываем соответствующий метод инструкции. Итак, наконец, давайте взглянем на некоторые простые реализации инструкций.

  • Двусторонняя привязка v-образная модель
resolveFn = { // 工具函数集
    setValue(vm, exp, value) {
        exp.split('.').reduce((data, current, index, arr)=>{ // 
            if(index === arr.length-1) { // 最后一个成员时,设置值
                return data[current] = value;
            }
            return data[current];
        }, vm.$data);
    },
    model(node, exp, vm) {
        new Watcher(vm, exp, (newVal) => { // 添加观察者,数据变化,更新视图
            node.value = newVal;
        });
        node.addEventListener('input', (e) => { //监听 input 事件(视图变化),事件触发,更新数据
            let value = e.target.value;
            this.setValue(vm, exp, value); // 设置新值
        });
        // 编译时触发
        let value  = this.getValue(vm, exp);
        node.value = value;
    }
}

Двусторонняя привязка должна быть проста для понимания.Следует отметить, что при использовании setValue его нельзя установить напрямую с возвращаемым значением сокращения. Потому что возвращаемое значение в это время является просто значением, которое не может достичь цели переназначения.

  • привязка события v-on

Помните, как мы обрабатывали методы при инициализации?

for(let key in methods){
    Object.defineProperty(this, key, {
        get(){
            return methods[key];
        }
    })
} 

Проксируем все методы к этому и компилируемv-on:click="clear"Когда команда разложена на 'on', 'click', 'clear', готова ли реализация функции on?

on(node, exp, vm, eventName) { // 监听对应节点上的事件,触发时调用相对应的代理到 this 上的方法
    node.addEventListener(eventName, e => {
        vm[exp].call(vm, e);
    })
}

Vue предоставляет гораздо больше инструкций, таких как: v-if, которая на самом деле является операцией по добавлению или удалению элементов dom; v-show, которая на самом деле является операцией, в которой атрибут отображения элемента является блочным или отсутствует; v -html, который должен отображать значение инструкции. Добавление его непосредственно в элемент dom может быть реализовано с помощью innerHTML, но эта операция слишком небезопасна и имеет риск xss, поэтому Vue также рекомендует не открывать интерфейс пользователю. . Есть также относительно сложные инструкции, такие как v-for и v-slot, Заинтересованные студенты могут изучить их самостоятельно.

Суммировать

Полный код статьи находится по адресуРепозиторий статей🍹🍰fe-код. В этом выпуске в основном рассказывается об адаптивном принципе Vue, включая перехват данных, публикацию и подписку, прокси иObject.definePropertyОтличия и т.п., а простой MVVM кстати писался. Как превосходный интерфейсный фреймворк, Vue имеет слишком много моментов, которые нам нужно изучить, и каждая деталь достойна нашего глубокого изучения. В последующем будет представлена ​​серия статей по вопросам интерфейса, таким как Vue и javascript.Заинтересованные студенты могут обратить внимание.

Справочная статья

группа обмена

Группа внешнего обмена QQ: 960807765, приветствуем все виды технического обмена, с нетерпением ждем вашего присоединения

постскриптум

Если вы это видите, и эта статья вам полезна, надеюсь, вы сможете поддержать автора своими ручонками, спасибо 🍻. Если в тексте что-то не так, укажите на это и поделитесь.

Еще статьи:

Передняя усовершенствованная дорожная серия

Серия боев с ног на голову

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