github.com/insionng/yougam@v0.0.0-20170714101924-2bc18d833463/public/libs/vue-1.0.24/src/watcher.js (about)

     1  import config from './config'
     2  import Dep from './observer/dep'
     3  import { parseExpression } from './parsers/expression'
     4  import { pushWatcher } from './batcher'
     5  import {
     6    extend,
     7    warn,
     8    isArray,
     9    isObject,
    10    nextTick,
    11    _Set as Set
    12  } from './util/index'
    13  
    14  let uid = 0
    15  
    16  /**
    17   * A watcher parses an expression, collects dependencies,
    18   * and fires callback when the expression value changes.
    19   * This is used for both the $watch() api and directives.
    20   *
    21   * @param {Vue} vm
    22   * @param {String|Function} expOrFn
    23   * @param {Function} cb
    24   * @param {Object} options
    25   *                 - {Array} filters
    26   *                 - {Boolean} twoWay
    27   *                 - {Boolean} deep
    28   *                 - {Boolean} user
    29   *                 - {Boolean} sync
    30   *                 - {Boolean} lazy
    31   *                 - {Function} [preProcess]
    32   *                 - {Function} [postProcess]
    33   * @constructor
    34   */
    35  
    36  export default function Watcher (vm, expOrFn, cb, options) {
    37    // mix in options
    38    if (options) {
    39      extend(this, options)
    40    }
    41    var isFn = typeof expOrFn === 'function'
    42    this.vm = vm
    43    vm._watchers.push(this)
    44    this.expression = expOrFn
    45    this.cb = cb
    46    this.id = ++uid // uid for batching
    47    this.active = true
    48    this.dirty = this.lazy // for lazy watchers
    49    this.deps = []
    50    this.newDeps = []
    51    this.depIds = new Set()
    52    this.newDepIds = new Set()
    53    this.prevError = null // for async error stacks
    54    // parse expression for getter/setter
    55    if (isFn) {
    56      this.getter = expOrFn
    57      this.setter = undefined
    58    } else {
    59      var res = parseExpression(expOrFn, this.twoWay)
    60      this.getter = res.get
    61      this.setter = res.set
    62    }
    63    this.value = this.lazy
    64      ? undefined
    65      : this.get()
    66    // state for avoiding false triggers for deep and Array
    67    // watchers during vm._digest()
    68    this.queued = this.shallow = false
    69  }
    70  
    71  /**
    72   * Evaluate the getter, and re-collect dependencies.
    73   */
    74  
    75  Watcher.prototype.get = function () {
    76    this.beforeGet()
    77    var scope = this.scope || this.vm
    78    var value
    79    try {
    80      value = this.getter.call(scope, scope)
    81    } catch (e) {
    82      if (
    83        process.env.NODE_ENV !== 'production' &&
    84        config.warnExpressionErrors
    85      ) {
    86        warn(
    87          'Error when evaluating expression ' +
    88          '"' + this.expression + '": ' + e.toString(),
    89          this.vm
    90        )
    91      }
    92    }
    93    // "touch" every property so they are all tracked as
    94    // dependencies for deep watching
    95    if (this.deep) {
    96      traverse(value)
    97    }
    98    if (this.preProcess) {
    99      value = this.preProcess(value)
   100    }
   101    if (this.filters) {
   102      value = scope._applyFilters(value, null, this.filters, false)
   103    }
   104    if (this.postProcess) {
   105      value = this.postProcess(value)
   106    }
   107    this.afterGet()
   108    return value
   109  }
   110  
   111  /**
   112   * Set the corresponding value with the setter.
   113   *
   114   * @param {*} value
   115   */
   116  
   117  Watcher.prototype.set = function (value) {
   118    var scope = this.scope || this.vm
   119    if (this.filters) {
   120      value = scope._applyFilters(
   121        value, this.value, this.filters, true)
   122    }
   123    try {
   124      this.setter.call(scope, scope, value)
   125    } catch (e) {
   126      if (
   127        process.env.NODE_ENV !== 'production' &&
   128        config.warnExpressionErrors
   129      ) {
   130        warn(
   131          'Error when evaluating setter ' +
   132          '"' + this.expression + '": ' + e.toString(),
   133          this.vm
   134        )
   135      }
   136    }
   137    // two-way sync for v-for alias
   138    var forContext = scope.$forContext
   139    if (forContext && forContext.alias === this.expression) {
   140      if (forContext.filters) {
   141        process.env.NODE_ENV !== 'production' && warn(
   142          'It seems you are using two-way binding on ' +
   143          'a v-for alias (' + this.expression + '), and the ' +
   144          'v-for has filters. This will not work properly. ' +
   145          'Either remove the filters or use an array of ' +
   146          'objects and bind to object properties instead.',
   147          this.vm
   148        )
   149        return
   150      }
   151      forContext._withLock(function () {
   152        if (scope.$key) { // original is an object
   153          forContext.rawValue[scope.$key] = value
   154        } else {
   155          forContext.rawValue.$set(scope.$index, value)
   156        }
   157      })
   158    }
   159  }
   160  
   161  /**
   162   * Prepare for dependency collection.
   163   */
   164  
   165  Watcher.prototype.beforeGet = function () {
   166    Dep.target = this
   167  }
   168  
   169  /**
   170   * Add a dependency to this directive.
   171   *
   172   * @param {Dep} dep
   173   */
   174  
   175  Watcher.prototype.addDep = function (dep) {
   176    var id = dep.id
   177    if (!this.newDepIds.has(id)) {
   178      this.newDepIds.add(id)
   179      this.newDeps.push(dep)
   180      if (!this.depIds.has(id)) {
   181        dep.addSub(this)
   182      }
   183    }
   184  }
   185  
   186  /**
   187   * Clean up for dependency collection.
   188   */
   189  
   190  Watcher.prototype.afterGet = function () {
   191    Dep.target = null
   192    var i = this.deps.length
   193    while (i--) {
   194      var dep = this.deps[i]
   195      if (!this.newDepIds.has(dep.id)) {
   196        dep.removeSub(this)
   197      }
   198    }
   199    var tmp = this.depIds
   200    this.depIds = this.newDepIds
   201    this.newDepIds = tmp
   202    this.newDepIds.clear()
   203    tmp = this.deps
   204    this.deps = this.newDeps
   205    this.newDeps = tmp
   206    this.newDeps.length = 0
   207  }
   208  
   209  /**
   210   * Subscriber interface.
   211   * Will be called when a dependency changes.
   212   *
   213   * @param {Boolean} shallow
   214   */
   215  
   216  Watcher.prototype.update = function (shallow) {
   217    if (this.lazy) {
   218      this.dirty = true
   219    } else if (this.sync || !config.async) {
   220      this.run()
   221    } else {
   222      // if queued, only overwrite shallow with non-shallow,
   223      // but not the other way around.
   224      this.shallow = this.queued
   225        ? shallow
   226          ? this.shallow
   227          : false
   228        : !!shallow
   229      this.queued = true
   230      // record before-push error stack in debug mode
   231      /* istanbul ignore if */
   232      if (process.env.NODE_ENV !== 'production' && config.debug) {
   233        this.prevError = new Error('[vue] async stack trace')
   234      }
   235      pushWatcher(this)
   236    }
   237  }
   238  
   239  /**
   240   * Batcher job interface.
   241   * Will be called by the batcher.
   242   */
   243  
   244  Watcher.prototype.run = function () {
   245    if (this.active) {
   246      var value = this.get()
   247      if (
   248        value !== this.value ||
   249        // Deep watchers and watchers on Object/Arrays should fire even
   250        // when the value is the same, because the value may
   251        // have mutated; but only do so if this is a
   252        // non-shallow update (caused by a vm digest).
   253        ((isObject(value) || this.deep) && !this.shallow)
   254      ) {
   255        // set new value
   256        var oldValue = this.value
   257        this.value = value
   258        // in debug + async mode, when a watcher callbacks
   259        // throws, we also throw the saved before-push error
   260        // so the full cross-tick stack trace is available.
   261        var prevError = this.prevError
   262        /* istanbul ignore if */
   263        if (process.env.NODE_ENV !== 'production' &&
   264            config.debug && prevError) {
   265          this.prevError = null
   266          try {
   267            this.cb.call(this.vm, value, oldValue)
   268          } catch (e) {
   269            nextTick(function () {
   270              throw prevError
   271            }, 0)
   272            throw e
   273          }
   274        } else {
   275          this.cb.call(this.vm, value, oldValue)
   276        }
   277      }
   278      this.queued = this.shallow = false
   279    }
   280  }
   281  
   282  /**
   283   * Evaluate the value of the watcher.
   284   * This only gets called for lazy watchers.
   285   */
   286  
   287  Watcher.prototype.evaluate = function () {
   288    // avoid overwriting another watcher that is being
   289    // collected.
   290    var current = Dep.target
   291    this.value = this.get()
   292    this.dirty = false
   293    Dep.target = current
   294  }
   295  
   296  /**
   297   * Depend on all deps collected by this watcher.
   298   */
   299  
   300  Watcher.prototype.depend = function () {
   301    var i = this.deps.length
   302    while (i--) {
   303      this.deps[i].depend()
   304    }
   305  }
   306  
   307  /**
   308   * Remove self from all dependencies' subcriber list.
   309   */
   310  
   311  Watcher.prototype.teardown = function () {
   312    if (this.active) {
   313      // remove self from vm's watcher list
   314      // this is a somewhat expensive operation so we skip it
   315      // if the vm is being destroyed or is performing a v-for
   316      // re-render (the watcher list is then filtered by v-for).
   317      if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
   318        this.vm._watchers.$remove(this)
   319      }
   320      var i = this.deps.length
   321      while (i--) {
   322        this.deps[i].removeSub(this)
   323      }
   324      this.active = false
   325      this.vm = this.cb = this.value = null
   326    }
   327  }
   328  
   329  /**
   330   * Recrusively traverse an object to evoke all converted
   331   * getters, so that every nested property inside the object
   332   * is collected as a "deep" dependency.
   333   *
   334   * @param {*} val
   335   */
   336  
   337  const seenObjects = new Set()
   338  function traverse (val, seen) {
   339    let i, keys
   340    if (!seen) {
   341      seen = seenObjects
   342      seen.clear()
   343    }
   344    const isA = isArray(val)
   345    const isO = isObject(val)
   346    if (isA || isO) {
   347      if (val.__ob__) {
   348        var depId = val.__ob__.dep.id
   349        if (seen.has(depId)) {
   350          return
   351        } else {
   352          seen.add(depId)
   353        }
   354      }
   355      if (isA) {
   356        i = val.length
   357        while (i--) traverse(val[i], seen)
   358      } else if (isO) {
   359        keys = Object.keys(val)
   360        i = keys.length
   361        while (i--) traverse(val[keys[i]], seen)
   362      }
   363    }
   364  }