源码学习——vue2中的this为什么可以直接获取到data和methods

176 阅读7分钟

在写vue的时候,我们可以通过this直接拿到data和methods里面的内容,这在vue中是如何实现的呢?本篇文章通过源码解析来分析其实现方案。先说结论:

  • this直接获取data内属性的原因:首先会将data存放到Vue的实例vm上的_data中,并利用Object.defineProperty方法,将我们访问this.xxx时,代理到this._data.xxx

  • this直接获取methods内方法的原因:methods里的方法,因为使用了bind将函数的this指向了实例vm。所以我们可以直接用this访问到。

使用示例如下:

const vm = new Vue({
            data: {
                name: 'L_Look',
            }, 
            methods: {
                say() {
                    console.log('======', this.name);
                }
            }
        })
        console.log(vm.name); // L_Look
        console.log(vm.say()); // ====== L_Look

接下来,我们从源码来进行分析。

调试环境搭建

首先我们新建一个HTML文件,引入一个线上版的vue。

<body>
    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <script>
        const vm = new Vue({
            data: {
                name: 'L_Look',
            }, 
            methods: {
                say() {
                    console.log('======', this.name);
                }
            }
        })
        console.log(vm.name); // L_Look
        console.log(vm.say()); // ====== L_Look
    </script>
</body>

在vsCode中可以下载Live Server插件,用该插件起一个node服务,来调试我们的代码。

调试过程

在浏览器中打开devTools调试,进入sourcec面板,在const vm = new Vue({行打个断点。

image.png

进入vue构造函数

image.png 可以按下F11或点击上面调试操作面板红框的按钮,进入函数内部。

  function Vue (options) {
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }
  // 引入vue之后,做的一些初始化操作。
  initMixin(Vue);
  stateMixin(Vue);
  eventsMixin(Vue);
  lifecycleMixin(Vue);
  renderMixin(Vue);

这里可以看下在Vue构造函数中的第一个判断if (!(this instanceof Vue),是用来判断是否是用new关键字调用的构造函数。为什么要加一个这个判断呢?

我们接着看后面的this._init()这个方法,用的是this来调用的,在点进_init函数我们会看到这个方法是挂载在原型上的,这也就证明Vue是作为一个构造函数,必须用new来调用,这时this是指向构造函数的实例。如果用普通函数的方法调用,那这个this指向的就有不确定性了,根据其执行时的环境来判定。

this指向这里不做过多说明,可以查看其他相关文章

进入_init函数

这里我们只聚焦到,vue是如何处理data和methods的,其他方法暂时先放下。

function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // 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 */
      {
        initProxy(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');
    };
  }

这里我们可以依次点击一下_init这个函数里调用的方法,来大致浏览一下都是干什么用的。或者可以参考别人的源码分析,效率更高一些。这里我们发现是在initStat()这个方法里对data和methods做了处理。接下来我们看下这个方法。

initState 初始化状态

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);
    }
  }
  • _init函数中已经把options参数挂载到vm实例上。
  • 这个函数,主要就是处理options里面的数据,针对props、methods、data、computed、watch做处理。

本篇文章主要分析下methods方法和data方法。所以下面我们来看些initMethods方法和initData方法。

initMethods方法,初始化methods

function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        if (typeof methods[key] !== 'function') {
          warn(
            "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        if (props && hasOwn(props, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a prop."),
            vm
          );
        }
        if ((key in vm) && isReserved(key)) {
          warn(
            "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
            "Avoid defining component methods that start with _ or $."
          );
        }
      }
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
  }
  • 首先遍历methods里的方法,并判断是否符合预期

    • 是否是函数,不是函数进行警告
    • 是否与props冲突了,如果冲突进行警告
    • 判断是否是Vue内置保留的方法名或已经存在在实例中。
  • 除了上面的判断,就是将methods的方法,用bind绑定函数的this指向vue的实例vm。

这样也就解释了为什么this可以直接调用methods里的方法,因为使用了bind将函数的this指向了实例vm。

这里的bind函数是vue自己封装的一个,主要是为了兼容不兼容bind函数的运行环境。至于bind函数的作用,简单说就是返回一个新的函数,将bind()被调用时传入的第一个参数作为新函数的this对象,而其余参数作为新函数的参数,供调用时使用。 具体细节可以参考其他bind函数解析的文章。

