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

138 阅读4分钟

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

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

1. 前言

三大框架的源码对于大家应该是有些难度的,但是愚公尚可移山,铁杵也能磨成针,不就是慢慢摸鱼么。盘它

2. 源码解析

Vue中示例

const vue = new Vue({
    data: {
        name: 'curtis',
    },
    methods: {
        getName(){
            console.log(this.name);
        }
    },
});
console.log(vue.name); // curtis
console.log(vue.getName()); // curtis
复制代码

vue对象中可以直接获取内部data对象中的属性,而且method对象的方法可以直接通过this获取data对象中的属性值,但是对于JavaScript中的对象,这样的访问方式是不合理的。

const obj = new Object({
    data: {
        name: 'curtis',
    },
    method: {
        getName(){
            console.log(this.name);
        }
    },
});
console.log(obj.name); // undefined
console.log(obj.data.name); // curtis
console.log(obj.getName()); // Uncaught TypeError: obj.getName is not a function
console.log(obj.method.getName()); // undefined undefined
复制代码

环境准备

  1. 本地创建测试文件夹examples,新建文件index.html,加入以下代码

    <!DOCTYPE html>
    <html>
      <header>
        <title>vue</title>
      </header>
      <body>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
        <script>
          const vue = new Vue({
            data: {
              name: "curtis",
            },
            methods: {
              getName() {
                console.log(this.name);
              },
            },
          });
          console.log(vue.name); // curtis
          console.log(vue.getName()); // curtis
        </script>
      </body>
    </html>
    复制代码
    
  2. 全局安装http-server服务

    npm i -g http-server
    复制代码
    
  3. 启动服务

    cd examples
    http-server .
    复制代码
    
  4. 启动完成后通过http://localhost:8080/可以访问到index.html文件,之后可通过控制台上调试学习源码。

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关键词调用构造函数。

_init

初始化函数

var uid$3 = 0;
function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      // 为每个实例生成一个唯一uid
      vm._uid = uid$3++;
