【若川视野 x 源码共读】 第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods

387 阅读3分钟

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

1. 学习准备

2. 学习目标

  1. 如何学习调试 vue2 源码
  2. methods 中的方法为什么可以用 this 直接获取到
  3. data 中的数据为什么可以用 this 直接获取到
  4. 学习源码中优秀代码和思想,投入到自己的项目中

3. 学习过程

为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘! 这篇文章写得很清楚了,一步一步跟着操作和调试就可以了,所以这篇笔记不会做详细地记录。

const vm = new Vue({
    data: {
        name: '我是若川',
    },
    methods: {
        sayName() {
            console.log(this.name);
        }
    },
});
console.log(vm.name); // 我是若川
console.log(vm.sayName()); // 我是若川

问题1. methods 中的方法为什么可以用 this 直接获取到?

解答:使用bind绑定函数的this指向为vm,也就是new Vue的实例对象来实现的。具体代码如下:

/**
 * 判断 methods 中的每一项是不是函数,如果不是警告。
 * 判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
 * 判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指
 * 内部变量标识)开头,如果是警告。
 * /
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);
    }
}

我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。

我们可以把鼠标移上 bind 变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。

image.png bind 的实现代码如下:

// bind 返回一个函数,修改 this 指向
function polyfillBind(fn, ctx) {
    function boundFn(a) {
        var l = arguments.length;
        return l
            ? l > 1
                ? fn.apply(ctx, arguments)
                : fn.call(ctx, a)
            : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
}

function nativeBind(fn, ctx) {
    return fn.bind(ctx)
}

var bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;

简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用callapply实现,据说是因为性能问题。

问题2. data 中的数据为什么可以用 this 直接获取到?

解答:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx

先看一下initData方法对数据进行初始化,

data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};

如果typeof datafunction,则执行getData(data, vm),否则,vm._data = data || {}

initData方法做了如下事情:

  1. 先给 _data 赋值,以备后用。
  2. 最终获取到的 data 不是对象给出警告。
  3. 遍历 data ,其中每一项: 如果和 methods 冲突了,报警告。
  4. 如果和 props 冲突了,报警告。
  5. 不是内部私有的保留属性,做一层代理,代理到 _data 上。
  6. 最后监测 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 */);
}

是函数时调用函数,执行获取到对象。

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();
    }
}

之后就执行proxy(vm, "_data", key);

// proxy 代理,其实就是用 `Object.defineProperty` 定义对象
/**
   * Perform no operation.
   * Stubbing args to make Flow happy without leaving useless transpiled code
   * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
   */
function noop (a, b, c) {}
var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};

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);
}

proxy代理方法的用处是:this.xxx 是访问的 this._data.xxx,所以this.name 是读取的 this._data.name

摘抄两个工具方法

1. hasOwn 是否是对象本身拥有的属性

/**
* Check whether an object has the property.
* 是自己的本身拥有的属性,不是通过原型链向上查找的。
*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false

2. isReserved 是否是内部私有保留的字符串$ 和 _ 开头

/**
   * Check if a string starts with $ or _
   */
function isReserved (str) {
  var c = (str + '').charCodeAt(0);
  return c === 0x24 || c === 0x5F
}
isReserved('_data'); // true
isReserved('$options'); // true
isReserved('data'); // false
isReserved('options'); // false

4. 学习收获或感受

跟着原文的例子调试了代码,大概知道了一些,还有很多方法的写法没去深究,比如getData方法中为什么要使用 pushTarget()和popTarget()等等。