Vue 소스 탐구 - 데이터 귀속의 실현

Vue 소스 탐구 - 데이터 귀속의 실현
이 코드는 vue/src/core/observer/
데이터 귀속 실현의 논리 구조를 한 편 정리한 후에 Vue의 데이터 관찰 시스템의 역할과 각자의 기능에 대해 비교적 투철하게 이해했다. 이 편은 계속해서 원본 코드의 구체적인 실현을 자세하게 분석했다.
Observer
// Observer             。
//                    ,
//            
/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
 //       Observer  
export class Observer {
  //        ,    ,           
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  //              
  constructor (value: any) {
    //              value  
    this.value = value
    //     Dep        dep  
    this.dep = new Dep()
    //       vmCount 0
    this.vmCount = 0
    //            '__ob__‘   
    def(value, '__ob__', this)
    //          
    if (Array.isArray(value)) {
      //         __proto__  ,    augment  
      const augment = hasProto
        ? protoAugment
        : copyAugment
      //                  
      //                                
      augment(value, arrayMethods, arrayKeys)
      //         
      this.observeArray(value)
    } else {
      //                    
      this.walk(value)
    }
  }

  // walk              ,       
  //               
  /**
   * Walk through each property 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])
    }
  }

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

//          ,              __proto__       
//       ,      
// helpers

/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

// observe                
//               ,          
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
 //      observe  ,          data        
 //   Observer       
export function observe (value: any, asRootData: ?boolean): Observer | void {
  //            ,       
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  //   Observer      ob  
  let ob: Observer | void
  //        __ob__  ,     Observer  ,    ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    //   shouldObserve  ,        ,          
    //       ,      Vue  ,             ob
    //       Vue             _isVue    
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  //   asRootData   ob    ,ob.vmCount  
  if (asRootData && ob) {
    ob.vmCount++
  }
  //   ob
  return ob
}

// defineReactive             
/**
 * Define a reactive property on an Object.
 */
//      defineReactive  ,       obj,  key,  val,
//    setter  customSetter,        shallow    
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //         
  const dep = new Dep()

  //   obj      
  const property = Object.getOwnPropertyDescriptor(obj, key)
  //                
  if (property && property.configurable === false) {
    return
  }
  //            
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  //      getter   settter,      2   ,    val 
  //      Obserber walk        ,       
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  //            ,              ,       
  let childOb = !shallow && observe(val)
  //       
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    //   getter
    get: function reactiveGetter () {
      //       getter   value  getter      
      //          
      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
    },
    //   setter,    newVal  
    set: function reactiveSetter (newVal) {
      //       getter   value  getter      
      //          
      const value = getter ? getter.call(obj) : val
      //                null    
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      //         customSetter  ,   customSetter
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      //      setter     ,        
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //                    
      childOb = !shallow && observe(newVal)
      //       
      dep.notify()
    }
  })
}


//                        
// set                           ,     
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

// Delete                       ,     
/**
 * Delete a property and trigger change if necessary.
 */
export function del (target: Array | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

//               ,                
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
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)
    }
  }
}

Dep
let uid = 0

// dep       ,          
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
//      Dep 
export default class Dep {
  //     
  //     ,    watcher  
  static target: ?Watcher;
  // dep  Id
  id: number;
  // dep     /     
  subs: Array;

  //      
  constructor () {
    //          id
    this.id = uid++
    this.subs = []
  }

  //   addSub  ,  Watcher   sub  
  addSub (sub: Watcher) {
    //  subs       watcher
    this.subs.push(sub)
  }

  //   removeSub  ,  Watcher   sub  
  removeSub (sub: Watcher) {
    //  subs       watcher
    remove(this.subs, sub)
  }

