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

66 阅读3分钟

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

步骤

1. 进入Vue

    const vm = new Vue({    //打上断点
        data: {
            name: 'liuyang',
        },
        methods: {
            sayName(){
                console.log(this.name);
            }
        },
    });

2. 进入this._init(options);

  function Vue (options) {
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options); // 进入
  }

3. 进入initState(vm);

      vm._self = vm;
      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      initState(vm); 
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');

4. 分别进入initMethods() 和 initData()

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

5.initMethods()

  function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        if (typeof methods[key] !== 'function') { // 如果method的key不是函数就报警告
          warn(
            "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        if (props && hasOwn(props, key)) { // 如果props存在,且props也有key这个属性那么会报警告(方法xx已经在props里面定义了)
          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); // 这里是this.方法名能获取到的关键
    }
  }

6. initData()

  function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm) // getData()
      : 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); // data对象所有属性值组成的数组
    var props = vm.$options.props;
    var methods = vm.$options.methods; //
    var i = keys.length; // 长度i
    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); // proxy函数 重点
      }
    }
    // observe data
    observe(data, true /* asRootData */); //TODO 这里也是一个重点
  }

调用vm.key的时候实际上是调用vm[_data][key] 我们打印Vue实例的时候也能看到_data对象

  var sharedPropertyDefinition = {
    enumerable: true, //为true时该属性才会出现在对象的枚举属性中,默认false
    configurable: true,
    get: noop, // noop(function) 啥也不返回
    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() 函数的详细介绍可以去看MDN
  }

总结

Vue2中能获取到methods的原因: vue 将methods中的方法变成vm.xxx 然后使用bind方法改变了函数的指向,将其指向变成了vm 能获取到data的原因:vue创建了一个_data用来保存data中的数据 当this.data[key]的时候 调用的是this._data[key]

简单模拟一下

<!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>test</title>
</head>
<body>
  <script>
    let sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: null,
      set: null
    };
    // 初始化data
    function initData(vm) {
      const keys = Object.keys(vm.$option.data)
      let i = keys.length
      while (i--) {
        const key = keys[i]
        proxy(vm, "_data", key)
      }
    }
    function proxy(target, sourceKey, key) {
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      }
      sharedPropertyDefinition.set = function proxySetter (val) {
        return this[sourceKey][key] = val
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    // 初始化methods
    function initMethods(vm, methods) {
      for (const key in methods) {
        vm[key] = methods[key].bind(vm)
      }
    }
    function Person(option) {
      const vm = this
      vm.$option = option
      vm._data = vm.$option.data
      if (option.data) {
        initData(vm)
      }
      if (option.methods) {
        initMethods(vm, option.methods)
      }
    }
    const p = new Person({
      data: {
        name: 'xxx'
      },
      methods: {
        sayName() {
          console.log(`in Function--${this.name}`);
        }
      }
    })
    console.log(p.name);
    p.sayName()
  </script>
</body>
</html>

getData 函数的作用其实就是获取真正的数据对象并返回,即:data.call(vm, vm),try..catch有错误发生那么则返回一个空对象。

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

出现的vue2的一些工具函数

在文章最后复习一下 前不久刚看完vue2的工具函数

hasOwn 检查一个对象是否具有某个属性

let hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(params) {
  return hasOwnProperty.call(obj, key)
}

isReserved 检查属性是否是保留的字符串 $ 或者 _

  function isReserved (str) {
    var c = (str + '').charCodeAt(0);
    return c === 0x24 || c === 0x5F
  }

bind 有一些地方不支持bind方法 这里是兼容性处理

  function polyfillBind (fn, ctx) {
    function boundFn (a) {
      var l = arguments.length;
      return l
        ? l > 1
          ? fn.apply(ctx, arguments)
          : fn.call(ctx, a)
        : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
  }

  function nativeBind (fn, ctx) {
    return fn.bind(ctx)
  }

  var bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;