【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的实例
相关推荐
前端小趴菜0516 分钟前
React - createPortal
前端·vue.js·react.js
三原3 小时前
7000块帮朋友做了2个小程序加一个后台管理系统,值不值?
前端·vue.js·微信小程序
白仑色3 小时前
完整 Spring Boot + Vue 登录系统
vue.js·spring boot·后端
阳火锅4 小时前
Vue 开发者的外挂工具:配置一个 JSON,自动造出一整套页面!
javascript·vue.js·面试
G_whang5 小时前
jenkins部署前端vue项目使用Docker+Jenkinsfile方式
前端·vue.js·jenkins
寻觅~流光5 小时前
封装---统一封装处理页面标题
开发语言·前端·javascript·vue.js·typescript·前端框架·vue
江上暮云6 小时前
手摸手带你彻底搞懂Vue的响应式原理
vue.js
恰薯条的屑海鸥6 小时前
前端进阶之路-从传统前端到VUE-JS(第五期-路由应用)
前端·javascript·vue.js·学习·前端框架
wangpq6 小时前
Echart饼图自动轮播效果封装
javascript·vue.js