  //   depend  ,      watcher    
  depend () {
    //    Wacther         Watcher   Dep.target
    //          Watcher,    Watcher addDep  
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  //   notify  ,    
  notify () {
    //         update      
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// Dep.target           watcher
//     ,          watcher   
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
// targetStack    watcher 
const targetStack = []

//      pushTarget  ,  Watcher     
export function pushTarget (_target: ?Watcher) {
  //       watcher   Dep.target
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

//      popTarget  
export function popTarget () {
  //     
  Dep.target = targetStack.pop()
}

Watcher
let uid = 0

// watcher       ,      ,                
//    $watch()             
/**
 * 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 
export default class Watcher {
  //     
  vm: Component; //   
  expression: string; //    
  cb: Function; //     
  id: number; // watcher  Id
  deep: boolean; //       
  user: boolean; //       
  computed: boolean; //       
  sync: boolean; //     
  dirty: boolean;  //        
  active: boolean; //      
  dep: Dep; //     
  deps: Array; //       
  newDeps: Array; //        
  depIds: SimpleSet;  //   id  
  newDepIds: SimpleSet; //    id  
  before: ?Function; //       
  getter: Function; //   getter
  value: any; //    

  //       
  //   vue  ,     ,    ,    ,       5   
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    //            
    this.vm = vm
    //                  _watcher  
    if (isRenderWatcher) {
      vm._watcher = this
    }
    //    vm._watchers   
    vm._watchers.push(this)
    //         ,         
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      //         false
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    //       getter  
    // parse expression for getter
    //      expOrFn          getter  
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      //              ,          
      //                             
      this.getter = parsePath(expOrFn)
      //    getter      
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
    //        ,  dep  
    if (this.computed) {
      this.value = undefined
      // 
      this.dep = new Dep()
    } else {
      //     get       
      this.value = this.get()
    }
  }

  //   getter,        
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    //       watcher  
    pushTarget(this)
    let value
    const vm = this.vm
    //     vm getter  
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      //       ,        watcher     
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        //       
        throw e
      }
    } finally {
      //     “  ”       ,                   
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        // traverse         ,                
        traverse(value)
      }
      //     
      popTarget()
      //     cleanupDeps  
      this.cleanupDeps()
    }
    //       
    return value
  }

  //     
  /**
   * Add a dependency to this directive.
   */
  //   addDep  ,  Dep        
  addDep (dep: Dep) {
    const id = dep.id
    //        ,      id           
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      //   dep          
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  //        
  /**
   * Clean up for dependency collection.
   */
  //   cleanupDeps  
  cleanupDeps () {
    let i = this.deps.length
    //       
    while (i--) {
      const dep = this.deps[i]
      //                      
      //             
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    //             ,
    //               
    //         
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  //      ,         
  /**
   * Subscriber   .
   * Will be called when a dependency changes.
   */
  //   update  
  update () {
    //        
    /* istanbul ignore else */
    if (this.computed) {
      //             :        
      //          ,                 ,
      //                   
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      //         
      if (this.dep.subs.length === 0) {
        //   dirty   true,                     ,
        //           dirty     true,           
        //            。
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        //         ,        
        //                  
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        //   getAndInvoke  ,           ,       
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      //       ,     run  
      this.run()
    } else {
      //               
      queueWatcher(this)
    }
  }

  //       ,       
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  //   run  
  run () {
    //              ,     getAndInvoke  
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  //   getAndInvoke  ,          
  getAndInvoke (cb: Function) {
    //       
    const value = this.get()
    //          ,        ,        
    //     ,    
    if (
      value !== this.value ||
      //             ,                 
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      //      ,     dirty  
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      //            ,               
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

  //           ,          
  /**
   * Evaluate and return the value of the watcher.
   * This only gets called for computed property watchers.
   */
  //   evaluate  
  evaluate () {
    //        ,     ,   
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  //           ,        
  /**
   * Depend on this watcher. Only for computed property watchers.
   */
  //   depend  
  depend () {
    //                   ,    
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }

  //        ,           
  /**
   * Remove self from all dependencies' subscriber list.
   */
  //   teardown  
  teardown () {
    //           ,    
    if (this.active) {
      //                    
      //                 
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      //         ,            
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      //               
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      //            
      this.active = false
    }
  }
}

이 편은 주로 원본 코드에 대한 해석으로 관찰 시스템의 원리편을 뒤집어 대조하여 이해할 수 있다.
여기에 Vue의 데이터 귀속이 구체적으로 실현된 소스 코드에 대한 개인적인 이해를 기록했습니다. 일부 세부적인 부분은 충분히 인식하지 못했을 수도 있습니다. 시스템에 있는 세 가지 유형을 살펴보면 관련성이 강하고 각자의 방법에서 자신과 다른 유형의 사례를 교차적으로 인용하면 머리가 어지럽고 눈앞이 캄캄할 수 있습니다. 어쨌든 전체적인 기능 논리에 대해 명확한 인식을 가지게 됩니다.앞으로 더 높은 차원으로 매진할 수 있을 것이다.

좋은 웹페이지 즐겨찾기