從這一小節開始,正式進入
Vue
源碼的核心,也是難點之一,響應式系統的構建。這一節將做爲分析響應式構建過程源碼的入門,主要分爲兩大塊,第一塊是針對響應式數據props,methods,data,computed,wather
初始化過程的分析,另外一塊則是在保留源碼設計理念的前提下,嘗試手動構建一個基礎的響應式系統。有了這兩個基礎內容的鋪墊,下一篇進行源碼具體細節的分析會更加駕輕就熟。vue
回顧一下以前的內容,咱們對Vue
源碼的分析是從初始化開始,初始化_init
會執行一系列的過程,這個過程包括了配置選項的合併,數據的監測代理,最後纔是實例的掛載。而在實例掛載前還有意忽略了一個重要的過程,數據的初始化(即initState(vm)
)。initState
的過程,是對數據進行響應式設計的過程,過程會針對props,methods,data,computed
和watch
作數據的初始化處理,並將他們轉換爲響應式對象,接下來咱們會逐步分析每個過程。node
function initState (vm) { vm._watchers = []; var opts = vm.$options; // 初始化props if (opts.props) { initProps(vm, opts.props); } // 初始化methods if (opts.methods) { initMethods(vm, opts.methods); } // 初始化data if (opts.data) { initData(vm); } else { // 若是沒有定義data,則建立一個空對象,並設置爲響應式 observe(vm._data = {}, true /* asRootData */); } // 初始化computed if (opts.computed) { initComputed(vm, opts.computed); } // 初始化watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } } 複製代碼
簡單回顧一下props
的用法,父組件經過屬性的形式將數據傳遞給子組件,子組件經過props
屬性接收父組件傳遞的值。算法
// 父組件 <child :test="test"></child> var vm = new Vue({ el: '#app', data() { return { test: 'child' } } }) // 子組件 Vue.component('child', { template: '<div>{{test}}</div>', props: ['test'] }) 複製代碼
所以分析props
須要分析父組件和子組件的兩個過程,咱們先看父組件對傳遞值的處理。按照以往文章介紹的那樣,父組件優先進行模板編譯獲得一個render
函數,在解析過程當中遇到子組件的屬性,:test=test
會被解析成{ attrs: {test: test}}
並做爲子組件的render
函數存在,以下所示:數組
with(){..._c('child',{attrs:{"test":test}})} 複製代碼
render
解析Vnode
的過程遇到child
這個子佔位符節點,所以會進入建立子組件Vnode
的過程,建立子Vnode
過程是調用createComponent
,這個階段咱們在組件章節有分析過,在組件的高級用法也有分析過,最終會調用new Vnode
去建立子Vnode
。而對於props
的處理,extractPropsFromVNodeData
會對attrs
屬性進行規範校驗後,最後會把校驗後的結果以propsData
屬性的形式傳入Vnode
構造器中。總結來講,props
傳遞給佔位符組件的寫法,會以propsData
的形式做爲子組件Vnode
的屬性存在。下面會分析具體的細節。瀏覽器
// 建立子組件過程 function createComponent() { // props校驗 var propsData = extractPropsFromVNodeData(data, Ctor, tag); ··· // 建立子組件vnode var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), data, undefined, undefined, undefined, context, { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory ); } 複製代碼
先看檢測props
規範性的過程。**props
編譯後的結果有兩種,其中attrs
前面分析過,是編譯生成render
函數針對屬性的處理,而props
是針對用戶自寫render
函數的屬性值。**所以須要同時對這兩種方式進行校驗。markdown
function extractPropsFromVNodeData (data,Ctor,tag) { // Ctor爲子類構造器 ··· var res = {}; // 子組件props選項 var propOptions = Ctor.options.props; // data.attrs針對編譯生成的render函數,data.props針對用戶自定義的render函數 var attrs = data.attrs; var props = data.props; if (isDef(attrs) || isDef(props)) { for (var key in propOptions) { // aB 形式轉成 a-b var altKey = hyphenate(key); { var keyInLowerCase = key.toLowerCase(); if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { // 警告 } } } } } 複製代碼
重點說一下源碼在這一部分的處理,HTML對大小寫是不敏感的,全部的瀏覽器會把大寫字符解釋爲小寫字符,所以咱們在使用DOM
中的模板時,cameCase(駝峯命名法)的props
名須要使用其等價的 kebab-case
(短橫線分隔命名) 命代替。 即: <child :aB="test"></child>
須要寫成<child :a-b="test"></child>
app
剛纔說到分析props
須要兩個過程,前面已經針對父組件對props
的處理作了描述,而對於子組件而言,咱們是經過props
選項去接收父組件傳遞的值。咱們再看看子組件對props
的處理:框架
子組件處理props
的過程,是發生在父組件_update
階段,這個階段是Vnode
生成真實節點的過程,期間會遇到子Vnode
,這時會調用createComponent
去實例化子組件。而實例化子組件的過程又回到了_init
初始化,此時又會經歷選項的合併,針對props
選項,最終會統一成{props: { test: { type: null }}}
的寫法。接着會調用initProps
, initProps
作的事情,簡單歸納一句話就是,將組件的props
數據設置爲響應式數據。async
function initProps (vm, propsOptions) { var propsData = vm.$options.propsData || {}; var loop = function(key) { ··· defineReactive(props,key,value,cb); if (!(key in vm)) { proxy(vm, "_props", key); } } // 遍歷props,執行loop設置爲響應式數據。 for (var key in propsOptions) loop( key ); } 複製代碼
其中proxy(vm, "_props", key);
爲props
作了一層代理,用戶經過vm.XXX
能夠代理訪問到vm._props
上的值。針對defineReactive
,本質上是利用Object.defineProperty
對數據的getter,setter
方法進行重寫,具體的原理能夠參考數據代理章節的內容,在這小節後半段也會有一個基本的實現。函數
initMethod
方法和這一節介紹的響應式沒有任何的關係,他的實現也相對簡單,主要是保證methods
方法定義必須是函數,且命名不能和props
重複,最終會將定義的方法都掛載到根實例上。
function initMethods (vm, methods) { var props = vm.$options.props; for (var key in methods) { { // method必須爲函數形式 if (typeof methods[key] !== 'function') { warn( "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " + "Did you reference the function correctly?", vm ); } // methods方法名不能和props重複 if (props && hasOwn(props, key)) { warn( ("Method \"" + key + "\" has already been defined as a prop."), vm ); } // 不能以_ or $.這些Vue保留標誌開頭 if ((key in vm) && isReserved(key)) { warn( "Method \"" + key + "\" conflicts with an existing Vue instance method. " + "Avoid defining component methods that start with _ or $." ); } } // 直接掛載到實例的屬性上,能夠經過vm[method]訪問。 vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm); } } 複製代碼
data
在初始化選項合併時會生成一個函數,只有在執行函數時纔會返回真正的數據,因此initData
方法會先執行拿到組件的data
數據,而且會對對象每一個屬性的命名進行校驗,保證不能和props,methods
重複。最後的核心方法是observe
,observe
方法是將數據對象標記爲響應式對象,並對對象的每一個屬性進行響應式處理。與此同時,和props
的代理處理方式同樣,proxy
會對data
作一層代理,直接經過vm.XXX
能夠代理訪問到vm._data
上掛載的對象屬性。
function initData(vm) { var data = vm.$options.data; // 根實例時,data是一個對象,子組件的data是一個函數,其中getData會調用函數返回data對象 data = vm._data = typeof data === 'function'? getData(data, vm): data || {}; 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 (methods && hasOwn(methods, key)) { warn(("Method \"" + key + "\" has already been defined as a data property."),vm); } } // 命名不能和props重複 if (props && hasOwn(props, key)) { warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm); } else if (!isReserved(key)) { // 數據代理,用戶可直接經過vm實例返回data數據 proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); } 複製代碼
最後講講observe
,observe
具體的行爲是將數據對象添加一個不可枚舉的屬性__ob__
,標誌對象是一個響應式對象,而且拿到每一個對象的屬性值,重寫getter,setter
方法,使得每一個屬性值都是響應式數據。詳細的代碼咱們後面分析。
和上面的分析方法同樣,initComputed
是computed
數據的初始化,不一樣之處在於如下幾點:
computed
能夠是對象,也能夠是函數,可是對象必須有getter
方法,所以若是computed
中的屬性值是對象時須要進行驗證。computed
的每一個屬性,要建立一個監聽的依賴,也就是實例化一個watcher
,watcher
的定義,能夠暫時理解爲數據使用的依賴自己,一個watcher
實例表明多了一個須要被監聽的數據依賴。除了不一樣點,initComputed
也會將每一個屬性設置成響應式的數據,一樣的,也會對computed
的命名作檢測,防止與props,data
衝突。
function initComputed (vm, computed) { ··· for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; // computed屬性爲對象時,要保證有getter方法 if (getter == null) { warn(("Getter is missing for computed property \"" + key + "\"."),vm); } if (!isSSR) { // 建立computed watcher watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions); } if (!(key in vm)) { // 設置爲響應式數據 defineComputed(vm, key, userDef); } else { // 不能和props,data命名衝突 if (key in vm.$data) { warn(("The computed property \"" + key + "\" is already defined in data."), vm); } else if (vm.$options.props && key in vm.$options.props) { warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); } } } } 複製代碼
顯然Vue
提供了不少種數據供開發者使用,可是分析完後發現每一個處理的核心都是將數據轉化成響應式數據,有了響應式數據,如何構建一個響應式系統呢?前面提到的watcher
又是什麼東西?構建響應式系統還須要其餘的東西嗎?接下來咱們嘗試着去實現一個極簡風的響應式系統。
Vue
的響應式系統構建是比較複雜的,直接進入源碼分析構建的每個流程會讓理解變得困難,所以我以爲在儘量保留源碼的設計邏輯下,用最小的代碼構建一個最基礎的響應式系統是有必要的。對Dep,Watcher,Observer
概念的初步認識,也有助於下一篇對響應式系統設計細節的分析。
咱們以MyVue
做爲類響應式框架,框架的搭建不作贅述。咱們模擬Vue
源碼的實現思路,實例化MyVue
時會傳遞一個選項配置,精簡的代碼只有一個id
掛載元素和一個數據對象data
。模擬源碼的思路,咱們在實例化時會先進行數據的初始化,這一步就是響應式的構建,咱們稍後分析。數據初始化後開始進行真實DOM
的掛載。
var vm = new MyVue({ id: '#app', data: { test: 12 } }) // myVue.js (function(global) { class MyVue { constructor(options) { this.options = options; // 數據的初始化 this.initData(options); let el = this.options.id; // 實例的掛載 this.$mount(el); } initData(options) { } $mount(el) { } } }(window)) 複製代碼
首先引入一個類Observer
,這個類的目的是將數據變成響應式對象,利用Object.defineProperty
對數據的getter,setter
方法進行改寫。在數據讀取getter
階段咱們會進行依賴的收集,在數據的修改setter
階段,咱們會進行依賴的更新(這兩個概念的介紹放在後面)。所以在數據初始化階段,咱們會利用Observer
這個類將數據對象修改成相應式對象,而這是全部流程的基礎。
class MyVue { initData(options) { if(!options.data) return; this.data = options.data; // 將數據重置getter,setter方法 new Observer(options.data); } } // Observer類的定義 class Observer { constructor(data) { // 實例化時執行walk方法對每一個數據屬性重寫getter,setter方法 this.walk(data) } walk(obj) { const keys = Object.keys(obj); for(let i = 0;i< keys.length; i++) { // Object.defineProperty的處理邏輯 defineReactive(obj, keys[i]) } } } 複製代碼
咱們能夠這樣理解,一個Watcher
實例就是一個依賴,數據不論是在渲染模板時使用仍是在用戶計算時使用,均可以算作一個須要監聽的依賴,watcher
中記錄着這個依賴監聽的狀態,以及如何更新操做的方法。
// 監聽的依賴 class Watcher { constructor(expOrFn, isRenderWatcher) { this.getter = expOrFn; // Watcher.prototype.get的調用會進行狀態的更新。 this.get(); } get() {} } 複製代碼
那麼哪一個時間點會實例化watcher
並更新數據狀態呢?顯然在渲染數據到真實DOM
時能夠建立watcher
。$mount
流程前面章節介紹過,會經歷模板生成render
函數和render
函數渲染真實DOM
的過程。咱們對代碼作了精簡,updateView
濃縮了這一過程。
class MyVue { $mount(el) { // 直接改寫innerHTML const updateView = _ => { let innerHtml = document.querySelector(el).innerHTML; let key = innerHtml.match(/{(\w+)}/)[1]; document.querySelector(el).innerHTML = this.options.data[key] } // 建立一個渲染的依賴。 new Watcher(updateView, true) } } 複製代碼
watcher
若是理解爲每一個數據須要監聽的依賴,那麼Dep
能夠理解爲對依賴的一種管理。數據能夠在渲染中使用,也能夠在計算屬性中使用。相應的每一個數據對應的watcher
也有不少。而咱們在更新數據時,如何通知到數據相關的每個依賴,這就須要Dep
進行通知管理了。而且瀏覽器同一時間只能更新一個watcher
,因此也須要一個屬性去記錄當前更新的watcher
。而Dep
這個類只須要作兩件事情,將依賴進行收集,派發依賴進行更新。
let uid = 0; class Dep { constructor() { this.id = uid++; this.subs = [] } // 依賴收集 depend() { if(Dep.target) { // Dep.target是當前的watcher,將當前的依賴推到subs中 this.subs.push(Dep.target) } } // 派發更新 notify() { const subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { // 遍歷dep中的依賴,對每一個依賴執行更新操做 subs[i].update(); } } } Dep.target = null; 複製代碼
咱們看看數據攔截的過程。前面的Observer
實例化最終會調用defineReactive
重寫getter,setter
方法。這個方法開始會實例化一個Dep
,也就是建立一個數據的依賴管理。在重寫的getter
方法中會進行依賴的收集,也就是調用dep.depend
的方法。在setter
階段,比較兩個數不一樣後,會調用依賴的派發更新。即dep.notify
const defineReactive = (obj, key) => { const dep = new Dep(); const property = Object.getOwnPropertyDescriptor(obj); let val = obj[key] if(property && property.configurable === false) return; Object.defineProperty(obj, key, { configurable: true, enumerable: true, get() { // 作依賴的收集 if(Dep.target) { dep.depend() } return val }, set(nval) { if(nval === val) return // 派發更新 val = nval dep.notify(); } }) } 複製代碼
回過頭來看watcher
,實例化watcher
時會將Dep.target
設置爲當前的watcher
,執行完狀態更新函數以後,再將Dep.target
置空。這樣在收集依賴時只要將Dep.target
當前的watcher push
到Dep
的subs
數組便可。而在派發更新階段也只須要從新更新狀態便可。
class Watcher { constructor(expOrFn, isRenderWatcher) { this.getter = expOrFn; // Watcher.prototype.get的調用會進行狀態的更新。 this.get(); } get() { // 當前執行的watcher Dep.target = this this.getter() Dep.target = null; } update() { this.get() } } 複製代碼
一個極簡的響應式系統搭建完成。在精簡代碼的同時,保持了源碼設計的思想和邏輯。有了這一步的基礎,接下來深刻分析源碼中每一個環節的實現細節會更加簡單。
這一節內容,咱們正式進入響應式系統的介紹,前面在數據代理章節,咱們學過Object.defineProperty
,這是一個用來進行數據攔截的方法,而響應式系統構建的基礎就是數據的攔截。咱們先介紹了Vue
內部在初始化數據的過程,最終得出的結論是,不論是data,computed
,仍是其餘的用戶定義數據,最終都是調用Object.defineProperty
進行數據攔截。而文章的最後,咱們在保留源碼設計思想和邏輯的前提下,構建出了一個簡化版的響應式系統。完整的功能有助於咱們下一節對源碼具體實現細節的分析和思考。