一.什么是響應式的?
響應式就是當對象本身(對象的增刪值)或者對象屬性(重新賦值)發生了改變的時候,就會運行一些函數,最常見的示render函數。
在具體的實現上,vue用了幾個核心的部件,每一個部件都解決一個問題:
- Observer
- Dep
- Watcher
- Scheduler
Observer
Observer 要實現的目標非常簡單,就是把一個普通的對象 轉換為響應式的對象。
為了實現這一點,Observer 把對象的每個屬性通過 Object.defineProperty 將對象的每一個屬性轉換為帶有 getter 和 setter 的屬性,這樣一來,我們訪問和設置屬性的時候,會分別調用 getter 和 setter 方法,Vue 就有機會做一些別的事情。
Observer 是 vue 內部的構造器,我們可以通過Vue 提供的靜態方法 Vue.observable( obj ) 間接的使用該功能。
<body>
<script src="./vue.min.js"></script>
<script> let obj = { name:"法醫", age:100, like:{ a:"琴", b:"棋" }, character:[{ c:"性格好" },{ d:"真帥哦" }] } Vue.observable(obj); </script>
</body>
數據中的(...)表示數據是響應式的,如果對象中還包含有對象,它就會深度遍歷這個對象,讓所有的 數據都是響應式的。
Observer 在具體的實現上,它會遍歷對象的所有屬性,已完成數據響應式的轉化,但是如果一個屬性一開始就不存在於對象中,是后面添加的,這種屬性是檢測不到的,但是在 Vue3 里面是使用 proxy 來實現數據的響應式的,就可以檢測到了。為了解決Vue2 中給新添加的數據實現響應式的能力,Vue2 提供了兩個方法 $set 和 $ delete 這兩個實例方法,我們可以通過這兩個實例方法對響應式的對象添加或者刪除屬性。
對於數組,vue會更改它的隱式原型,之所以這樣做,是因為vue需要監聽那些可能改變數組內容的 方法。
總之,Observer 的目標,就是要讓一個對象,它的屬性的讀取、賦值,內部數組的變化都要能夠被vue檢測到,這樣才能讓數據轉換為響應式數據。
Dep
現在就是有兩個問題沒有解決,就是讀取屬性的時候需要做什么?屬性變化的時候做什么,這個問題就需要 Dep 來解決。
Dep 的全稱是 Dependency,表示是 依賴 的意思。
Vue 會給響應式對象中的每一個屬性,對象本身,數組本身 創建一個Dep 實例,每個 Dep 實例都有能力做以下 兩件事:
* 記錄依賴: 是誰在用我
* 派發依賴: 我變了,我要通知那些用到我的人
當讀取響應式對象的某個屬性的時候,它會進行依賴收集:誰用到了我。
當改變響應式對象的某個屬性的時候,它就回發出通知:用我的人聽好了,我被改變了,
watcher
現在又有了一個問題:就是Dep 是如何知道是誰在用我?
要解決這個問題,就需要依賴另一個東西,就是 Watcher
當某個函數執行的時候,用到了響應式數據,響應式數據是無法知道哪個函數在用自己,因此,Vue 通過一種巧妙的方法倆解決這個問題:
我們不要直接的去執行這個函數,而是把函數交給一個叫做 watcher 的東西去執行,watcher 是一個對象,每個這樣的函數執行時候,都應該創建一個 watcher ,通過 watcher 去執行。watcher 會創建一個全局變量,這個全局變量記錄當前的 watcher ,然后再去執行函數,在函數執行的過程中,如果發生了依賴收集 dep.depend() ,那么 Dep 就會將這個全局變量記錄下來,表示:有一個 watcher 用到了我這個屬性。
當Dep 進行派發更新的時候,就會通知之前記錄的所有 watcher :我變了。
每一個Vue組件實例,都會對應一個 watcher ,該 watcher 中記錄這個組件的 render 函數。
watcher 首先會把 render 函數運行一次以收集依賴,於是那些在 render 中用到的響應式數據就會記錄給這個 watcher .
當數據發生了變化的時候,dep 就會通知該 watcher ,而 watcher 將會重新運行 render 函數,從而讓界面重新渲染,同時重新記錄當前的依賴。
Scheduler
現在就是還剩下最后一個問題,就是Dep 通知 了 watcher 之后,響應數據又多次發生了改變,造成了 watcher 執行重復運行對應的函數,就有可能導致函數頻繁的運行,從而導致效率低下。
試想,如果一個交給watcher的函數,它里面用到了屬性a、b、c、d,那么a、b、c、d屬性都會記錄依賴,於是下面的代碼將會觸發4次更新:
state.a = "new data"; state.b = "new data"; state.c = "new data"; state.d = "new data";
這樣肯定是不行的,因此,watcher收到派發更新的通知后,它不會立即執行對應render函數,當然不僅僅是render函數,還有可能是其它的函數,而是把自己交給一個叫調度器
的東西,在調度器里面有個隊列,可以認為是一個數組,這個隊列數組中記錄了當前要運行哪些watcher,調度器維護一個執行隊列,在隊列中同一個watcher只會存在一次,隊列中的watcher不是立即執行,它會通過一個叫做nextTick
的工具方法,把這些需要執行的watcher放入到事件循環的微隊列
中,nextTick的具體做法是通過Promise
完成的,nextTick其實就是一個函數。
nextTick((fn)=>{ Promise.resolve().then(fn);//通過這種方式就跑到微隊列中去了
})
也就是說,當響應式數據變化時,render函數的執行是異步的,並且在微隊列中。
總體流程:
簡單的描敘這個流程就是:
- 原始的對象通過 Observer 轉換為 響應式的數據,具有 getter 和 setter 方法,然后就靜靜等待着。
- 突然有一天,雷雨交加,有一個render函數要執行,但不是直接就執行了,而是交給watcher來執行,watcher通過設置全局變量的方式讀取數據,因為讀取了數據,所以會觸發響應式對象的getter,隨后getter會從全局變量的位置讀取到當前正在讀取的watcher並把watcher收集到Dep中。
- 通過以上步驟頁面就會被渲染出來了。
- 又是突然的一天哈,風和日麗,我觸發了一個按鈕或者事件,不管干了什么,反正是數據改變了,進行新的步驟——
派發更新
,隨后通知watcher,我變了哦,你給我馬上搞定這件事情,但是watcher並不是立即就執行的,因為數據變動有時候不是一個,而是很多,立即執行的話會重復執行很多render函數或者其它數據變動的函數,執行效率會變低。然而watcher把自己交給調度器Scheduler
- 調度器會把watcher添加到隊列中,當然在隊列中也不會執行的,而是將隊列交給
nextTick
隊列,nextTick里面的函數全是在微隊列的,等同步代碼執行完成后,會異步地執行函數fn1、fn2、watcher等等,這一步相當於重新執行了watcher,然后又重新執行了render函數,就這樣地循環往復。
轉自:https://mp.weixin.qq.com/s/6jJFe5172GdqNkKyv_awoQ