​
      var startTag, endTag; 
      // istanbul忽略覆盖率
      /* 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
      // 判断是否传入初始对象以传入的初始对象中是否包含_isComponent属性,即是否为组件,在本次调用中不会走到这儿 
      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); // 初始化render
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      initState(vm);
      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);
      }
    };
}
复制代码
  • resolveConstructorOptions()解析构造函数上的options属性,主要是为$options添加一些属性,这里不展开说,详细可参看人人都能懂的Vue源码系列(三)—resolveConstructorOptions函数-上
  • initLifecycle(vm)初始化生命周期,大致看了下,内部主要是对vm对象的一些生命周期相关私有属性赋值,应该不会改变对象的属性的访问关系
  • initEvents(vm)初始化事件监听,添加一些事件相关私有属性赋值
  • initRender(vm)初始化render,仍然是添加属性
  • callHook具体做了什么事情暂时还不太清楚,但也不像会改变对象的属性访问关系
  • initInjections(vm)初始化注入器,具体作用还未知
  • initState(vm)看到函数找那个使用了datamethods,猜测应该和这个函数有关,看了参考文章,果然是它,继续分析

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);
    }
}
复制代码
  • var opts = vm.$optionsopts.method依旧是getName()方法,即创建对象时传入的method;但是data的值在vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)时变为了mergedInstanceDataFn方法,暂时的知识盲区……
  • 之后就是最为重要的两个函数,也是全文的重点

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);
  }
}
复制代码
  • for...in遍历method对象中的所有方法,在本次调用中只有getName()一个方法,if (typeof methods[key] !== "function")判断是否为方法,不是则报错
  • if (props && hasOwn(props, key))判断props中是否包含该属性,有则警告;hasOwn()定义在vueutils中,用于判断对象中是否包含某个属性
  • if (key in vm && isReserved(key))判断keyvm实例的保留属性,是则警告
  • 以上判断条件都通过的话,将methods属性中的方法赋值给vm,但是在赋值时首相判断属性是否为方法,不是则赋值为noop(function noop (a, b, c) {}全局定义的空函数)函数(这里不太明白之前已经判断过是否为函数了,为什么还要判断一次),函数的话则进行赋值,但在赋值时使用了bind()将函数的this绑定到了vm,因而函数中的this.data就相当于vm.data

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 */);
}
复制代码
  1. 判断data属性是否为函数,是,调用getData方法获取数据

  2. getData()

    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.call(vm, vm)是如何获取到传入的data的值的,所以重新看了下之前初始化时mergeOptions()的流程,虽然还是没太明白用意,但大致是清楚了data的函数是从哪里传来的,在源码1237行,最终data变成了函数mergedInstanceDataFn()

    • mergedInstanceDataFn()

      return function mergedInstanceDataFn () {
              // instance merge
              var instanceData = typeof childVal === 'function'
                ? childVal.call(vm, vm)
                : childVal;
              var defaultData = typeof parentVal === 'function'
                ? parentVal.call(vm, vm)
                : parentVal;
              if (instanceData) {
                return mergeData(instanceData, defaultData)
              } else {
                return defaultData
              }
       }
      复制代码
      
      • 其中childVal即为实例化Vue时传入的data
    • 因而,在getData()函数调用data.call时直接就可获取到childVal的值,既是传入的data值。

  3. 如果data不是纯粹的对象,返回警告;isPlainObject()在之前工具函数中看到过判断其类型为Object

  4. 获取data的所有属性,遍历,判断属性是否存在methodvm实例自身属性中,是则警告;

  5. 使用isReserved()判断是否无保留属性

    /**
     * Check if a string starts with $ or _
     */
    function isReserved (str) {
      var c = (str + '').charCodeAt(0);
      return c === 0x24 || c === 0x5F
    }
    复制代码
    
  6. 使用proxy(vm, "_data", key)进行访问代理

    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);
    }
    复制代码
    

    首先看函数的最后一行Object.defineProperty(target, key, sharedPropertyDefinition),为vm实例定义一个属性,属性名为key(本次调用值为'name')然后定义一系列属性描述符,其中最为重要的就是getset;然后可以看到将get定义为return this[sourceKey][key],将set定义为this[sourceKey][key] = val;其中传入的sourceKey值为_data;因此,在访问vm.name时会被代理到vm._data.name,设置vm.name是同样会被代理访问到vm._data.name,而vm._data.name中保存的即为创建实例是传入的data

    关于Object.defineProperty()的更多说明可参看MDN

自己尝试简单复刻

// 属性描述符定义
var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: ()=>{},
  set: ()=>{},
};
// 访问代理的方法,这里我是直接用的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);
}
// 声明构造函数
function MyVue(options) {
  this._init(options);
}
MyVue.prototype._init = function (options) {
  var vm = this;
  vm["_data"] = options.data;
  initMethods(vm, options.methods);
  initData(vm, options.data);
};
// 初始化Methods的方法,简化版
function initMethods(vm, methods) {
  for (const key in methods) {
    vm[key] = methods[key].bind(vm);
  }
}
// 初始化Data的方法,简化版
function initData(vm, data) {
  var keys = Object.keys(data);
  var i = keys.length;
  while (i--) {
    let key = keys[i];
    proxy(vm, "_data", key);
  }
}
// 创建实例测试测试
let mv = new MyVue({
  data: {
    name: "xinxinzi",
    job: "coder",
  },
  methods: {
    getName() {
      console.log(this.name);
    },
    getJob() {
      console.log(this.job);
    },
  },
});
// 成功输出
console.log(mv.name); // xinxinzi
console.log(mv.job); // coder
console.log(mv.getName()); // xinxinzi
console.log(mv.getJob()); // coder
复制代码

3. 总结

通过这期源码阅读活动又学习巩固了很多知识,比如bind绑定函数作用域,defineProperty定义对象属性等等;也有自己写一个mvvm框架的自信。