likes
comments
collection
share

【源码共读】第23期 | new Vue()的过程发生了什么?

作者站长头像
站长
· 阅读数 16

前言

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <script>
        const vm = new Vue({
            data: {
                name: '我是AcWrong',
            },
            methods: {
                sayName(){
                    console.log(this.name);
                }
            },
        });
    console.log(vm.name); //我是AcWrong
    console.log(vm.sayName());//我是AcWrong
    </script>
</body>
</html>

上面的代码中,我们用Vue写了一段代码,以此来研究new Vue()的过程到底发生了什么。 除此之外,我们还会研究研究一下以下几个问题:

  • 为什么在methods中可以直接使用this.name访问到data中的数据
  • 为什么vm.name就可以获取到data中的数据、vm.sayName()调用的就是methods中的方法?

调试代码

Vue构造函数

【源码共读】第23期 | new Vue()的过程发生了什么? 在此处打上断点后进入函数,查看函数的内容: 【源码共读】第23期 | new Vue()的过程发生了什么?

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

这就是Vue构造函数的源码了。

_init初始化函数

我们继续在this._init(options)处打上断点,随后进入函数查看内容: 【源码共读】第23期 | new Vue()的过程发生了什么? 可以看到,Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。 知道了new Vue()的过程大致发生了什么时候,我们就可以去研究刚刚提到的问题了:

  • 为什么在methods中可以直接使用this.name访问到data中的数据
  • 为什么vm.name就可以获取到data中的数据、vm.sayName()调用的就是methods中的方法?

要解开这两个问题,我们就要知道data和method是在哪里被初始化的。 在initState处打上断点,进入initState查看函数的代码。 【源码共读】第23期 | new Vue()的过程发生了什么?

initState函数

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

从函数内容来看,我们可以发现这个函数主要实现了以下功能:

初始化 props
初始化 methods
监测数据
初始化 computed
初始化 watch

因此,我们可以继续完善new Vue()过程中发生了什么的答案: Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,在initState中初始化 data、props、computed、watcher等等。

我们重点来看初始化 methods,之后再看初始化 data。

initMethods处打上断点,进入initMethods函数查看。

initMethods

可以看到,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);
    }
  }

initMethods主要做了以下判断

判断 methods 中的每一项是不是函数,如果不是警告。
判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告

这边涉及到两个函数,调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。

hasOwn 是否是对象本身拥有的属性

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

不直接用obj.hasOwnProperty(key)判断是为了防止obj.hasOwnProperty()被意外修改而返回错误的结果,保证程序的健壮性。

isReserved 是否是内部私有保留的字符串$ 和 _ 开头

/**
   * Check if a string starts with $ or _
   */
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

因此,我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。

bind 返回一个函数,修改 this 指向

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;

这边是为了兼容老版本不支持 原生的bind函数。

initData 初始化 data

看完了initMethods函数,我们退出函数回到上文提到的initData(vm)函数断点处。 initData函数内容如下:

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

可以看到,这段代码的功能如下:

给当前的vm对象上的_data赋值,以备后用。
最终获取到的 data 不是对象给出警告。
遍历 data ,其中每一项:
如果和 methods 冲突了,报警告。
如果和 props 冲突了,报警告。
不是内部私有的保留属性,做一层代理,代理到 _data 上。
最后监测 data,使之成为响应式的数据。

proxy 代理

用 Object.defineProperty 定义对象,这里用处是:this.xxx 则是访问的 this._data.xxx。

/**
   * Perform no operation.
   * Stubbing args to make Flow happy without leaving useless transpiled code
   * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
   */
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);
}

实现Vue初始化过程简易版

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);
}
function initData(vm){
  const data = vm._data = vm.$options.data;
  const keys = Object.keys(data);
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    proxy(vm, '_data', key);
  }
}
function initMethods(vm, methods){
  for (var key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
  } 
}

function Person(options){
  let vm = this;
  vm.$options = options;
  var opts = vm.$options;
  if(opts.data){
    initData(vm);
  }
  if(opts.methods){
    initMethods(vm, opts.methods)
  }
}

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

console.log(p.name);
console.log(p.sayName());

总结

这篇文章总共涉及到了三个问题:

  1. new Vue()的过程发生了什么?这也是面试的常考题
  2. 为什么this直接访问到methods里面的函数?
  3. 为什么 this 直接访问到 data 里面的数据

下面进行答案的汇总:

  1. Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,在initState中初始化 data、props、computed、watcher等等。
  2. 通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。
  3. 通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx。

一天进步一点点,加油。