Просмотр исходного кода

v-for: no longer pre-convert values into arrays before piping it through filters

Evan You 10 лет назад
Родитель
Сommit
03b9d41574

+ 4 - 2
src/directive.js

@@ -99,11 +99,12 @@ Directive.prototype._bind = function () {
     } else {
       this._update = noop
     }
-    // pre-process hook called before the value is piped
-    // through the filters. used in v-for.
     var preProcess = this._preProcess
       ? _.bind(this._preProcess, this)
       : null
+    var postProcess = this._postProcess
+      ? _.bind(this._postProcess, this)
+      : null
     var watcher = this._watcher = new Watcher(
       this.vm,
       this.expression,
@@ -113,6 +114,7 @@ Directive.prototype._bind = function () {
         twoWay: this.twoWay,
         deep: this.deep,
         preProcess: preProcess,
+        postProcess: postProcess,
         scope: this._scope
       }
     )

+ 37 - 31
src/directives/public/for.js

@@ -61,12 +61,6 @@ module.exports = {
   },
 
   update: function (data) {
-    if (process.env.NODE_ENV !== 'production' && !_.isArray(data)) {
-      _.warn(
-        'v-for pre-converts Objects into Arrays, and ' +
-        'v-for filters should always return Arrays.'
-      )
-    }
     this.diff(data)
     this.updateRef()
     this.updateModel()
@@ -87,8 +81,14 @@ module.exports = {
    */
 
   diff: function (data) {
+    // check if the Array was converted from an Object
+    var item = data[0]
+    var convertedFromObject = this.fromObject =
+      isObject(item) &&
+      item.hasOwnProperty('$key') &&
+      item.hasOwnProperty('$value')
+
     var idKey = this.idKey
-    var converted = this.converted
     var oldFrags = this.frags
     var frags = this.frags = new Array(data.length)
     var alias = this.alias
@@ -96,7 +96,7 @@ module.exports = {
     var end = this.end
     var inDoc = _.inDoc(start)
     var init = !oldFrags
-    var i, l, frag, item, key, value, primitive
+    var i, l, frag, key, value, primitive
 
     // First pass, go through the new Array and fill up
     // the new frags array. If a piece of data has a cached
@@ -104,8 +104,8 @@ module.exports = {
     // instance.
     for (i = 0, l = data.length; i < l; i++) {
       item = data[i]
-      key = converted ? item.$key : null
-      value = converted ? item.$value : item
+      key = convertedFromObject ? item.$key : null
+      value = convertedFromObject ? item.$value : item
       primitive = !isObject(value)
       frag = !init && this.getCachedFrag(value, i, key)
       if (frag) { // reusable fragment
@@ -126,7 +126,7 @@ module.exports = {
         }
         // update data for track-by, object repeat &
         // primitive values.
-        if (idKey || converted || primitive) {
+        if (idKey || convertedFromObject || primitive) {
           frag.scope[alias] = value
         }
       } else { // new isntance
@@ -230,7 +230,7 @@ module.exports = {
     if (!ref) return
     var hash = (this._scope || this.vm).$refs
     var refs
-    if (!this.converted) {
+    if (!this.fromObject) {
       refs = this.frags.map(findVmFromFrag)
     } else {
       refs = {}
@@ -458,29 +458,28 @@ module.exports = {
 
   /**
    * Pre-process the value before piping it through the
-   * filters, and convert non-Array objects to arrays.
-   *
-   * This function will be bound to this directive instance
-   * and passed into the watcher.
-   *
-   * @param {*} value
-   * @return {Array}
-   * @private
+   * filters. This is passed to and called by the watcher.
    */
 
   _preProcess: function (value) {
     // regardless of type, store the un-filtered raw value.
     this.rawValue = value
-    var type = this.rawType = typeof value
-    if (!_.isPlainObject(value)) {
-      this.converted = false
-      if (type === 'number') {
-        value = range(value)
-      } else if (type === 'string') {
-        value = _.toArray(value)
-      }
-      return value || []
-    } else {
+    return value
+  },
+
+  /**
+   * Post-process the value after it has been piped through
+   * the filters. This is passed to and called by the watcher.
+   *
+   * It is necessary for this to be called during the
+   * wathcer's dependency collection phase because we want
+   * the v-for to update when the source Object is mutated.
+   */
+
+  _postProcess: function (value) {
+    if (_.isArray(value)) {
+      return value
+    } else if (_.isPlainObject(value)) {
       // convert plain object to array.
       var keys = Object.keys(value)
       var i = keys.length
@@ -493,8 +492,15 @@ module.exports = {
           $value: value[key]
         }
       }
-      this.converted = true
       return res
+    } else {
+      var type = typeof value
+      if (type === 'number') {
+        value = range(value)
+      } else if (type === 'string') {
+        value = _.toArray(value)
+      }
+      return value || []
     }
   },
 

+ 22 - 7
src/filters/array-filters.js

@@ -1,5 +1,6 @@
 var _ = require('../util')
 var Path = require('../parsers/path')
+var toArray = require('../directives/public/for')._postProcess
 
 /**
  * Filter filter for arrays
@@ -10,6 +11,7 @@ var Path = require('../parsers/path')
  */
 
 exports.filterBy = function (arr, search, delimiter /* ...dataKeys */) {
+  arr = toArray(arr)
   if (search == null) {
     return arr
   }
@@ -25,15 +27,27 @@ exports.filterBy = function (arr, search, delimiter /* ...dataKeys */) {
   var keys = _.toArray(arguments, n).reduce(function (prev, cur) {
     return prev.concat(cur)
   }, [])
-  return arr.filter(function (item) {
-    if (keys.length) {
-      return keys.some(function (key) {
-        return contains(Path.get(item, key), search)
-      })
+  var res = []
+  var item, key, val, j
+  for (var i = 0, l = arr.length; i < l; i++) {
+    item = arr[i]
+    val = (item && item.$value) || item
+    j = keys.length
+    if (j) {
+      while (j--) {
+        key = keys[j]
+        if ((key === '$key' && contains(item.$key, search)) ||
+            contains(Path.get(val, key), search)) {
+          res.push(item)
+        }
+      }
     } else {
-      return contains(item, search)
+      if (contains(item, search)) {
+        res.push(item)
+      }
     }
-  })
+  }
+  return res
 }
 
 /**
@@ -44,6 +58,7 @@ exports.filterBy = function (arr, search, delimiter /* ...dataKeys */) {
  */
 
 exports.orderBy = function (arr, sortKey, reverse) {
+  arr = toArray(arr)
   if (!sortKey) {
     return arr
   }

+ 4 - 0
src/watcher.js

@@ -21,6 +21,7 @@ var uid = 0
  *                 - {Boolean} sync
  *                 - {Boolean} lazy
  *                 - {Function} [preProcess]
+ *                 - {Function} [postProcess]
  * @constructor
  */
 
@@ -110,6 +111,9 @@ Watcher.prototype.get = function () {
   if (this.filters) {
     value = scope._applyFilters(value, null, this.filters, false)
   }
+  if (this.postProcess) {
+    value = this.postProcess(value)
+  }
   this.afterGet()
   return value
 }

+ 22 - 16
test/unit/specs/directives/public/for/for_spec.js

@@ -88,6 +88,28 @@ if (_.inBrowser) {
       expect(el.innerHTML).toBe('<div>aaa</div>')
     })
 
+    it('filter converting array to object', function () {
+      new Vue({
+        el: el,
+        data: {
+          items: [
+            { msg: 'aaa' },
+            { msg: 'bbb' }
+          ]
+        },
+        template: '<div v-for="item in items | test">{{item.msg}} {{$key}}</div>',
+        filters: {
+          test: function (val) {
+            return {
+              a: val[0],
+              b: val[1]
+            }
+          }
+        }
+      })
+      expect(el.innerHTML).toBe('<div>aaa a</div><div>bbb b</div>')
+    })
+
     it('component', function (done) {
       var vm = new Vue({
         el: el,
@@ -660,22 +682,6 @@ if (_.inBrowser) {
       expect(hasWarned(_, 'It seems you are using two-way binding')).toBe(true)
     })
 
-    it('warn filters that return non-Array values', function () {
-      new Vue({
-        el: el,
-        template: '<div v-for="item in items | test"></div>',
-        data: {
-          items: []
-        },
-        filters: {
-          test: function (val) {
-            return {}
-          }
-        }
-      })
-      expect(hasWarned(_, 'should always return Arrays')).toBe(true)
-    })
-
     it('nested track by', function (done) {
       var vm = new Vue({
         el: el,