Простой пример — освежите свое понимание реактивных принципов Vue2

Vue.js
Простой пример — освежите свое понимание реактивных принципов Vue2

1 последовательность

почему это отличаетсяVue2Принцип отзывчивости?

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

Vue2Отзывчивость объекта, общее восприятие таково,来源于官网:

  • для объекта

    Проще говоря: отзывчивость объекта можно использовать только для существующих свойств,obj.newProа такжеdelete obj.oldProВремяVue2Отзывчивость не может быть перехвачена и должна быть использованаthis.$setилиVue.setметод может

  • для массивов

    Проще говоря: ответ массива обычно осуществляется через 7 методов работы с массивом (поObject.definePropertyвоспитал семь бахчевых), чтобы добиться отзывчивости, какarr[0] = 1,arr.length = 0Невозможно добиться отзывчивости

но,不一样的Vue2响应式原理Это заставит вас заново понять и обнаружить, что приведенное выше утверждение не совсем верно.

Далее мы будем использовать простой пример для源码Уровень, чтобы найти ответ, чтобы проанализироватьVueЧто сделали два процесса от инициализации до обновления?Конечно, эта статья предназначена в основном для иллюстрации проблемы.Исходный код был частично упрощен, и слишком много слов нужно убрать.

2 Каково явление и внутренний принцип (процесс выполнения) следующего примера?

Вы можете сначала посмотреть код, а потом посмотреть ответ.Конечно, вы также можете написать пример и попробовать его самостоятельно.Проверьте следующее

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id = "app">
    <p>{{ form }}</p>
    <ul>
      <li v-for = "item in arr" :key = "item">{{ item }}</li>
    </ul>
  </div>
  <script src = "../../dist/vue.js"></script>
  <script>
  const ins = new Vue({
    data () {
      return {
        form: {
          name: 'lyn'
        },
        arr: [1]
      }
    },
    mounted () {
      setTimeout(() => {
        this.form.name = 'test'
        this.arr[0] = 11
      }, 2000)
    }
  })
  ins.$mount('#app')
  </script>
</body>
</html>

2.1 Сначала сделайте вывод

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

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

Если ваш ответ такой, то эту статью стоит прочитать

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

2.1.1 Феномен

Первоначальный результат рендеринга:

Через 2 секунды выполняется функция синхронизации, и страница обновляется до:

2.1.2 Внутренний принцип

После загрузки примера кода в браузер,VueЗапустите инициализацию, выполните различныеinitоперации, наиболее важной из которых является создание экземпляров компонентовWatcher, инициализировать данные, собрать зависимости (dep), связатьdepа такжеwatcher, естественно вперемежку с выполнением методов жизненного цикла, таких какbeforeCreate,created,beforeMount,mounted, если есть дочерние компоненты,beforemountПосле завершения выполнения дочерний компонент будет инициализирован до тех пор, пока не будет запущен собственный компонент.mountedвыполнение завершено, затем вернитесь к выполнениюmounted, увы, далековато, но не влияет, проблема не в инициализации, а в последующем обновлении.

Выполнить функцию синхронизации после рендеринга страницы в течение 2 с.

сначала выполнитьthis.form.name = 'test', который делится на два шага

  • первыйthis.formвызыватьgetterполучатьvalue = { name: 'lyn' }

  • затем выполнитьvalue.name = 'test'вызыватьsetter, обновите данные, сейчасthis.form.nameценностьtest

существуетsetterсредний триггерdep.notify(),уведомлятьwatcherВыполняйте свои собственныеupdateметод,updateметод будетwatcherВставьте себя в очередь (массив очередей), а затем вызовитеnextTickМетод регистрирует функцию, которая обновляет очередь (фактически выполняет метод run каждого наблюдателя в массиве очереди),nextTickметод оборачивает функцию, очищающую очередь, стрелочной функцией и сохраняет ее вcallbacksв массиве следующийnextTickбудет выполнятьtimerFuncфункция

