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

104 阅读3分钟

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

环境准备

  1. Vue2 最新的 2.7 版本涉及到V3的新语法,我们直接下载2.6分支下的代码即可,代码下载 https://github.com/vuejs/vue/blob/2.6/dist/vue.js

  2. 新建一个 index.html 引入上面 js,并新建个 Vue 应用。

    <!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>
        <script src="./dist/vue.js"></script>
      </head>
      <body>
        <div id="app">{{msg}}</div>
        <script>
          let App = new Vue({
            el: "#app",
            data: {
              msg: "hello world",
            },
          });
        </script>
      </body>
    </html>
    
  3. 打上断点。根据上面代码可知,new Vue({...}) 是我们代码入口,我们打开 Vue.js 5088行,可以加上断点。

    image.png

Vue 的 _init 方法

当前方法挂载在Vue的原型上,接受一个参数,也就是我们创建Vue应用传入的参数,其中我们主要先看一下 initState 里面包含了 props methods data computed watch 的初始化和挂载。

Vue.prototype._init = function (options) {
  // 当前 this 也就是当前 Vue 实例
  var vm = this;
  // a uid
  vm._uid = uid$3++;

  var startTag, endTag;
  /* 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
  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);
  callHook(vm, 'beforeCreate');
  initInjections(vm); // resolve injections before data/props
  // 初始化 props methods data computed watch
  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);
  }
};

initState

主要实现的功能

  • initProps 初始化 props
  • initMethods 初始化 methods
  • initData、observe 数据监听
  • initComputed 始化 computed 计算属性
  • initWatch 初始化 watch 监听
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);
    }
  }

我们重点看 initMethods initData

initMethods

先判断 methods 中的属性是否是函数,不是则打印错误日志
判断 methods 中的属性名在 props 中是否已定义,已存在的话,打印出错误日志
判断如果方法名已存在在实例并且以 _ 或者 $ 开头,打印错误日志
将方法挂载到 vm 上,并更换执行时候的 this 绑定,绑定成当前的 Vue 实例 "vm"
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);
    }
  }

关于 bind、call、apply,三个方法都可以更改执行时候的 this,区别在于

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 
function.bind(thisArg[, arg1[, arg2[, ...]]])

apply() 方法调用一个具有给定 `this` 值的函数,以及以一个数组或一个[类数组对象]的形式提供的参数。

call() 方法使用一个指定的 `this` 值和单独给出的一个或多个参数来调用一个函数。

initData

data 数据初始化

判断 data 是否是一个函数,是的话通过 getData 来获取数据,否则直接取对应 data 数据
判断 data 是否是一个普通对象,否则打印报错日志
判断 data 中定义的属性是否在 props、methods 中已存在,存在的话打印报错日志
data 值挂载到 vm
数据监听,处理成响应式
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 */);
  }
  
  

getData

是函数时候,通过函数执行来获取data

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();
    }
  }

isReserved

判断是否是内部变量, _ 开头或者 $ 开头

function isReserved (str) {
    var c = (str + '').charCodeAt(0);
    return c === 0x24 || c === 0x5F
}
isReserved("_data") // true
isReserved("$data") // true
isReserved("data") // false
isReserved("props") // false

proxy 数据代理

访问 this.keyA 实际是访问 this._data.keyA 访问 this.keyB 实际是访问 this._props.keyB

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 定义一个新属性

Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。obj: 要定义属性的对象。 prop:要定义或修改的属性的名称或 Symbol。descriptor:属性描述符

Object.defineProperty(obj, prop, {
    // 该属性对应的值
    value: "",
    // 该属性为true,上面的value才会可以修改,默认值是 false
    writable: true,
    // 可枚举 默认为 false
    enumerable: true,
    // 可配置 默认为 false
    configurable: true,
    // 获取value,会执行当前函数
    get: noop,
    // 设置value,会执行当前函数
    set: noop
})
let obj = { name: "tom" }
Object.defineProperty(obj, 'ext1', {
    value:"扩展字段",
})
obj.ext1 = "12" // 修改会失败
for(let key in obj) console.log(key) // ext1 无法被枚举

Object.defineProperty(obj, 'ext3', {
    get(){
        console.log('ext3 get')
        return obj.ext3
    },
    set(val){
        obj.ext3 = val;
    }
})
obj.ext3 // undefined
obj.ext3 = "hello"
obj.ext3 // hello

总结

由上面可知其实 this 中可以访问到 data、methods数据,其实就是通过 proxy 方法进行数据代理访问的。