【Vue.js】不要把所有东西都放进data里了(至少2.0是如此)!

Vue组件实例中的data是我们再熟悉不过的东西了,用来存放需要绑定的数据
但是对于一些特定场景,data虽然能够达到预期效果,但是会存在一些问题
我们写下如下代码,建立一个名单,记录他们的名字,年龄和兴趣:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app"> </div>
  <script src="some-data.js"></script>
  <script>
    const template = `
      <div>
        <h3>data列表</h3>
        <ol>
          <li v-for="item in dataList">
            姓名:{{item.name}},年龄:{{item.age}},兴趣:{{item.hobby.join('、')}}
          </li>
        </ol>
      </div>
    `
    new Vue({
      el: '#app',
      data () {
        return {
          dataList: [
            { name: '张三', age: 33, hobby: ['唱','跳','rap','篮球'] },
            { name: '李四', age: 24, hobby: ['唱','跳','rap','篮球'] },
            { name: '王五', age: 11, hobby: ['唱','跳','rap','篮球'] },
            { name: '赵六', age: 54, hobby: ['唱','跳','rap','篮球'] },
            { name: '孙七', age: 23, hobby: ['唱','跳','rap','篮球'] },
            { name: '吴八', age: 55, hobby: ['唱','跳','rap','篮球'] }
          ],
        }
      },
      mounted () {
        console.table(this.dataList) // 打印列表形式的dataList
        console.log(this.dataList) // 打印字面量
      },
      template
    })
  </script>
</body>
</html>

Vue通过data生成我们能用的绑定数据,大概走了以下几个步骤:
1.从 initData[1]方法 中获取你传入的data,校验data是否合法
2.调用observe[2]函数,新建一个Observer[3]实例,将data变成一个响应式对象,而且为data添加 __ob__属性,指向当前Observer实例
3.Observer保存了你的value值、数据依赖dep[4]和vue组件实例数vmCount
4.对你的data调用defineReactive$$1[5]递归地监所有的key/value(你在data中声明的),使你的key/value都有自己的dep, getter[6]setter[7]

我们忽略html的内容,重点放在这个dataList上(我用2种不同的形式打印了dataList),如上述步骤2、3、4所说,data中每个key/value值(包括嵌套的对象和数组)都添加了一个Observer:

datalist

之前我们说滥用data会产生一些问题,问题如下:
设想一下这样的场景,如果你的data属于纯展示的数据,你根本不需要对这个数据进行监听,特别是一些比这个例子还复杂的列表/对象,放进data中纯属浪费性能。
那怎么办才好?
放进computed中
还是刚才的代码,我们创建一个数据一样的list,丢进computed里:

computed: {
        computedList () {
          return [
            { name: '张三', age: 33, hobby: ['唱','跳','rap','篮球'] },
            { name: '李四', age: 24, hobby: ['唱','跳','rap','篮球'] },
            { name: '王五', age: 11, hobby: ['唱','跳','rap','篮球'] },
            { name: '赵六', age: 54, hobby: ['唱','跳','rap','篮球'] },
            { name: '孙七', age: 23, hobby: ['唱','跳','rap','篮球'] },
            { name: '吴八', age: 55, hobby: ['唱','跳','rap','篮球'] }
          ]
        }
      },

打印computedList,你得到了一个没有被监听的列表


computedList

为什么computed没有监听我的data
因为我们的computedList中,没有依赖,即没有任何访问响应式数据(如data/props上的属性/其他依赖过的computed等)的操作,根据Vue的依赖收集机制,只有在computed中引用了实例属性,触发了属性的getter,getter会把依赖收集起来,等到setter调用后,更新相关的依赖项

我们来看官方文档对computed的说明:

computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }

这里强调的是

所以,对于任何复杂逻辑,你都应当使用计算属性

但是很少有人注意到api说明中的这一句:

计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。

也就是说,对于纯展示的数据,使用computed会更加节约你的内存
另外 computed 其实是Watcher[6]的实现,有空的话会更新这部分的内容

为什么说“至少2.0是如此”

因为3.0将使用Proxy来实现监听,性能将节约不少,参见//www.greatytc.com/p/f99822cde47c

源码附录 (v2.6.10)

[1] initData: 检查data的合法性(比如是否作为函数返回、是否冲突或者没有提供data属性),初始化data

function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) {
      data = {};
      warn(
        'data functions should return an object:\n' +
        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key);
      }
    }
    // observe data
    observe(data, true /* asRootData */);
  }

[2] observe: 对当前对象新建一个Observer实例

function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
  }

[3] Observer类:为对象声明依赖,和响应式方法,同时对数组做兼容处理(Vue可以通过调用使原数组变化的方法如push、reverse、sort等触发监听)

/**
 * 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.
 */
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) {
        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])
    }
  }

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

[4] dep 依赖类Dep的实例实例,当notify被setter调用时触发Watcher更新,建议先看[5][6][7]再回过头来看参考

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

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

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

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

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

[5] `defineReactive$$1 [6] getter [7] setter,数据绑定的核心方法:通过调用Object.defineProperty对对象中的每一个key添加dep依赖和设置getter和setter,getter触发依赖收集,setter触发依赖更新

/**
   * Define a reactive property on an Object.
   */
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容