timerFuncФункция использует асинхронный механизм браузера и будет обновлятьcallbacksФункция массива зарегистрирована как асинхронная задача.Когда все синхронные задачи будут выполнены, только что зарегистрированная очередь будет обновлена.Поскольку синхронная задача еще не выполнена, осталась еще одна.this.arr[0] = 11, поэтому асинхронная задача сначала временно приостанавливается

выполнить следующийthis.arr[0] = 11, который также выполняется в два этапа

  • первыйthis.arrвызоветgetterполучатьvalue = [1]

  • Потом его нет, потому чтоthis.arr[0] = 11написано так,Vue2отзывчивое ядроObject.definePropertyне может быть перехвачен, но

Но важный момент,this.arr[0] = 11Этот код действительно выполняется, а это значит, чтоthis.arrТекущее значение действительно[11]Да, это очень важно, если у вас есть какие-то сомнения, продолжайте смотреть вниз с сомнениями.

В этот момент все синхронные задачи завершаются, и только что зарегистрированные асинхронные задачи начинают выполняться.

Упомянутая выше асинхронная задача — это всего лишь набор callback-функций, что в итоге она делает очень просто (чисто), то есть выполняетwatcher.runметод,watcher.runвыполнение методаwatcher.getметод,getметод отвечает за выполнениеupdateComponentметод, этот метод создается при инициализации компонентаwatcherкогда перешел кwatcherда, выполнитьupdateComponentметод будет выполнен первымvm._renderфункция для создания новыхvdom, обратите внимание, что создание новыхvdomнужно читатьvueэкземплярthisЧитаются, конечно, только те атрибуты, которые используются в шаблоне, в нашем примере этоthis.formа такжеthis.arr, Вы немного поняли после прочтения этого?

несмотря на то чтоthis.arr[0] = 11нет способа вызватьVue2, но может изменитьсяthisЗначение атрибута , поэтому я увидел на странице сцену, которую раньше не понимал

генерировать новыеvdom,updateComponentвоплощать в жизньvm._updateметод, вызовpatchМетод сравнения старого и новогоvdomВыявление измененийdomузел, затем обновить

Увидев это, вы уже немного понимаете или у вас другое представление? Например:

я хочу пройтиthis.arr[idx] = xxxОбновите элементы массива, не хотите использоватьthis.spliceи т. д., вам нужно только принести еще один, чтобы вызватьsetterЭффективная работа黑魔法Ну а ведь если вы это напишете, то не очень хорошо будет навлекать на окружающих негативный опыт.

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

2.2 Найдите ответ в исходном коде

