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

74 阅读2分钟

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

学习链接:juejin.cn/post/701092…

1、问题-如何访vue2得到data与methods

function Person(options){

}

const p = new Person({
    data: {
        name: '若川'
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    }
});

console.log(p.name);
// undefined
console.log(p.sayName());
// Uncaught TypeError: p.sayName is not a function

2、准备

新建文件index.html,body中增加下面代码

<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <script>
      const vm = new Vue({
        data: {
          name: 'haha'
        },
        methods: {
          sayName() {
            console.log(this.name)
          }
        }
      })
      console.log(vm.name)
      console.log(vm.sayName())
    </script>

调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。F11进入函数,走到Vue构造函数。

3、源代码解释

1、Vue构造函数

  function Vue (options) {
    if (!(this instanceof Vue) //检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
    ) {
      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);

if (!(this instanceof Vue)){} 判断是不是用了 new 关键词调用构造函数。因为Vue一个项目会new Vue()一次,所以平时开发不需要写。

继续在 this._init(options)打上断点,并F11进入。

2、_init(options) 初始化函数

function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      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
      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);
      }
    };
  }

3、 initState(vm) 初始化状态

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }  //初始化props
    if (opts.methods) { initMethods(vm, opts.methods); }  //初始化methods
    if (opts.data) {  //初始化data
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }  //初始化computed
    if (opts.watch && opts.watch !== nativeWatch) {  //初始化watch
      initWatch(vm, opts.watch);
    }
  }

调试,进入initMethods方法

4、initMethods 初始化方法

function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        if (typeof methods[key] !== 'function') { //判断methods每一项不是函数
          warn(
            "Method "" + key + "" has type "" + (typeof methods[key]) + "" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        if (props && hasOwn(props, key)) {  //判断methods每一项函数与props的函数冲突
          warn(
            ("Method "" + key + "" has already been defined as a prop."),
            vm
          );
        }
        //判断 methods每一项是否在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ 开头
        //( _ $ 在JS中一般指内部变量标识)
        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 将this指向vm
  }
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;

5、initData 初始化数据

function initData (vm) {
    var data = vm.$options.data; 
    data = vm._data = typeof data === 'function'   //先用 _data 赋值,后续使用
      ? 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)) {   //每一项data与methods冲突
          warn(
            ("Method "" + key + "" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props && hasOwn(props, key)){      //每一项data与props名冲
        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);  //代理到_data上
      }
    }
    // observe data
    observe(data, true /* asRootData */); //检测,成为响应式的数据
  }

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

2、proxy 代理

function noop (a, b, c) {}
var sharedPropertyDefinition = {
    enumerable: true, //可配置
    configurable: true,  //可删除
    get: noop,  //get方法
    set: noop   //set方法
};

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);
}
//es3方式 允许删除属性和改变属性值
var person = {};
person.legs = 2;

//es5方式  值默认是undefined 属性值默认为false
var person = {};
Object.defineProperty(person, 'legs', {
    value: 2,
    writable: true,  //可写
    configurable: true, //可删除
    enumerable: true  //可通过for...in 枚举属性
});
var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1  (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)

6、hasOwn 对象本身是否拥有该属性

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

7、isReserved 是否是内部预留的字符串$或者_开头

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

4、简易实现

vue data实现 将数据代理到_data上,observe实现响应式检测,通过get与set挂在vm上,获取与修改数据。

  function Vue(options) {
        let vm = this
        vm.$options = options
        let opt = vm.$options
        if (opt.data) {
          initData(vm, opt.data)
        }
        if (opt.methods) {
          initMethods(vm, opt.methods)
        }
      }

      function noop(a, b, c) {
        return {}
      }

      const sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: noop,
        set: noop
      }

      function initData(vm, data) {
        data = vm._data = typeof data === 'function' ? data.call(data, vm) : data || {}
        let keys = Object.keys(data)
        let len = keys.length
        while (len--) {
          proxy(vm, '_data', keys[len])
        }
      }

      function proxy(target, sourceKey, key) {
        sharedPropertyDefinition.get = function proxyGetter() {
          return target[sourceKey][key]
        }
        sharedPropertyDefinition.set = function proxySetter(val) {
          target[sourceKey][key] = val
        }
        Object.defineProperty(target, key, sharedPropertyDefinition)
      }
      function initMethods(vm, methods) {
        for (var key in methods) {
          vm[key] = typeof methods[key] === 'function' ? methods[key].bind(vm) : noop()
        }
      }
      

vue实例对象