2017/6/2 15:27:50 第一次復習
vue 框架號稱五分鍾就能上手,半小時就能精通,這是因為其使用非常簡單,就像下面一樣:
let vm = new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
在最開始,我傳遞了兩個選項
el 以及
data ,很簡單,官網上也是這樣寫的。
你肯定注意到了,我使用了 new 操作符。這就很自然的想到,Vue 就是一個構造函數,vm是
Vue構造函數 生成的實例,我們的配置項是傳入構造函數的參數,是一個包括
el 屬性 和 data屬性的對象,事實上在實例化 Vue 時,傳入的選項對象可以包含
數據、模板、掛載元素、方法、生命周期鈎子等選項。全部的選項可以在 vue的官方API 文檔中查看。;
那么我們下面就要受好奇心的驅動,來看看
Vue構造函數 是什么樣的?
在
\node_modules\vue\src\core\instance\index.js
文件里面,是下面的代碼:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default V
不用害怕,我帶你捋一捋,我們首先關注第8行,我摘抄出來:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && // 這個 if 判斷,是當你不用new操作符來實例化Vue構造函數時,會爆出警告
!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 主要就是這一句,
}
發現了吧,Vue 的確是一個構造函數,和你平時使用的
Array, Object 等普普通通的構造函數,沒有本質的區別。
在構造函數里面,我們要關心的是
this._init( options ) , 稍微我會詳細的來講,我們先看
\node_modules\vue\src\core\instance\index.js
文件中的第16行~20行:
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
上面的代碼調用了五個方法,這五個方法都是把Vue構造函數作為參數傳入,其目的都是
在 Vue .prototype 上掛載方法或屬性,這個概念很好理解,我們在js 的原型鏈繼承的學習中,經常把屬性和方法丟到構造函數的原型上作為公有的屬性和方法。
// initMixin(Vue) src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue) src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
// renderMixin(Vue) src/core/instance/render.js **************************************************
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
Vue.prototype._s = _toString
Vue.prototype._v = createTextVNode
Vue.prototype._n = toNumber
Vue.prototype._e = createEmptyVNode
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = function(){}
Vue.prototype._o = function(){}
Vue.prototype._f = function resolveFilter (id) {}
Vue.prototype._l = function(){}
Vue.prototype._t = function(){}
Vue.prototype._b = function(){}
Vue.prototype._k = function(){}
// eventsMixin(Vue) src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************
Vue.prototype._mount = function(){}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype._updateFromParent = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
經過上面5個方法對Vue構造函數的處理,vm實例上就可以使用這些屬性和方法了。其實在其他地方,Vue 構造函數也被處理了:在
src/core/index.js 文件中:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', { //為 Vue.prototype 添加$isServer屬性
get: isServerRendering
})
Vue.version = '__VERSION__' // 在VUE 身上掛載了 version的靜態屬性
export default Vue
initGlobalAPI() 的作用是在 Vue 構造函數上掛載靜態屬性和方法,Vue 在經過 initGlobalAPI 之后,會變成這樣:
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
Vue.prototype.$isServer
Vue.version = '__VERSION__'
下一個就是
web-runtime.js 文件了,
web-runtime.js 文件主要做了三件事兒:
1、覆蓋 Vue.config 的屬性,將其設置為平台特有的一些方法
2、Vue.options.directives 和 Vue.options.components 安裝平台特有的指令和組件
3、在 Vue.prototype 上定義 __patch__ 和 $mount
經過 web-runtime.js 文件之后,Vue 變成下面這個樣子:
// 安裝平台特定的utils
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp
// 安裝平台特定的 指令 和 組件
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount
這里要注意的是Vue.options 的變化。
最后一個處理 Vue 的文件就是入口文件
web-runtime-with-compiler.js 了,該文件做了兩件事:
1、緩存來自
web-runtime.js 文件的 $mount 函數
const mount = Vue.prototype.$mount
2、在 Vue 上掛載 compile
Vue.compile = compileToFunctions
上面
compileToFunctions 函數可以將模板 template 編譯為render函數。
至此,我們算是還原了 Vue 構造函數,總結一下:
1、Vue.prototype 下的屬性和方法的掛載主要是在 src/core/instance 目錄中的代碼處理的
2、Vue 下的靜態屬性和方法的掛載主要是在 src/core/global-api 目錄下的代碼處理的
3、web-runtime.js 主要是添加web平台特有的配置、組件和指令,web-runtime-with-compiler.js 給Vue的 $mount 方法添加 compiler 編譯器,支持 template。
好了,我們再回過頭來看 this._init() 方法,_init() 方法就是Vue調用的第一個方法,然后將我們的參數 options 傳了過去。_init() 是在
\node_modules\vue\src\core\instance\init.js 文件中被聲明的:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-init:${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 { // 大部分情況下是走了這個分支,也是vue第一步要做的事情,使用mergeOptions來合並參數選項
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')
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(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
好了,我們一開始不需要關心那么多邊邊角角,直接從23行代碼開始看,因為大部分情況下是走了這條分支,也就是執行了下面的代碼:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
這里是執行了
mergeOptions 函數,並將返回值賦值給
vm.$options 屬性。 mergeOptions 函數接受三個參數,分別是
resolveContructorOptions方法,
我們調用 vue 構造函數傳入的配置對象(如果沒有就是空對象),以及
vm 實例 本身。
我們先看
resovleContructorOptions 方法, 傳入的參數是 vm.constructor 。
vm.constructor 代表的是啥?
const
vm
:
Component
=
this
人家_init() 函數第一行就定義了,是指向_init() 函數內部的this, _init( ) 函數是 Vue.prototype上的一個方法,所以在其身上調用的時候,this 指向本身
Vue.prototype, 那么 vm.constructor 也就是指向 Vue 構造函數.
export function resolveConstructorOptions (Ctor: Class<Component>) { //ctor 就是 VUE 構造函數
let options = Ctor.options // vue 構造函數身上的 options 屬性
if (Ctor.super) { // 判斷是否定義了 Vue.super ,這個是用來處理繼承的,我們后續再講
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
第22行,
resolveConstructorOptions 方法直接返回了 Vue.options。也就是說,傳遞給 mergeOptions 方法的第一個參數其實是 Vue.options。那么,實際上原來的代碼就變成了下面這樣:
// 這是原來的代碼
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// 實際上傳過去的參數是下面這些
vm.$options = mergeOptions(
// Vue.options
{
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
},
// 調用Vue構造函數時傳入的參數選項 options
{
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
},
// this
vm
)
為什么要使用 mergeOptions 方法呢? 是為了 合並策略, 對於子組件和父組件如果有相同的屬性(option)時要進行合並,相關文章:
那么我們繼續查看 _init() 方法在合並完選項之后,Vue 第二部做的事情就來了:初始化工作與Vue實例對象的設計:
通過initData 看vue的數據響應系統
Vue的數據響應系統包含三個部分:
Observer 、
Dep 、
Watcher 。我們還是先看一下 initData 中的代碼:
function initData (vm: Component) {
let data = vm.$options.data // 第一步還是要先拿到數據,vm.$options.data 這時候還是通過 mergeOptions 合並處理后的 mergedInstanceDataFn 函數
data = vm._data = typeof data === 'function'
? data.call(vm)
: 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
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
while (i--) {
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else {
proxy(vm, keys[i]) // 目的是在實例對象上對數據進行代理,這樣我們就能通過 this.a 來訪問 data.a 了
}
}
// observe data
observe(data)
data.__ob__ && data.__ob__.vmCount++
}
上面 proxy 方法非常簡單,僅僅是在實例對象上設置與 data 屬性同名的訪問器屬性,然后使用 _data 做數據劫持,如下:
function proxy (vm: Component, key: string) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, { // vm是實例,key是data屬性上的屬性,
configurable: true,
enumerable: true,
get: function proxyGetter () {
return vm._data[key]
},
set: function proxySetter (val) {
vm._data[key] = val
}
})
}
}
做完數據的代理,就正式進入響應系統:
observe(data)
我們說過,數據響應系統主要包含三部分:
Observer 、Dep、Watcher,
我們首先思考,我們應該如何觀察一個數據對象的變化?
vue.js和avalon.js 都是通過 Object.definedProperty() 方法來實現的, 下面我們主要來介紹一下這個方法為什么可以實現對對象屬性改變的監聽。
Object.defineProperty ( )有三個參數, 三個參數都需要,分別是對象,屬性,屬性的屬性
var o = {};
Object.definedProperty(o, 'a', {
value: 'b'
})
屬性的屬性有下面這些:
configurable:true | false,
enumerable:true | false,
value:任意類型的值,
writable:true | false
writable
該屬性的值是否可以修改;如果設置為false,則不能被修改,修改不會報錯,只是默默的不修改;
var obj = {}
//第一種情況:writable設置為false,不能重寫。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //hello
//第二種情況:writable設置為true,可以重寫
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //change value
enumerable
是否該屬性可以被
for……in 或者
Object.keys( ) 枚舉
var obj = {}
//第一種情況:enumerable設置為false,不能被枚舉。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false
});
//枚舉對象的屬性
for( var attr in obj ){
console.log( attr );
}
//第二種情況:enumerable設置為true,可以被枚舉。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:true
});
//枚舉對象的屬性
for( var attr in obj ){
console.log( attr ); //newKey
}
configurable
是否可以刪除目標屬性或是否可以再次修改屬性的特性(writable, configurable, enumerable)。設置為true可以被刪除或可以重新設置特性;設置為false,不能被可以被刪除或不可以重新設置特性。默認為false。
這個屬性起到兩個作用:
1、
目標屬性是否可以使用delete刪除
2、目標屬性是否可以再次設置特性
//-----------------測試目標屬性是否能被刪除------------------------
var obj = {}
//第一種情況:configurable設置為false,不能被刪除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//刪除屬性
delete obj.newKey; //可以用delete 關鍵字來刪除某一個對象上的屬性
console.log( obj.newKey ); //hello
//第二種情況:configurable設置為true,可以被刪除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//刪除屬性
delete obj.newKey;
console.log( obj.newKey ); //undefined
//-----------------測試是否可以再次修改特性------------------------
var obj = {}
//第一種情況:configurable設置為false,不能再次修改特性。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//重新修改特性
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true,
enumerable:true,
configurable:true
});
console.log( obj.newKey ); //報錯:Uncaught TypeError: Cannot redefine property: newKey
//第二種情況:configurable設置為true,可以再次修改特性。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//重新修改特性
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true,
enumerable:true,
configurable:true
});
console.log( obj.newKey ); //hello
一旦使用
Object.defineProperty
給對象添加屬性,那么如果不設置屬性的特性,那么configurable、enumerable、writable這些值都為默認的false
存取器描述
:
get set
不能
同時
設置訪問器 (get 和 set) 和 wriable 或 value,否則會錯,就是說想用(get 和 set),就不能用(wriable 或 value中的任何一個)
注意:get set是加在對象屬性上面的,不是對象上面的;賦值或者修改該對象屬性,會分別觸發get 和 set 方法;
正規用法:
var o = {}; // 不能是O.name=" dudu "了
var val = 'dudu'; // o 對象上的屬性是其他人家的一個變量
Object.definedProperty(o,'name',{ // Object.definedProperty( ) 方法通過定set get 方法,強行給拉郎配
get:function(){ return val }; //get: return val 把人家變量給返回了,就是人家的人了
set;function(value){ val = value } //set: val = value 把人家變量賦值為傳進來的參數,就是人間人了
})
實驗性代碼:
var O = {};
Object.definedProperty(o,"name",{
set:function(){console.log('set')}; //在獲取對象該屬性的時候觸發,
get:function(){console.log('get')}; // 在設置對象該屬性的時候觸發 , 並不會真正的設置;因為沖突了value,默認是falue
})
所以,你看到這里,基本上就能夠明白,通過Object.defineProperty()來重寫對象的get, set 方法,就可以在對象屬性被訪問和修改的時候獲知 ,從而觸發響應的回調函數,但是同一個數據屬性,很可能有多個 watcher 來訂閱的 ,所觸發的回調函數可能有很多,不可能都寫在 get set 里面,我們更希望更通過這樣的方式:
var data = {
a: 1,
b: {
c: 2
}
}
observer(data) // 在這里遍歷改寫了get,set
new Watch('a', () => {
alert(9)
})
new Watch('a', () => {
alert(90)
})
new Watch('b.c', () => {
alert(80)
})
現在的問題是, Watch 構造函數要怎么寫?
在 Watch 構造函數里面,我們已經可以獲取到 data,當我們訪問的時候,就會觸發 data 的改寫的get 方法:
class Watch {
constructor (exp, fn) {
// ……
data[exp] // 觸發了data 身上的get 方法
}
}
當我們每實例化一個 Watch來訂閱data上的a屬性 ,
data.a 上的get 方法就會被觸發一次, data.a 就多了一個訂閱器。那么問題來了,這么多的訂閱器watcher,我們肯定希望放在一個數組上進行管理,同時我們還希望有,向數組中 push 新的訂閱器watcher的方法, 逐個觸發數組中各個watcher的方法等等。這樣,我們的data 上的每一個屬性,它都有一個數組來放訂閱器,都有相應的方法來操作這個數組。根據面向對象中的思想,我們可以把這個數組和操作數組的方法放進一個對象中, 這個對象就叫dep吧 :
dep {
subs: [watcher1,watcher2,watcher3], // subs 屬性是一個數組,用來維護眾多訂閱器
addSubs: function(){ this.subs.push( …… ) },
notify: function() {
for(let i = 0; i< this.subs.length; i++){
this.subs[i].fn()
}
}
}
dep 對象我們希望用構造函數來生成,這樣會比較方便:
class Dep {
constructor () {
this.subs = []
}
addSub () {
this.subs.push(……)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
}
接下來,我們要在每一個data 屬性上生成一個dep實例對象:
function defineReactive (data, key, val) { // 這個函數就是用來重寫對象屬性的get set 方法
observer(val) // 遞歸的調用從而遍歷
let dep = new Dep() // 在這里實例化一個dep實例
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.addSub() //每當有訂閱者訂閱,我就新增一個
return val
},
set: function (newVal) {
if(val === newVal){
return
}
observer(newVal)
dep.notify() // 新增
}
})
}
等等,在第8行,執行 dep.addSub , 我怎么知道是要push 進去哪個 watcher 呢? 我們需要改寫一下 watch 的構造函數:
Dep.target = null //類似於全局變量的一個東西,用來放 這次實例化的watcher
function pushTarget(watch){
Dep.target = watch
}
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
pushTarget(this) // 讓Dep.target賦值為本次實例化的實例
data[exp] //緊接着就觸發get 方法
}
}
被觸發的get 方法在下面:
get: function () {
dep.addSub() //好吧,我又被觸發了一次,
return val
},
dep.addSub() 方法的廬山真面目:
class Dep {
constructor () {
this.subs = []
}
addSub () {
this.subs.push(Dep.target)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
}