text: /* jshint proto:true */
  
  var Emitter = require('./emitter'),
    utils = require('./utils'),
    // cache methods
    def = utils.defProtected,
    isObject = utils.isObject,
    isArray = Array.isArray,
    hasOwn = {}.hasOwnProperty,
    oDef = Object.defineProperty,
    slice = [].slice,
    // fix for IE + __proto__ problem
    // define methods as inenumerable if __proto__ is present,
    // otherwise enumerable so we can loop through and manually
    // attach to array instances
    hasProto = {}.__proto__
  
  // Array Mutation Handlers & Augmentations ------------------------------------
  
  // The proxy prototype to replace the __proto__ of
  // an observed array
  var ArrayProxy = Object.create(Array.prototype)
  
  // intercept mutation methods
  ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
    watchMutation
  )
  
  // Augment the ArrayProxy with convenience methods
  def(
    ArrayProxy,
    '$set',
    function (index, data) {
      return this.splice(index, 1, data)[0]
    },
    !hasProto
  )
  
  def(
    ArrayProxy,
    '$remove',
    function (index) {
      if (typeof index !== 'number') {
        index = this.indexOf(index)
      }
      if (index > -1) {
        return this.splice(index, 1)[0]
      }
    },
    !hasProto
  )
  
  /**
   *  Intercep a mutation event so we can emit the mutation info.
   *  we also analyze what elements are added/removed and link/unlink
   *  them with the parent Array.
   */
  function watchMutation(method) {
    def(
      ArrayProxy,
      method,
      function () {
        var args = slice.call(arguments),
          result = Array.prototype[method].apply(this, args),
          inserted,
          removed
  
        // determine new / removed elements
        if (method === 'push' || method === 'unshift') {
          inserted = args
        } else if (method === 'pop' || method === 'shift') {
          removed = [result]
        } else if (method === 'splice') {
          inserted = args.slice(2)
          removed = result
        }
  
        // link & unlink
        linkArrayElements(this, inserted)
        unlinkArrayElements(this, removed)
  
        // emit the mutation event
        this.__emitter__.emit('mutate', '', this, {
          method: method,
          args: args,
          result: result,
          inserted: inserted,
          removed: removed,
        })
  
        return result
      },
      !hasProto
    )
  }
  
  /**
   *  Link new elements to an Array, so when they change
   *  and emit events, the owner Array can be notified.
   */
  function linkArrayElements(arr, items) {
    if (items) {
      var i = items.length,
        item,
        owners
      while (i--) {
        item = items[i]
        if (isWatchable(item)) {
          // if object is not converted for observing
          // convert it...
          if (!item.__emitter__) {
            convert(item)
            watch(item)
          }
          owners = item.__emitter__.owners
          if (owners.indexOf(arr) < 0) {
            owners.push(arr)
          }
        }
      }
    }
  }
  
  /**
   *  Unlink removed elements from the ex-owner Array.
   */
  function unlinkArrayElements(arr, items) {
    if (items) {
      var i = items.length,
        item
      while (i--) {
        item = items[i]
        if (item && item.__emitter__) {
          var owners = item.__emitter__.owners
          if (owners) owners.splice(owners.indexOf(arr))
        }
      }
    }
  }
  
  // Object add/delete key augmentation -----------------------------------------
  
  var ObjProxy = Object.create(Object.prototype)
  
  def(
    ObjProxy,
    '$add',
    function (key, val) {
      if (hasOwn.call(this, key)) return
      this[key] = val
      convertKey(this, key, true)
    },
    !hasProto
  )
  
  def(
    ObjProxy,
    '$delete',
    function (key) {
      if (!hasOwn.call(this, key)) return
      // trigger set events
      this[key] = undefined
      delete this[key]
      this.__emitter__.emit('delete', key)
    },
    !hasProto
  )
  
  // Watch Helpers --------------------------------------------------------------
  
  /**
   * Check if a value is watchable
   * 检查一个值是否可观测
   */
  function isWatchable(obj) {
    return typeof obj === 'object' && obj && !obj.$compiler
  }
  
  /**
   * Convert an Object/Array to give it a change emitter.
   * 挂载 emitter
   */
  function convert(obj) {
    if (obj.__emitter__) return true
    var emitter = new Emitter()
    def(obj, '__emitter__', emitter)
    emitter
      .on('set', function (key, val, propagate) {
        if (propagate) propagateChange(obj)
      })
      .on('mutate', function () {
        propagateChange(obj)
      })
    emitter.values = utils.hash()
    emitter.owners = []
    return false
  }
  
  /**
   * Propagate an array element's change to its owner arrays
   * 传递变动响应
   */
  function propagateChange(obj) {
    var owners = obj.__emitter__.owners,
      i = owners.length
    while (i--) {
      owners[i].__emitter__.emit('set', '', '', true)
    }
  }
  
  /**
   *  Watch target based on its type
   */
  function watch(obj) {
    if (isArray(obj)) {
      watchArray(obj)
    } else {
      watchObject(obj)
    }
  }
  
  /**
   *  Augment target objects with modified
   *  methods
   */
  function augment(target, src) {
    if (hasProto) {
      target.__proto__ = src
    } else {
      for (var key in src) {
        def(target, key, src[key])
      }
    }
  }
  
  /**
   *  Watch an Object, recursive.
   */
  function watchObject(obj) {
    augment(obj, ObjProxy)
    for (var key in obj) {
      convertKey(obj, key)
    }
  }
  
  /**
   *  Watch an Array, overload mutation methods
   *  and add augmentations by intercepting the prototype chain
   */
  function watchArray(arr) {
    augment(arr, ArrayProxy)
    linkArrayElements(arr, arr)
  }
  
  /**
   *  Define accessors for a property on an Object
   *  so it emits get/set events.
   *  Then watch the value itself.
   */
  function convertKey(obj, key, propagate) {
    var keyPrefix = key.charAt(0)
    if (keyPrefix === '$' || keyPrefix === '_') {
      return
    }
    // emit set on bind
    // this means when an object is observed it will emit
    // a first batch of set events.
    var emitter = obj.__emitter__,
      values = emitter.values
  
    init(obj[key], propagate)
  
    oDef(obj, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        var value = values[key]
        // only emit get on tip values
        if (pub.shouldGet) {
          emitter.emit('get', key)
        }
        return value
      },
      set: function (newVal) {
        var oldVal = values[key]
        unobserve(oldVal, key, emitter)
        copyPaths(newVal, oldVal)
        // an immediate property should notify its parent
        // to emit set for itself too
        init(newVal, true)
      },
    })
  
    function init(val, propagate) {
      values[key] = val
      emitter.emit('set', key, val, propagate)
      if (isArray(val)) {
        emitter.emit('set', key + '.length', val.length, propagate)
      }
      observe(val, key, emitter)
    }
  }
  
  /**
   * When a value that is already converted is
   * observed again by another observer, we can skip
   * the watch conversion and simply emit set event for
   * all of its properties.
   * 当另一个观察者再次观察到已经转换的值时，
   * 我们可以跳过观察转换并简单地为其所有属性发出 set 事件。
   */
  function emitSet(obj) {
    var emitter = obj && obj.__emitter__
    if (!emitter) return
    if (isArray(obj)) {
      emitter.emit('set', 'length', obj.length)
    } else {
      var key, val
      for (key in obj) {
        val = obj[key]
        emitter.emit('set', key, val)
        emitSet(val)
      }
    }
  }
  
  /**
   * Make sure all the paths in an old object exists
   * in a new object.
   * So when an object changes, all missing keys will
   * emit a set event with undefined value.
   * 确保旧对象中的所有路径都存在于新对象中。
   * 所以当一个对象发生变化时，
   * 所有丢失的键都会发出一个带有未定义值的 set 事件。
   */
  function copyPaths(newObj, oldObj) {
    if (!isObject(newObj) || !isObject(oldObj)) {
      return
    }
    var path, oldVal, newVal
    for (path in oldObj) {
      if (!hasOwn.call(newObj, path)) {
        oldVal = oldObj[path]
        if (isArray(oldVal)) {
          newObj[path] = []
        } else if (isObject(oldVal)) {
          newVal = newObj[path] = {}
          copyPaths(newVal, oldVal)
        } else {
          newObj[path] = undefined
        }
      }
    }
  }
  
  /**
   *  walk along a path and make sure it can be accessed
   *  and enumerated in that object
   */
  function ensurePath(obj, key) {
    var path = key.split('.'),
      sec
    for (var i = 0, d = path.length - 1; i < d; i++) {
      sec = path[i]
      if (!obj[sec]) {
        obj[sec] = {}
        if (obj.__emitter__) convertKey(obj, sec)
      }
      obj = obj[sec]
    }
    if (isObject(obj)) {
      sec = path[i]
      if (!hasOwn.call(obj, sec)) {
        obj[sec] = undefined
        if (obj.__emitter__) convertKey(obj, sec)
      }
    }
  }
  
  // Main API Methods -----------------------------------------------------------
  
  /**
   *  Observe an object with a given path,
   *  and proxy get/set/mutate events to the provided observer.
   */
  function observe(obj, rawPath, observer) {
    if (!isWatchable(obj)) return
  
    var path = rawPath ? rawPath + '.' : '',
      alreadyConverted = convert(obj),
      emitter = obj.__emitter__
  
    // setup proxy listeners on the parent observer.
    // we need to keep reference to them so that they
    // can be removed when the object is un-observed.
    observer.proxies = observer.proxies || {}
    var proxies = (observer.proxies[path] = {
      get: function (key) {
        observer.emit('get', path + key)
      },
      set: function (key, val, propagate) {
        if (key) observer.emit('set', path + key, val)
        // also notify observer that the object itself changed
        // but only do so when it's a immediate property. this
        // avoids duplicate event firing.
        if (rawPath && propagate) {
          observer.emit('set', rawPath, obj, true)
        }
      },
      mutate: function (key, val, mutation) {
        // if the Array is a root value
        // the key will be null
        var fixedPath = key ? path + key : rawPath
        observer.emit('mutate', fixedPath, val, mutation)
        // also emit set for Array's length when it mutates
        var m = mutation.method
        if (m !== 'sort' && m !== 'reverse') {
          observer.emit('set', fixedPath + '.length', val.length)
        }
      },
    })
  
    // attach the listeners to the child observer.
    // now all the events will propagate upwards.
    emitter
      .on('get', proxies.get)
      .on('set', proxies.set)
      .on('mutate', proxies.mutate)
  
    if (alreadyConverted) {
      // for objects that have already been converted,
      // emit set events for everything inside
      emitSet(obj)
    } else {
      watch(obj)
    }
  }
  
  /**
   * Cancel observation, turn off the listeners.
   * 取消观察，关闭监听器
   */
  function unobserve(obj, path, observer) {
    if (!obj || !obj.__emitter__) return
  
    path = path ? path + '.' : ''
    var proxies = observer.proxies[path]
    if (!proxies) return
  
    // turn off listeners
    obj.__emitter__
      .off('get', proxies.get)
      .off('set', proxies.set)
      .off('mutate', proxies.mutate)
  
    // remove reference
    observer.proxies[path] = null
  }
  
  // Expose API -----------------------------------------------------------------
  
  var pub = (module.exports = {
    // whether to emit get events
    // only enabled during dependency parsing
    shouldGet: false,
  
    observe: observe,
    unobserve: unobserve,
    ensurePath: ensurePath,
    copyPaths: copyPaths,
    watch: watch,
    convert: convert,
    convertKey: convertKey,
  })
  