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

1,124 阅读4分钟

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

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

示例

class Person {
  constructor(options) {
    let { data, methods } = options;
    this.data = data;
    this.methods = methods;
  }
}
const p = new Person({
  data: {
    name: "女朋友",
  },
  methods: {
    sayName() {
      console.log("[ sayName ] >", "女朋友");
    },
  },
});
​
// 正确获取
console.log("[ this.data ] >", p.data.name);
console.log("[ this.methods ] >", p.methods.sayName);
​
// 那需要怎么做才能实现下面这种获取方式呢?
// console.log("[ this.data ] >", p.name);
// console.log("[ this.methods ] >", p.sayName());

搭建环境

新建 html 文件, 在<body></body>中加上如下js

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

启动本地服务

  • http-server

    • npm install http-server -g 全局安装
    • http-server 到对应想启动服务文件夹启动
  • Five Server

    • vscode 搜索该插件
    • 对应文件,右键 Open With Five Server

调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。

image-20220623154440036

打开页面之后,刷新页面 按 F11 即进入函数,这时断点就走进了 Vue 的构造函数

Vue 构造函数

function Vue (options) {
    // !(this instanceof Vue) 判断是不是用了 new 关键词调用构造函数
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
// ...// 初始化
  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')
​
// ...

Vue初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Vue的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然.

调试:继续在this._init(options);处打上断点,按F11进入函数。

_init 初始化函数

进入 _init 函数后,这个函数比较长,做了挺多事情,我们猜测跟datamethods相关的实现在initState(vm)函数里。

调试:接着我们在initState(vm)函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState函数。

initState 初始化状态

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    // 初始化 属性
    if (opts.props) { initProps(vm, opts.props); }
    // 有传入 methods,初始化方法
    if (opts.methods) { initMethods(vm, opts.methods); }
    // 有传入 data,初始化 data 检测数据
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    // 初始化 计算属性
    if (opts.computed) { initComputed(vm, opts.computed); }
    // 初始化 watch
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
}
​

调试:在 initMethods 这句打上断点,同时在initData(vm)处打上断点,看完initMethods函数后,可以直接按F8回到initData(vm)函数。 继续按F11,先进入initMethods函数。

initMethods 初始化方法

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 是否和 methods 重名
  • methods中的每一项是不是已经在 new Vue实例 vm上存在,而且是方法名是保留的 _ $(在JS中一般指内部变量标识)开头

通过this直接访问到methods里面的函数

关键就在于这行代码vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);

浏览器调式 : alt 放在函数名上,可出现提示,跳转到相对应位置

vscode调式: ctrl 跳转到相应位置

这里查看 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实现,据说是因为性能问题。

  • apply 传递数组,需要遍历参数,所以单个参数的情况下 call 性能好些 (大概吧,我也不确定 哈哈哈)

调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。

initData 初始化 data

function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    // 最后获取到的 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];
      {
          // methods 与 data 重复 警告
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method "" + key + "" has already been defined as a data property."),
            vm
          );
        }
      }
          // props 与 data 重复 警告
      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 */);
}
​

proxy 代理

其实就是用 Object.defineProperty 定义对象

这里用处是:this.xxx 则是访问的 this._data.xxx

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

总结

通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。

通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx