Vueソース学習:Arrayのデータリスニングについて

18074 ワード

サマリ
Vueの応答式はObjectであることはよく知られています.definePropertyはデータハイジャックを行います.しかし、それはObjectタイプに対して実現できますが、配列なら?set/get方式では無理です.
しかし,Vueの著者らは,Array型のモニタリング:ブロッキングを実現するための方法を使用した.
核心思想.
ブロックを作成することで配列自体のプロトタイプオブジェクトArrayを上書きする.prototype.
ブロッキング
Vueソースパスvue/src/core/observer/arrayを表示します.js.
/**
 * Vue        
 *   :           Array.prototype。
 *          Object,      Array.prototype  。                。
*/

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    })
}

//       
const arrayProto = Array.prototype
//    
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) {
      //    value                 
      const result = original.apply(this, args)
      //   value  observer()        __ob__  
      const ob = this.__ob__
      //                         ,               (    Observer)
      let inserted
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      //   Observe        
      if (inserted) ob.observeArray(inserted)
      //       
      ob.dep.notify()
      return result
    })
})


Vueがいつdata属性をObserverするかについて
Vueのソースコードに詳しい子供靴なら、すぐにVueの入り口ファイルvue/src/core/instance/indexを見つけることができるはずです.js.
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
//          $props, $data
//  Vue          : vm.$watch,vm.$set,vm.$delete
stateMixin(Vue)
//  Vue             : vm.$on, vm.$once ,vm.$off , vm.$emit
eventsMixin(Vue)
//  Vue               : vm.$forceUpdate, vm.destroy,       _update
lifecycleMixin(Vue)
//  Vue               : vm.$nextTick,       _render,         
renderMixin(Vue)

export default Vue

this.init()
ソースパス:vue/src/core/instance/init.js.

export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    //     
    const vm: Component = this
    // a uid
    //       
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    //     ,   Vue        performance.mark API      。
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      //              
      mark(startTag)
    }

    // a flag to avoid this being observed
    //      Vue  
    vm._isVue = true
    // merge options
    //       optionsMerge $options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    //        
    initLifecycle(vm)
    //        
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    //    State
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    //   
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

initState()
ソースパス:vue/src/core/instance/state.js.
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}


この時observeが現れたことに気づきます.
observe
ソースパス:vue/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
   // value               Observe  ,       
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    //     ,     Observer  
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

ブロッキングを使用するタイミング
Vueの応答システムにはObserveクラスがあります.ソースパス:vue/src/core/observer/index.js.
// can we use __proto__?
export const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

function copyAugment (target: Object, src: Object, keys: Array) {
  // target:    Observe   
  // src:         
  // keys: const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
  // keys:                  
  // const methodsToPatch = [
  //   'push',
  //   'pop',
  //   'shift',
  //   'unshift',
  //   'splice',
  //   'sort',
  //   'reverse'
  // ]
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

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()
    this.vmCount = 0
    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 through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   *               ,         Observer()
   */
  observeArray (items: Array) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

依存を収集する方法
Vueの中で本当にデータ応答式の処理をしているのはdefineReactive()です.defineReactiveメソッドは、オブジェクトのデータ属性をアクセサ属性に変換することです.すなわち、データ属性にget/setを設定します.
function dependArray (value: Array) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}


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

  //                 
  const property = Object.getOwnPropertyDescriptor(obj, key)
  //       :                      Object.defineProperty         。
  if (property && property.configurable === false) {
    return
  }

  //                        ,             get   set   
  //          Object.defineProperty           setter/getter
  //           set   get      ,          setter/getter   
  const getter = property && property.get
  const setter = property && property.set
  //       
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  //         ,      __ob__
  //  Vue.set   Vue.delete         。
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //    getter            ,               ,               
      //    getter        val       
      const value = getter ? getter.call(obj) : val
      // Dep.target     Watch        
      if (Dep.target) {
        //        dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            //    dependArray                  
            dependArray(value)
          }
        }
      }
      //         。
      return value
    },
    set: function reactiveSetter (newVal) {
      //       
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      //          ,   NaN  
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      //        setter,                   
      if (setter) {
        setter.call(obj, newVal)
      } else {
        //    
        val = newVal
      }
      //            ,                        ,
      //                ,           
      childOb = !shallow && observe(newVal)
      //   dep  watcher  
      dep.notify()
    }
  })
}


ストレージ配列依存リスト
なぜObserverインスタンスに依存を存在させる必要があるのか.すなわち
export class Observer {
    constructor (value: any) {
        ...
        this.dep = new Dep()
    }
}

まずgetterでObserverインスタンスにアクセスする必要があります
//     
let childOb = !shallow && observe(val)
...
if (childOb) {
  //   Observer   dep depend()      
  childOb.dep.depend()
  if (Array.isArray(value)) {
    //    dependArray                  
    dependArray(value)
  }
}

また、前述したブロッキングではObserverインスタンスを使用します.
methodsToPatch.forEach(function (method) {
    ...
    // this          
    //   __ob__    ?
    const ob = this.__ob__
    ...
    //   Observe        
    if (inserted) ob.observeArray(inserted)
    //       
    ob.dep.notify()
    ...
})

上記のことを考えます.ob__属性はどこから来ますか?
export class Observer {
    constructor () {
        ...
        this.dep = new Dep()
        //  vue          __ob__  ,         Observer  
        //              __ob__  Observer  
        //     __ob__  dep
        def(value, '__ob__', this)
        ...
    }
}

すべての属性が検出されると1つの__が打たれることを覚えておいてください.ob__のタグ、すなわち応答型データであることを示す.
Arrayに関する注意事項
JavaScriptの制限により、Vueは以下の変動する配列を検出できません.
  • インデックスを使用して直接アイテムを設定する場合、例えばvm.items[indexOfItem] = newValue
  • 配列の長さを変更すると、例えばvm.items.length = newLength

  • 解決方法は公式サイトのドキュメントを参照してください.
    Arrayに関する注意事項
    転載先:https://juejin.im/post/5cbc92906fb9a068890f3029