import Vue from ‘vue’(作者用的vue-cli一鍵生成)
node環境下import Vue from 'vue'
的作用是什麼意思?
在 NPM 包的 dist/ 目錄你將會找到很多不同的 Vue.js 構建版本。這裏列出了它們之間的差別:
具體參考:官網
完整版:同時包含編譯器和運行時的版本。
編譯器:用來將模板字符串編譯成爲 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 的掛載過程。