【源码阅读】Vue2中为什么this能获取methods和data?解析methods和data的初始化

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

这是源码共读的第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods

前言

为什么Vue2中的methods中 能直接使用this去 获得data中的属性, 当我们new一个Vue的时候 实际干了什么? 阅读本文你将了解其中的原理!

阅读准备

本地跑一个Server,通过CDN引入Vue生产文件,进行调试

<!DOCTYPE html>
<html lang="zh">

<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>
</head>

<body>
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

    <script>
        const Harexs = new Vue({
            data:{
                name: 'Harexs'
            },
            methods: {
                sayName() {
                    console.log(this.name)
                }
            }
        })
        Harexs.sayName()
    </script>
</body>
</html>
复制代码

可以使用http-server, npm i http-server -g

本地运行 http-server -c-1

调试

image.png

在入口处打下断点,如图中所示 15行处断下一个断点然后重新运行页面

快捷键

  1. F8 继续运行代码直至遇到下一个断点或代码结束
  2. F9 单步调试,遇到函数会进入到函数内部继续执行
  3. F10单步调试,但是会跳过函数的内部执行,代码继续往下走
  4. F11 进入下一个函数的调用
  5. shift + F11 跳出当前函数的执行

入口

在刚刚断点的位置,我们按下F11 进行初始化函数的调用(5087-5093行)

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

instanceof运算符用于检测构造函数的Prototype属性是否出现在实例对象的原型链上,也就是判断这个对象的__proto__的查找最后是否能找到这个构造函数的Prottoype,即这个对象是不是这个函数New出来的

instanceof

剖一个代码例子

function Harexs(){}
let haxs = {}
haxs.__proto__ = Harexs.prototype
console.log(haxs instaceof Harexs) //true
复制代码

如果你了解过new关键字的原理,它的实现过程 其实也是将创造的实例对象的__proto__指向了被new函数的Prototype

初始化

接下来,我们F9 单步下去,进入_init 初始化函数

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);
      }
    };
复制代码

这里是和初始化相关调用,我们F10单步往下走,到initState的位置,按下F11进入initState函数内部,它和我们的props methods data watch computed初始化有关

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, 它比initData更早

initMethods

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);
    }
  }
复制代码

函数内部主要是一个For...in的遍历, 它对三种特殊情况做了处理

  1. 先判断传进来的对象下的methods,即每一个key对应的value值的类型是否都是function
  2. 判断每个key是否已经出现在了props中,这里的hasOwn是Object.prototype.hasOwnProperty(), 判断某个属性是否存在这个对象下
  3. 判断每个key是否已经在vm下存在过,vm是我们的this, 即被实例化的对象,并且key和内部预留的关键字不起冲突,key开头不包含_$ 字符

最后,在vm下,挂载一个相同的key的函数, 并且这个函数 是通过 bind方法返回的一个高阶函数,它的this已经指向了vm, 机智的小伙伴在这已经猜到为啥 methods 内部的this可以直接获取到 data

到了这一步,此时vm下, 即被实例出来的Harexs对象,就有了一个sayName属性

initData

我们按下shift+F11跳出initMethods函数,接着F9单步到 initData函数F11进入函数内部

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 */);
  }
复制代码

函数内部先判断传进来的data对象是不是一个function, 主要和SFC组件有关,防止对象的拷贝,并且会判断处理后的data是不是一个纯对象, 我们接着看下面的主要逻辑

 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);
      }
    }
复制代码

遍历data中的每个key 是否和 methods 以及 props 出现 同名的 key, 最后再判断是否有使用预留的关键字, 最后进入proxy 函数

proxy函数

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);
  }
复制代码

sharedPropertyDefinition 是一个包含了存储描述符的对象, 用来给 Object.defineProperty 定义对象描述使用

proxy函数内部 先将 sharedPropertyDefinition的 get set 分别赋值,返回 this[souceKey][key],代入对象也就是this['_data']['name'], _data对应的就是我们传入的data属性

