为什么 Vue2 中 this 能够直接获取到 data 和 methods? 来一起研究下源码!

601 阅读4分钟

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

本文是原文学习后的笔记和总结. 望对其他伙伴研读源码有一定的帮助.

一、调试准备

1. 本地建个文件夹作为工作目录, 新建个index.html, 内容如下:

<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>

这种方式非常适合快速研读源码, 之前我都是用vue的源码工程开始, 要研究一系列编译问题、依赖安装, 还要受flow等语法困扰,还有sourcemap映射问题。这些东西让蛮多人望而却步。非常赞赏作者的研究方式。

2.安装live server vscode插件

我喜欢用这个插件调试本地html页面,这个插件支持代码改动热更新。会提高调试效率。在html页面代码区右键点击“open with live server” 就可以开始代码调试了

3. 开始调试代码

打开chrome调试器的source面板,在 const vm = new Vue({ 这行断点。刷新页面,就可以开始了。作者提供的代码非常简单,避免受到其它代码的干扰。

二、记录调试过程

vue构造函数

断点处F11跟进去来到Vue的构造函数:

function Vue (options) {
    if (!(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}

if (!(this instanceof Vue) 这句判断是不是使用new来调用的构造函数,不是用new调用的话,后续代码无法正常运行。

_init函数

这个函数有点长,为了避免进入代码地狱,快速搞清楚今天的学习内容。我们可以根据函数命名大体猜测功能再加上断点查看this状况快速定位

  • 没运行initState前: image.png

可以看到两个都没值

  • 运行后: image.png

可以看到都有了值

可以看出是 initState(vm);这里面实现的data和methods的代理。

initState

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    // 处理props选项
    if (opts.props) { initProps(vm, opts.props); }
    // 配置了methods就处理初始化这些方法
    if (opts.methods) { initMethods(vm, opts.methods); }
    // 有配置data选项就初始化 data
    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);
    }
}

在这个方法里可以看到 if (opts.methods) { initMethods(vm, opts.methods); } 从命名可以看出是操作methods的。那先到 initMethods 看看

initMethods

贴上代码好分析

function initMethods (vm, methods) {
    // 取到props的定义,后面判断冲突要用
    var props = vm.$options.props;
    // 遍历methods数组
    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
          );
        }
        
        // 和props里的key重名了,提醒用户不能冲突重名
        if (props && hasOwn(props, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a prop."),
            vm
          );
        }
        
        // 判断当前的方法名在vue实例上是否存在,若存在且是vue的保留命名(以_ 或 $ 开头),提醒用户使用错误。
        if ((key in vm) && isReserved(key)) {
          warn(
            "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
            "Avoid defining component methods that start with _ or $."
          );
        }
      }
      
      // 不是函数就给个默认无操作函数
      // 是函数要绑作用域到vue实例
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}

这里可以看出把绑定作用域后的函数赋值给了vue实例。 这个函数还是很简单的。但这bind写了个兼容处理,一起看看:

// 用apply和call模拟bind
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;

initData

代码有点长, 用注释帮助理解

function initData (vm) {
    var data = vm.$options.data;
    // 看选项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];
      // 这里有个大括号什么目的?
      {
        // 判断数据key是否和方法名冲突, 冲突给出提醒
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      
      // 数据key和props的key冲突给出提醒
      if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
        // 不是vue保留开头的就代理这个数据属性, 后面分析proxy
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key);
      }
    }
    
    // 观察数据,使其有响应能力
    // observe data
    observe(data, true /* asRootData */);
}

proxy

先看源码:

/**
   * 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
};

// 使用 Object.defineProperty 代理数据属性到vue实例
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);
}

这里把key通过sharedPropertyDefinition公共对象代理vm上. 在使用this.xxx时,实际上是访问的this._data.xxx

属性的描述符property有以下选项:

value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性是否出现在对象的枚举属性中, 即:该属性在for in循环中是否会被枚举。
configurable——当且仅当该属性为 true 时,该属性的描述符才能够被改变,同时该属性才能从对应的对象上被删除。默认为falseset()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。执行时不传入任何参数,但是会传入 this 对象

总结

分析代码时, 还是有其它相关代码想深入分析的, 但考虑避免进入代码地狱, 深陷其中, 暂且分析这些. 日后再做更深入分析.

回忆下this.xxx能访问到data里的xxx的原因: data的属性会被放到this._data上. 通过定义描述符的getter和setter来代理到this上

this.xxx可以访问methods里的方法的原因是通过bind方法把函数的作用域绑定到vue实例this上,然后赋值this.