vue源码分析系列一:new Vue的初始化过程

import Vue from ‘vue’(作者用的vue-cli一键生成)

node环境下import Vue from 'vue'的作用是什么意思?
在 NPM 包的 dist/ 目录你将会找到很多不同的 Vue.js 构建版本。这里列出了它们之间的差别:
vue各个版本图
具体参考:官网

完整版:同时包含编译器和运行时的版本。

编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。

运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。

UMD:UMD 版本可以通过

CommonJS:CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本 (vue.runtime.common.js)。

开始看代码:

在利用webpack的Vue项目中,在main.js中通过import Vue from 'vue'导入的vue包如下图所示。(在node_modules/vue/package.json中配置了main属性)
在这里插入图片描述
通过官网的解析,我们可以知道:这个包功能其实是不完整的,只有runtime-only的功能。来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码;基本上就是除去编译器的其它一切。

因为运行时版本相比完整版体积要小大约 30%,所以应该尽可能使用这个版本。如果你仍然希望使用完整版,则需要在打包工具里配置一个别名:
webpack:

module.exports = {
  // ...
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js'
    }
  }
}

或者我们可以直接引入:import Vue from '../node_modules/vue/dist/vue.js'

查看主流程上的源码-vue.runtime.common.js

import Vue from 'vue'过程中,Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。下面是源码中的初始化函数:

initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

我们可以改一下:

setTimeout(function(){
	console.log('初始化开始');
	initMixin(Vue);
	stateMixin(Vue);
	eventsMixin(Vue);
	lifecycleMixin(Vue);
	renderMixin(Vue);
	console.log('初始化结束');
},2000)

然后重新运行npm run dev
在这里插入图片描述
初始化不依赖浏览器。

initMixin()

function initMixin (Vue) {
  Vue.prototype._init = function (options) {// 在Vue的原型上定义 _init 方法
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    var startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && 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 */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);// 初始化生命周期
    initEvents(vm);// 初始化事件
    initRender(vm);// 初始化浏览器渲染方法
    callHook(vm, 'beforeCreate');// 初始化beforeCreate钩子
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && 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);// 挂载dom元素
    }
  };
}

new Vue()

在此之前,我修改了一下这3个入口文件:
在这里插入图片描述
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-cli</title>
  </head>
  <body>
    <div id="application"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

App.vue

<template>
  <div id="component">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'appy',
	data () {
		return {
			msg: 'vue源码解析',
		}
	},
}
</script>

main.js

import Vue from 'vue'
import App from './App'

Vue.config.productionTip = false
Vue.config.performance = true

let vm = new Vue({
  el: '#application',
  components: { App },
  template: '<App/>'
})

console.log('Vue.version', Vue.version)
console.log('vm', vm)

通过initMixin方法之后,执行 new Vue() 操作,在 Vue构造函数内部,调用了vm原型上的 _init()方法,在this._init()方法中我们目前主要关注的是 initState()方法

function Vue (options) {
     if (process.env.NODE_ENV !== 'production' &&
       !(this instanceof Vue)
     ) {
       warn('Vue is a constructor and should be called with the `new` keyword');
     }
     this._init(options);// 在Vue原型上定义了的方法(initMixin方法中),使构建出来的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);
  }
}

initData()

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function' // 在vm 上定义一个 '_data' 属性
    ? getData(data, vm) // getData()方法返回的就是我们在vue data()方法中定义了的属性和方法
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== 'production' && 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 (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && 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 */);
}

proxy()

在这个方法中,通过Object.defineProperty()来实现数据劫持,当我们通过 this.xxx 访问我们定义的数据时,其实就是访问的 this._data.xxx,从而达到数据代理的目的-双向数据绑定。

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

function proxy (target, sourceKey, key) {
// target是vm, sourceKey是'_data', key 是我们定义的data中的键
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

总结

Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,这样的编程思想是非常值得借鉴和学习的。

为了弄清楚模板和数据如何渲染成最终的 DOM,所以各种初始化逻辑我们先不看。在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM,下一节我们来分析 Vue 的挂载过程。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值