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 的掛載過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章