第23期:为什么 Vue2 this 能够直接获取到 data 和 methods ?

195 阅读4分钟

第23期:为什么 Vue2 this 能够直接获取到 data 和 methods ?

一、学习前言

1.1 本文参加了由公众号@若川视野发起的每周源码共读活动,点击了解详情一起参与。

1.2 源码阅读辅助文档:juejin.cn/post/708298…

1.3 在线调试地址:github1s vue/vue/src/shared

二、学习目标

2.1 data中的数据为什么用this可以直接获取到

2.2 methods中的方法为什么用this可以直接获取到

三、调试环境准备(此处我用vscode进行调试)

3.1 先在vscode中下载一个插件,[Debugger for Chrome]

3.2 在代码行中左侧进行点击添加断点,然后按F5,开始调试。F11继续向下

四、源码解读

4.1 先建立一个html文件,然后引入vue.js。为了调试方便,我此处引入下载到本地的vue.js

在线vue.js自取unpkg.com/vue@2.6.14/…

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vue中this直接获取data和method</title>
  </head>
  <body>
    <script src="./vue.js"></script>
  </body>
</html>
<script>
  const vm = new Vue({
    data: {
      name: "小羊笑嘻嘻",
    },
    methods: {
      sayName() {
        console.log(this.name);
      },
    },
  });
  console.log(vm.name);
  vm.sayName();
</script>

4.2 在new Vue行左侧点击添加断点,然后按F5可以进入到vscode的调试页面。如下图:

4.2.png
然后点击F11继续向下

4.3 断点进入vue函数

function Vue (options) {
  if (!(this instanceof Vue) //判断是否是vue的实例,不是的话警告
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

然后再this._init(options)此行前添加断点,右击「运行到行」,按F11进入_init函数

4.4 init方法中我们关注下initState()方法

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

    var startTag, endTag;
    /* istanbul ignore if */
    if (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 */
    {
      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); // data和methods
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if (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行前点击添加断点,右击「运行到行」,按F11进入initState方法

4.5 initState方法中判断各属性若被定义进行各自的初始化

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

在initMethods行前点击添加断点,右击「运行到行」,按F11进入方法内部。如图:

4.5.png

4.6 initMethods中主要是进行各项判断,然后改变原有方法的this指向,全部通过bind绑定到vm上

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)) { // 判断有没有和props冲突
        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);// 将函数的指针变换到vm实例上
  }
}

4.7 initData中主要涉及初始化data之前的系列判断,然后进行数据代理,数据监测。

function initData (vm) {
  var data = vm.$options.data;
  // 将data相关的值保_data,以备后用
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) { // 如果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)) { // 判断data是否与method冲突
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) { // 判断是否与prop冲突
      warn(
        "The data property \"" + key + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(key)) { // 不是内部私有的保留属性,做一层代理,代理到 _data 上。
      proxy(vm, "_data", key);
    }
  }
  // observe data // 最后监测 data,使之成为响应式的数据。
  observe(data, true /* asRootData */);
}

4.8 proxy方法解读

var noop = function(a,b,c) {}
var sharedPropertyDefinition = {
  enumerable: true, // 该属性在for in循环中是否会被枚举。
  configurable: true, // 该属性是否可被删除。
  get: noop,  // 获取属性值时所调用的函数。
  set: noop   // 该属性的更新操作所调用的函数。
};
// 访问this.xxx 实际是访问的 this._data.xxx
function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

4.9 涉及到的一些基础工具类和方法,可以看24期vue工具类中有详解,此处不做赘述

第24期:vue.js工具函数源码解读

五、跟着手写一遍vue中data和methods的简易实现

var noop = function (a, b, c) {};
var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};
class Vue {
  constructor(options) {
    let vm = this;
    vm.$options = options;
    const opts = vm.$options;
    if (opts.data) {
      this.initData(vm);
    }
    if (opts.methods) {
      this.initMethods(vm);
    }
  }
  initData(vm) {
    const data = (vm._data = vm.$options.data);
    const keys = Object.keys(data);
    let i = keys.length;
    while (i--) {
      this.proxy(vm, "_data", keys[i]);
    }
  }
  proxy(target, sourcesKey, key) {
    sharedPropertyDefinition.get = function proxyGetter() {
      return this[sourcesKey][key];
    };
    sharedPropertyDefinition.set = function proxySetter(val) {
      return (this[sourcesKey][key] = val);
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }
  initMethods(vm) {
    const methods = vm.$options.methods;
    for (var key in methods) {
      vm[key] =
        typeof methods[key] !== "function" ? noop : methods[key].bind(vm);
    }
  }
}

六、学习总结

6.1 data中的数据为什么用this可以直接获取到

vue构造函数中,先对data判断保证最后结果获取到的是纯object。其次对其各属性值进行判断,防止和props和methods重复。对不是vue保留关键词的属性通过Object.defineProperty代理到vue的_data。data中的属性最终存储到vue实例vm的_data中。访问this.xxx,实际访问的是this._data.xxx

6.2 methods中的方法为什么用this可以直接获取到

同样先对methods中的方法名进行校验,不重复且是一个function,将methods中方法的this指向通过bind转换为vue的实例vm上。

6.3 异常判断

源码中有针对各属性进行的类型、重复等判断。自己在开发过程中,有时候会忽略一些异常的判断,思路考虑的应该更全面一些。要有容错机制