1

初入vue 的大门,对vue 在data 中定义一个值,在DOM 中通过双花括号{{}} 就可以访问的到,对此我是很好奇的,本着好奇的心思,打开了vue 的源码,探索这个奇妙的过程。
问题的根源就在于这个方法:
Object.defineProperty()定义对象的属性相关描述符, 其中的set和get函数对于完成数据双向绑定起到了至关重要的作用。
下面我们就通过一个简单的例子来解释一个这个方法:

var obj = {
      name: "south Joe"
    }

    Object.defineProperty(obj, 'foo', {
      get: function () {
        console.log('将要读取obj.name属性');
      }, 
      set: function (newVal) {
        console.log('当前值为', newVal);
      }
    });

    obj.name; // 将要读取obj.foo属性
    obj.name = 'yangyang'; // 当前值为 name

那么解释完这个属性之后呢,我们就深入vue 源码,了解一下在源码背后做了一些什么呢?
我们调用Vue,并传入一个data;

import Vue from 'vue'
new Vue({
  el: '#app',
  mounted() {
    console.log(this.message);
    console.log(this._data.message);
  },
  data() {
    return {
      message: 'hello vue'
    }
  }
})

我们实例化一个Vue 的实例,并传入了一个对象;
调用了源码中的这个方法:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  // 执行_init 方法;_init 是Vue 原型上的方法,该函数在调用initMixin 的时候,在Vue的原型上定义的;同时传入options;
  this._init(options);
}

在_init 方法中,做了一堆初始化的操作; 定义uid,合并options,我们这里主要看一个方法initState(vm);

Vue.prototype._init = function (options) {
    debugger
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    var startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = "vue-perf-start:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(("vue " + (vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };

在initState 中我们初始化了一个initData();

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

看到这里,大家是不是有些着急了呢,关键的地方马上就要来了,请大家耐心。在initData中我们主要获取到data 中的对象,data 可以是一个对象,也可以是一个函数;还在里面做了一个代理proxy(vm, _data, key)

function initData (vm: Component) {
  // 拿到¥options 上的data; data 可以是一个函数,但也可以是一个对象;
  let data = vm.$options.data
  // 这里的赋值导致可以通过this._data.message
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    // 判断是不是一个对象,如果不是一个对象,就报一个警告
  
    data = {}
    process.env.NODE_ENV !== 'production' && 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
  // 拿到对象的key;data,props,method 不能重名,因为他们最终都会挂载到vm上,当前这个实例上,那么是怎么实现的,就是通过proxy(vm, `_data`, key);
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      // hasOwn 判断对象里面是不是又这个key
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // proxy 顾名思义就是一层代理,
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 响应式处理;
  observe(data, true /* asRootData */)
}

将数据代理到实例上

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  // 通过一个对象定义了一个get,和一个set;
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  // 通过Object.defineProperty 代理了这个target,key ,对target,key 的访问做了一层get,set;
  // target 就是vm, key 就是_data
  // 当我们访问this.message ,就是访问this._data.message,执行的是get方法;
  // 不要通过这种方式去访问,this._data.message, 下划线在编程界默认是私有属性;
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

当我们执行this.message 的时候,就会调用get 方法,当我们给this.message 赋值的时候就会调用set方法;
到这里,大家是不是就很清楚vue 的双向数据绑定了呢?
职场小白south Joe,望各位大神批评指正,祝大家学习愉快!


南乔
15 声望3 粉丝

我爱敲代码,代码给我快乐