initData 初始化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 */);
  }

这个函数主要做了以下几个操作。

  • 先判断data是不是一个函数,如果是函数调用getData获取返回的data数据源,然后将data赋值给_data。
  • 判断取到的data是不是一个对象,不是对象的话,给警告。
  • 对data进行遍历,对每一项进行判断,如果和methods冲突了,给警告,如果和props冲突了,给警告。并且最后判断是不是一个vue内部保留的方法或内部方法,不是的话使用proxy做一层代理,代理到_data上。
  • 最后将data转换为响应式数据。

getData() 获取data对象

function getData (data, vm) {
    // #7573 disable dep collection when invoking data getters
    pushTarget();
    try {
      return data.call(vm, vm)
    } catch (e) {
      handleError(e, vm, "data()");
      return {}
    } finally {
      popTarget();
    }
  }

这里需要注意,调用data函数时,要将this指向指向vue的实例,保证函数内this指向正确。

proxy 代理数据访问

这个方法就是vue2中的一个核心点,如何使用Object.defineProperty()来定义一个对象。 这里实现了访问this.xxx 就是访问this._data.xxx。

// proxy(vm, "_data", key);
function noop (a, b, c) {};
// 描述对象,描述对象的特性
var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
  };

function proxy (target, sourceKey, key) {
    // get描述符,当获取目标属性的值是,实质是获取_data对象上的属性值
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    // set描述符,同样设置值时,设置的也是_data对象的值。
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

学习下Object.defineProperty

会直接在第一个对象上定义一个新属性,或者修改一个对象的现有属性。

语法:Object.defineProperty(obj, prop, descriptor)

参数说明:

  • obj:目标对象
  • prop: 需要定义或修改的属性名
  • descriptor: 目标属性的属性描述符

属性描述符: 对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由getter函数和setter函数所描述的属性。

数据描述符:

  • writable: 是否可以被重写,默认为false
  • enumerable: 是否可以被枚举。默认false
  • value: 值可以使任意类型的值,默认为undefined。
  • configurable: 是否可以删除目标属性或是否可以再次修改属性的特性。默认false。

存取描述符:

当使用了getter或setter方法,则不被允许使用writable和value这两个属性。

举个例子:

let school = {};
        let name = '小学';
        Object.defineProperty(school, 'name', {
            configurable: true,
            enumerable: true,
            get() {
                return name
            },
            set(val) {
                name = val
            }
        })
        console.log(school.name ); // 小学
        school.name = '中学';
        console.log(name); // 中学

再看下源码的实现:

利用Object.defineProperty(),当我们使用this.xxx访问时,实际上是代理到this._data上面的。

这段源码中,出现了一些工具函数,比如hasOwnnoopisReservedbind等,可以参考我的另一篇文章 源码学习—VUE2中的那些工具函数

收获

  • 源码的调试。调试vue2源码,踏出了第一步,看过很多的源码分析,但却是第一次主动去调试源码。
  • 源码中代码功能的划分,可以看到在vue中,针对各个流程进行的函数划分,比如开始的initMixinstateMixineventsMixinlifecycleMixinrenderMixin的这几个函数。还有_init方法中,initLifecycleinitEventsinitRenderinitInjectionsinitStatainitProvide。这些在我们开发中,非常有借鉴意义。在代码的组织形式,结构设计时,针对性的模块划分,使代码的阅读性与扩展性都非常强。不要从头到尾一把梭。
  • Object.defineProperty的复习与实践深化。其实这个方法在日常开发中很少用到,但通过对该方法深入学习,可以在日常开发中拓展自己的解决方案,满足特定场景,来简化操作逻辑。、
  • 基础知识的复习。构造函数、this指向、call,apply,bind等方法的复习。

思考

  • 以一个问题为切入点,去阅读源码,会更加有效率且容易聚焦,相当于有个目标,否则容易陷入到源码复杂的实现逻辑中,找不到重点。
  • 看源码,看这些大牛的代码设计,处理某些问题的解决方案,然后真正的在日常开发中应用实践,能极大的提高我们的编程能力。写代码还是要带些洁癖的。

参考: