vue 源碼深刻學習分析——史上超詳細

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.$dataVue.prototype.$set = setVue.prototype.$delete = delVue.prototype.$watch = function(){}// renderMixin(Vue) src/core/instance/render.js **************************************************Vue.prototype.$nextTick = function (fn: Function) {}Vue.prototype._render = function (): VNode {}Vue.prototype._s = _toStringVue.prototype._v = createTextVNodeVue.prototype._n = toNumberVue.prototype._e = createEmptyVNodeVue.prototype._q = looseEqualVue.prototype._i = looseIndexOfVue.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.configVue.util = utilVue.set = setVue.delete = delVue.nextTick = util.nextTickVue.options = { components: { KeepAlive }, directives: {}, filters: {}, _base: Vue}Vue.useVue.mixinVue.cid = 0Vue.extendVue.component = function(){}Vue.directive = function(){}Vue.filter = function(){}Vue.prototype.$isServerVue.version = '__VERSION__'
下一個就是 web-runtime.js 文件了, web-runtime.js 文件主要作了三件事兒:
    
    
    
    
1、覆蓋 Vue.config 的屬性,將其設置爲平臺特有的一些方法2Vue.options.directives Vue.options.components 安裝平臺特有的指令和組件3、在 Vue.prototype 上定義 __patch__ $mount
通過 web-runtime.js 文件以後,Vue 變成下面這個樣子:
    
    
    
    
// 安裝平臺特定的utilsVue.config.isUnknownElement = isUnknownElementVue.config.isReservedTag = isReservedTagVue.config.getTagNamespace = getTagNamespaceVue.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 了,該文件作了兩件事:
一、緩存來自 web-runtime.js 文件的 $mount 函數
    
    
    
    
const mount = Vue.prototype.$mount
二、在 Vue 上掛載 compile
    
    
    
    
Vue.compile = compileToFunctions
上面 compileToFunctions 函數能夠將模板 template 編譯爲render函數。
至此,咱們算是還原了 Vue 構造函數,總結一下:
    
    
    
    
1Vue.prototype 下的屬性和方法的掛載主要是在 src/core/instance 目錄中的代碼處理的2Vue 下的靜態屬性和方法的掛載主要是在 src/core/global-api 目錄下的代碼處理的3web-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。
這個屬性起到兩個做用:
 一、  目標屬性是否可使用delete刪除
 二、目標屬性是否能夠再次設置特性
    
    
    
    
//-----------------測試目標屬性是否能被刪除------------------------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() } }}













相關文章
相關標籤/搜索