【 源码共读】 | 为什么 Vue2 this 能够直接获取到 data 和 methods

787 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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

【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods 点击了解本期详情一起参与

本文涉及

this指向

代码调试

bind函数使用

示例代码

<!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>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10"></script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          message: 'Hello JueJin'
        },
        methods: {
          getMessage() {
            console.log(this.message)
          }
        }
      })
      console.log(vm)
      vm.getMessage() // Hello JueJin
    </script>
  </body>
</html>

我们知道,运行时控制台会执行vm.getMessage(),输出this.message的数据。

我们尝试自己来实现一个对象

const obj = new Object({
    data: {
        message: 'Hello JueJin'
    },
    methods: {
        getMessage() {
            console.log(this.message)
        }
    }
})
console.log(obj.getMessage()) // TypeError: obj.getMessage is not a function
console.log(obj.methods.getMessage()) // undefined

一般而言我们是不能直接访问到obj里面的属性,而vue的对象可以这样使用,由此可得,this指向问题。

  • 我们来调试下代码

image-20220915090209499

  • 在浏览器Sources中打上断点,进入vue的构造函数中

image-20220915090501873

  • 继续进入_init函数
function initMixin$1(Vue) {
    Vue.prototype._init = function (options) {
        var vm = this;
        /* ... 中间省略 ... */
        // expose real self
        vm._self = vm;
        initLifecycle(vm);
        initEvents(vm); 
        initRender(vm);
        callHook$1(vm, 'beforeCreate', undefined, false /* setContext */);
        initInjections(vm); // resolve injections before data/props


        // 初始化状态
        initState(vm); 
        initProvide(vm); // resolve provide after data/props
        callHook$1(vm, 'created');
        /* istanbul ignore if */
        if (config.performance && mark) {
            vm._name = formatComponentName(vm, false);
            mark(endTag);
            measure("vue ".concat(vm._name, " init"), startTag, endTag);
        }
        if (vm.$options.el) {
            vm.$mount(vm.$options.el);
        }
    };
}
  • initState中打上断点,F8调到该断点,进入函数

image-20220915102656691

  • 根据函数名,我们可以得知这两个函数应该就是初始化方法和数据的,加上断点,进去查看一下
function initMethods(vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
        {
            // 判断是否是函数
            if (typeof methods[key] !== 'function') {
                warn$2("Method \"".concat(key, "\" has type \"").concat(typeof methods[key], "\" in the component definition. ") +
                       "Did you reference the function correctly?", vm);
            }
            // 判断方法名字和props是否冲突
            if (props && hasOwn(props, key)) {
                warn$2("Method \"".concat(key, "\" has already been defined as a prop."), vm);
            }
            // 判断方法是否已经存在vue实例中,并且方法名是否有$ _
            if (key in vm && isReserved(key)) {
                warn$2("Method \"".concat(key, "\" conflicts with an existing Vue instance method. ") +
                       "Avoid defining component methods that start with _ or $.");
            }
        }
        // 函数绑定  bind
        vm[key] = typeof methods[key] !== 'function' ? noop : bind$1(methods[key], vm);
    }
}

通过以上判断,将methods属性中的方法赋值给vm,并且通过bind将this指针指向vm,所以可以this.methods===vm.methods

  • 断点查看initData
  function initData(vm) {
      var data = vm.$options.data;
      data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
      if (!isPlainObject(data)) {
          data = {};
          warn$2('data functions should return an object:\n' +
                  'https://v2.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$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);
              }
          }
          if (props && hasOwn(props, key)) {
              warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +
                      "Use prop default value instead.", vm);
          }
          else if (!isReserved(key)) {
              proxy(vm, "_data", key);
          }
      }
      // observe data
      var ob = observe(data);
      ob && ob.vmCount++;
  }

通过校验后,会用proxy做了数据代理

  • 查看proxy定义
  function proxy(target, sourceKey, key) {
      sharedPropertyDefinition.get = function proxyGetter() {
          return this[sourceKey][key];
      };
      sharedPropertyDefinition.set = function proxySetter(val) {
          this[sourceKey][key] = val;
      };
      // 定义vm对象的key值
      Object.defineProperty(target, key, sharedPropertyDefinition);
  }

定义了get,set方法,我们可以知道,当我们获取数据,通过this.xxx时,实际上访问的是new Vue 实例中的_data对象,实际上就是this._data.xxx

构造自己的简易版实例

const 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 initMethods(vm, methods) {
  for (const key in methods) {
    // 省略类型检查
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm)
  }
}

function initData(vm) {
  const data = (vm._data = vm.$options.data)
  const keys = Object.keys(data)
  var i = keys.length
  while (i--) {
    // ... 省略类型检查
    var key = keys[i]
    proxy(vm, '_data', key)
  }
}
function MyVue(options) {
  let vm = this
  vm.$options = options
  const opts = vm.$options
  if (opts.methods) {
    initMethods(vm, opts.methods)
  }
  if (opts.data) {
    initData(vm)
  }
}

const vm = new MyVue({
  data: {
    message: 'Hello JueJin'
  },
  methods: {
    getMessage() {
      console.log(this.message)
    }
  }
})

vm.getMessage() // Hello JueJin

总结

这个章节,我们尝试去看vue2的源码

  • 通过bind的函数去绑定方法
  • 通过自定义proxy的方式去将this指向vue实例中的_data,从而实现this.xxx访问属性。
  • 然后,我们用了一小段代码来实现了这个简易版的vue功能