В этом разделе будет проанализирован процесс выполнения всего примера кода. Когда контент загружается в браузер,VueПроцесс выполнения исходного кода выглядит следующим образом:

  • src/core/instance/index.js

    /**
     * Vue 构造函数,执行初始化操作
     */
    function Vue (options) {
      this._init(options)
    }
    
  • src/core/instance/init.js

    /**
     * 执行各种初始化操作,比如:
     * 最重要的给数据设置响应式(这部分内容就不展开了,否则太多了)然后执行实例的 $mount 方法
     */
    Vue.prototype._init = function (options?: Object) {
      initLifecycle(vm)
      initEvents(vm) 
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
      
      if (vm.$options.el) {
        vm.$mount(vm.$options.el)
      }
    }
    
  • src/platforms/web/runtime/index.js

    /**
     * $mount, 负责执行 mountComponent 方法
     */
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    
  • src/platforms/web/entry-runtime-with-compiler.js

    /**
     * 不用管这个,和问题无关,这里其实重写了 $mount 执行了编译模版的动作,
     * 最后生成 render 函数,这部分内容被我删了
     */
    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      return mount.call(this, el, hydrating)
    }
    
  • src/core/instance/lifecycle.js

    /**
     * mountComponent 方法
     * 很重要的几点
     * 1、定义组件的 updateComponent 方法
     * 2、实例化组件 watcher,并将 updateComponent 方法传递给 watcher
     *
     * watcher 后面会在自己的 run 方法中调用 get 方法,get 方法会负责执行这个 updateComponent 方法,重新生成新的 vdom,watcher 相关看下面的 watcher 部分
     */
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      callHook(vm, 'beforeMount')
    
      let updateComponent = () => {
        // vm._render 执行后会生成新的 vdom,vm._update 方法会调用 patch 方法,对比新旧 dom,更新视图
        vm._update(vm._render(), hydrating)
      }
    
      // we set this to vm._watcher inside the watcher's constructor
      // since the watcher's initial patch may call $forceUpdate (e.g. inside child
      // component's mounted hook), which relies on vm._watcher being already defined
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false
    
      // 调用组件实例的 mounted 方法
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }
    
    /**
     *负责执行各种各样的生命周期方法,比如 mounted
     */
    export function callHook (vm: Component, hook: string) {
      // handlers = vm.$options.mounted
      const handlers = vm.$options[hook]
      const info = `${hook} hook`
      if (handlers) {
        for (let i = 0, j = handlers.length; i < j; i++) {
          invokeWithErrorHandling(handlers[i], vm, null, vm, info)
        }
      }
    }
    
    /**
     * 负责执行 patch 方法,分为首次渲染和再次更新
     */
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
      const vm: Component = this
      const prevEl = vm.$el
      const prevVnode = vm._vnode
      const restoreActiveInstance = setActiveInstance(vm)
      vm._vnode = vnode
      // Vue.prototype.__patch__ is injected in entry points
      // based on the rendering backend used.
      if (!prevVnode) {
        // initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
      } else {
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode)
      }
    }
    
  • src/core/util/error.js

    // 执行 声明周期方法
    export function invokeWithErrorHandling (
      handler: Function,
      context: any,
      args: null | any[],
      vm: any,
      info: string
    ) {
      // 真正执行声明周期方法的地方
      return args ? handler.apply(context, args) : handler.call(context)
    }
    

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

```javascript
/**
 * mounted 方法中的定时函数
 */
setTimeout(() => {
  this.form.name = 'test'
  this.arr[0] = 11
}, 2000)
```