最后最关键的也就是对象的定义,也就是Object.defineProperty(vm,'name',sharedPropertyDefinition), 这里就类似methods那边的操作,给vm下同时挂载了这个属性,此时就可以直接使用this.name 来得到data中的这个属性

我们代入对象来看这个代码就清晰明了了

Object.defineProperty(vm, 'name', {
    enumerable: true,
    configurable: true,
    get:function proxyGetter () {
      return this['_data']['name']
    },
    set:function proxySetter (val) {
      this['_data']['name'] = val;
    }
});
复制代码

至此methodsdata的初始化就结束了,我们简单概述下:

methods的初始化 会先判断 props 是否有同名key,再看本身是否是一个function,以及是否使用了预留关键字, 最后通过bind方法返回一个 指向了this的相同函数 挂载在 this

data会创建一个_data副本属性, 存储以及读取是通过_data操作,先判断props以及methods是否存在同名的key, 最后将每个key挂载在 this下, 它们通过Object.defineProperty定义了get和set

实现

接下来我们实现一个 简化版的 methods 和 data

    //空函数
    const noop = () => { }

    //对象定义
    let ObjectDefine = {
        enumerable: true,
        configurable: true,
        get: noop,
        set: noop
    };
    
    //对象定义
    function proxy(target, soucekey, key) {
        ObjectDefine.get = function proxyGetter() {
            return target[soucekey][key]
        }
        ObjectDefine.set = function proxyGetter(val) {
            target[soucekey][key] = val
        }
        Object.defineProperty(target, key, ObjectDefine)
    }

    function initMethods(vm, methods) {
        let props = vm.$options.props
        //遍历methods里的每个function
        for (let key in methods) {
            //是否是一个方法
            if (typeof methods[key] !== 'function') {
                throw new TypeError(`Method ${key} must be function`)
            }
            //是否已经在props存在
            if (props && props.hasOwnProperty(key)) {
                throw new TypeError(`Method ${key} has already been defined as a prop`)
            }
            //是否存在this上 并且使用了预留关键字
            if ((key in vm) && (key.startsWith('$') || key.startsWith('_'))) {
                throw new TypeError(`Method ${key} conflicts with an existing Vue instance method.
                Avoid defining component methods that start with _ or $.`)
            }
            //挂载在this上
            vm[key] = typeof methods[key] != 'function' ? noop : methods[key].bind(vm)
        }
    }

    function initData(vm) {
        vm._data = vm.$options.data
        //得到data的key数组 以及 props和methods
        let keys = Object.keys(vm.$options.data)
        let props = vm.$options.props
        let methods = vm.$options.methods

        let i = keys.length
        //循环
        while (i--) {
            let key = keys[i]
            if (methods && methods.hasOwnProperty(key)) {
                throw new TypeError(`data ${key} has already been defined as a methods`)
            }
            if (props && props.hasOwnProperty(key)) {
                throw new TypeError(`data ${key} has already been defined as a props`)
            }
            if (key.startsWith('$') || key.startsWith('_')) {
                throw new TypeError(`data ${key} conflicts with an existing Vue instance data.
                Avoid defining component methods that start with _ or $.`)
            }

            proxy(vm, '_data', key)
        }
    }

    function Harexs(options) {
        let vm = this
        //将传入的对象挂载在 this下的$options
        vm.$options = options
        let methods = vm.$options.methods

        //初始化methods
        initMethods(vm, methods)
        //初始化data
        initData(vm)
    }
    const haxs = new Harexs({
        props: {
            test: "haha"
        },
        data: {
            name: 'Harexs'
        },
        methods: {
            sayName() {
                console.log(this.name)
            }
        }
    })

    //Harexs
    haxs.sayName()
复制代码

总结

  1. instanceof 运算符的 原理以及作用
  2. Vue2中 methods 以及 data 初始化流程
  3. hasOwnProperty 以及 startsWith 方法的使用
  4. bind 在 methods 中的 绑定作用,防止this丢失

猜你喜欢

转载自juejin.im/post/7130459967772950535