【Vue源码学习】Vue新手友好!为什么vue2 this能够直接获取到data和methods中的属性?

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

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

1. 主题:

为什么vue2 this能够直接获取到data和methods?

源码解读原文:为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!

2. 源码解读补充:

如有不足或错误之处,欢迎各位大佬指正!

2.1 Vue 构造函数

js 复制代码
function Vue (options) {
    if (!(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}
// 初始化
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

this._init(options); 这一行指的是,调用实例的 _init 方法,实例上没有定义,沿着原型链,找到实例的构造函数 Vue 上的 _init 方法。

2.2 _init 初始化函数

js 复制代码
// 代码有删减
function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;

      // ......

      //  初始化状态
      initState(vm);
      // ......
    };
}

原型链和原型: 每一个对象,都有一个 __proto__ 属性,它指向该对象的构造函数的 prototype 对象,这个 prototype 就是该对象的原型对象。而这个层层嵌套的链式关系,就是原型链。调用该对象的方法或属性时,在自身属性上找不到就会沿着原型链查找。

initMixin(Vue) 这个方法里,给构造函数 Vueprototype 属性添加了 _init 方法。

2.3 initState 初始化状态

js 复制代码
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); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
}

初始化顺序:props → methods → data → computed → watch。

2.4 initMethods 初始化方法

js 复制代码
function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        // 这里是对methods中方法的一些判断,判断是否是函数、命名是否与props中冲突、命名是否是保留字段
      }
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}

这里对methods中定义的合法的方法遍历,并给 vm 实例添加同名方法,赋值为绑定 this 指向到 vm 实例本身的原方法 。如比便可以直接通过 this.xxx() 调用methods中的方法了。

关键在于 bind(methods[key], vm)

2.5 initData 初始化 data

js 复制代码
function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) { // 这里校验data是否返回一个对象
      // ......
    }
    // 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];
      {
        // 这里校验data和methods中的同名变量
      }
      if (props && hasOwn(props, key)) { // 这里校验data和props中的同名变量
        // ......
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key); // 合法的、不是内部私有的保留属性,做一层代理,代理到 _data 上。
      }
    }
    // observe data
    observe(data, true /* asRootData */); // 这里监听data,data中的数据转换为响应式数据(稍微看了下感觉有点复杂,没深入看)
}

关键在 proxy(vm, "_data", key) 这行。

proxy 方法利用 Object.defineProperty 定义对象,实现访问属性的转发this.xxx 实际上是访问的 this._data.xxx

js 复制代码
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 中我对 sharedPropertyDefinition 只全局(相对全局)定义了一次,而不是在 proxy 中每次调用每次生成有疑惑,思考加询问豆包之后得出以下结论:

  1. 利用了闭包的特点:

    1. 当调用 proxy 方法时,key 作为参数传入。在 sharedPropertyDefinitiongetset 函数内部,key 被引用。由于 JavaScript 的闭包特性,getset 函数会捕获并记住当前的 key 值。

      proxy(vm, "_data", "name") 实际上会将 sharedPropertyDefinitiongetset 函数修改为:

      js 复制代码
      function proxyGetter () {
        return this["_data"]["name"];
      }
      function proxySetter (val) {
        this["_data"]["name"] = val;
      }
  2. Object.defineProperty 会将这些修改后的 getset 函数应用到 target 对象的 key 属性上。target 对象的 key 属性会被赋予 sharedPropertyDefinition 中的 getset 函数的副本。一旦属性被定义,这些 getset 函数就与 sharedPropertyDefinitiongetset 解绑了。

  3. 这么做的目的,应该是出于性能考虑:

    在 JavaScript 里,对象的创建和初始化操作会带来一定的开销,这包含内存分配和属性初始化。要是在 proxy 方法每次调用时都创建一个新的 sharedPropertyDefinition 对象,就会有多次对象创建和属性初始化操作,这会对性能产生影响。

    而全局定义一次 sharedPropertyDefinition 对象,每次调用 proxy 方法时仅仅修改这个对象的 getset 属性,这样就避免了多次对象创建和属性初始化操作,降低了性能开销。

3. 总结:

手写实现 关键:

  1. methods:
    1. 对methods中定义的方法,是否与props中有重名的校验;
    2. 通过校验的方法,使用自定义bind方法将methods中的方法逐一对vm实例赋值同名函数并绑定this指向为实例;
  2. data:
    1. 对data类型的校验;
    2. 对data中定义的变量,是否与methods、props中有重名变量的校验;
    3. 通过校验的变量,使用自定义的proxy方法将data中的变量逐一对vm实例赋值同名变量并实现访问和修改的代理方法;
      1. proxy方法,本质上是通过Object.defineProperty方法实现,也即响应式原理实现关键。

4. 手写实现

开始默写了!(不考虑检验,只实现核心功能)

js 复制代码
// 构造函数 MyVue
class MyVue {
  constructor(options) {
    this._init(options)
  }
  _init(options) {
    // methods 实现
    const methods = options.methods || {}
    if (Object.prototype.toString.call(methods) === '[object Object]') {
      const methodsKeys = Object.keys(methods)
      if (methodsKeys.length > 0) {
        for(const key of methodsKeys) {
          this[key] = methods[key].bind(this)
        }
      }
    }
    // data 实现
    const data = options.data() || {}
    if (Object.prototype.toString.call(data) === '[object Object]') {
      const dataKeys = Object.keys(data)
      if (dataKeys.length > 0) {
        for(const key of dataKeys) {
          Object.defineProperty(this, key, {
            enumerable: true,
            configurable: true,
            get() {
              return data[key]
            },
            set(val) {
              data[key] = val
              return val
            }
          })
        }
      }
    }
  }
}
// 实例调用
const myVm = new MyVue({
  data() {
    return {
      msg: '这是MyVue的实例'
    }
  },
  methods: {
    getMsg() {
      return 'getMsg: ' + this.msg
    }
  }
})
console.log(myVm.msg) // 这是MyVue的实例
console.log(myVm.getMsg()) // getMsg: 这是MyVue的实例
相关推荐
知识分享小能手5 小时前
Vue3 学习教程,从入门到精通,Vue 3 + Tailwind CSS 全面知识点与案例详解(31)
前端·javascript·css·vue.js·学习·typescript·vue3
谷哥的小弟7 小时前
Spring Framework源码解析——BeanPostProcessor
spring·源码
柑橘乌云_7 小时前
vue中如何在父组件监听子组件的生命周期
前端·javascript·vue.js
小白的代码日记9 小时前
Springboot-vue 地图展现
前端·javascript·vue.js
kymjs张涛12 小时前
零一开源|前沿技术周刊 #11
前端·javascript·vue.js
anyup12 小时前
🚀 2025 最推荐的 uni-app 技术栈:unibest + uView Pro 高效开发全攻略
前端·vue.js·uni-app
掘金0112 小时前
🚀 Vue 中使用 `@vueuse/core` 终极指南:从入门到精通
vue.js
掘金0112 小时前
🔥 Vue 开发者的“外挂”库: 让你秒变超级赛亚人!🔥
javascript·vue.js·前端框架
北辰浮光13 小时前
[Element-plus]动态设置组件的语言
javascript·vue.js·elementui
李大玄13 小时前
一套通用的 JS 复制功能(保留/去掉换行,兼容 PC/移动端/微信)
前端·javascript·vue.js