Далее проанализируем, что представляет собой процесс выполнения callback-функции, зарегистрированный таймером

  • src/core/observer/index.js

    Когда функция синхронизации выполняется, будет запущено следующееgetterа такжеsetter,Например:this.form.name = 'test'будет выполняться первымthis.formвызыватьgetterполучатьvalue = { name: 'lyn' }, а затем выполнитьvalue.name = 'test'вызыватьsetterвозобновитьnameсвойства, затем выполнитеdep.notify()

    /**
     * 这个其实就是数据响应式的核心了,拦截了示例对象上的各个属性,数据读取时执行 get,设置数据时执行 set
     */
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
    
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          // 通知 watcher 去执行 update 方法
          dep.notify()
        }
      })
    }
    
  • src/core/observer/dep.js

    /**
     * A dep is an observable that can have multiple
     * directives subscribing to it.
     *
     * dep 负责收集依赖,通知 watcher 更新
     * 这里只保留了 notify(通知watcher更新) 和 构造函数
     */
    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
      // 通知通知执行 update 方法
      notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          // 这个其实就是 watcher 的 update 方法
          subs[i].update()
        }
      }
    }
    
  • src/core/observer/watcher.js

    /**
     * A watcher parses an expression, collects dependencies,
     * and fires callback when the expression value changes.
     * This is used for both the $watch() api and directives.
     * 
     * 一个组件对应一个 watcher 实例(渲染watcher),实例化过程是在 mountComponent 方法中做的,也就是执行 vm.$mount 之后
     * 在同步执行过程中最重要的就是将当前 watcher 实例 push 到一个 watcher 执行队列中,
     * 待将来执行,通过一个 Promise.resolve().then() 来执行 run 方法,从而执行 updateComponent 方法
     */
    export default class Watcher {
      constructor (
        vm: Component,
        // updateComponent
        expOrFn: string | Function,
        // noop
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        // 很重要,就是租价更新方法,updateComponent
        this.getter = expOrFn
      }
       
      /**
       * Evaluate the getter, and re-collect dependencies.
       * 
       * 由 this.run 执行,执行 updateComponent 方法,生成新的 vdom,然后执行 patch,更新视图
       */
      get () {
        // Dep.target = watcher实例,这里让 dep 和 watcher 关联
        pushTarget(this)
        let value
        // 组件实例
        const vm = this.vm
        try {
          // 这里其实执行的是这个 updateComponent 方法:
          // let updateComponent = () => { vm._update(vm._render(), hydrating) }
          value = this.getter.call(vm, vm)
        } catch (e) {
        } finally {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          if (this.deep) {
            traverse(value)
          }
          popTarget()
        }
        return value
      }
    
      /**
       * 将 watcher 实例加入 watcher 队列
       */
      update () { 
        queueWatcher(this)
      }
    
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       *
       *
       * 这里其实通过 timerFunc 来调用,借用了浏览器的异步机制(Promise)
       * 执行 this.get 方法,让 get 执行 updateComponent
       */
      run () {
        const value = this.get()
      }
    }
    
  • src/core/observer/scheduler.js

    /**
     * 将 watcher push 进 queue 数组,然后注册一个回到函数,在将来[Promise.resolve().then()]来执行这些 watcher 的 run 方法
     */
    export function queueWatcher (watcher: Watcher) {
      queue.push(watcher)
      // 这里其实就是注册回调函数 flushSchedulerQueue 
      nextTick(flushSchedulerQueue)
    }
    
    /**
     * 负责让队列中所有的 watcher 执行自己的 run 方法.
     */
    function flushSchedulerQueue () {
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        watcher.run()
      }
    }
    
  • src/core/util/next-tick.js

    Если вы не понимаете макрозадачи и микрозадачи, вы можете прочитатьэта статья

    /**
     * 很重要的几个点
     *
     * 定义 nextTick 方法,将回调函数全部放到一个 callbacks 数组,然后执行 timerFunc
     * 定义 timerFunc,其实就是就是利用了浏览器的异步任务机制,这里选了 Promise 微任务,Vue首选就是Promise
     * Promise.resolve().then() 注册的回调函数就是刷新刚才存储的 queue 队列(数组),
     * 执行 watcher.run(),触发 updateComponent,这里很关键的一点是理解宏任务、微任务,
     * 当宏任务都执行结束后,比如示例中的整个setTimeout 回调,就会执行这里注册的微任务,Promise.resolve().then()
     */ 
    const callbacks =[]
    let pending = false
    
    // nextTick 就是用一个箭头函数将 flushSchedulerQueue 函数包裹然后放到 callbacks 数组
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        cb.call(ctx)
      })
      if (!pending) {
        pending = true
    		// 就是执行一个 异步 方法,首选 Promise
        timerFunc()
      }
    }
    
    
    // 执行一个立即就绪 Promise,Promise 回调负责执行 flushCallbacks 函数
    let timerFunc = () => {
      Promise.resolve().then(flushCallbacks)
    }
    
    /**
     * 执行 callbacks 数组中的 () => flushSchedulerQueue.call(ctx),而最终会放 watcher 去执行自己的 run 方法,
     * run 方法执行 get 方法,get 方法中最终会调用组件的 updateComponent 方法,然后执行 render 重新生成 vnode,然后执行
     * patch 过程,最终更新 dom
     */
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    
  • Что генерируется после выполнения функции vm._render?

    /**
     * 可以看到,生成 vdom 的时候,会去组件实例对象上读取响应的属性值,比如我们这里的,this.form,this.arr
     * 理解这里很重要,为什么我们的视图会被有效更新?是因为 vm.arr 确实被更新成了 [11]
     */
    function anonymous() {
    	with(this) {
        return _c(
          'div',
          {attrs:{"id":"app"}},
          [
            // this.form
            _c('p',[_v(_s(form))]),_v(" "),
            _c(
              'ul',
              _l(
                // this.arr
                (arr),
                function(item) {
              	  return _c('li',{key:item},[_v(_s(item))])
            	  }
              ),
              0
            )
          ]
        )
      }
    }
    

3 сублимация?

Прочитав это, я разбираюсь в своих мыслях и оглядываюсь на предыдущие выводы.Чувствую ли я просветление? ?