VuevueVue

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

2019-08-04  本文已影响143人  Kagashino

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中,没有任何访问当前实例属性的操作,根据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来实现监听,性能将节约不少,参见https://www.jianshu.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();
      }
    });
  }
上一篇下一篇

猜你喜欢

热点阅读