這是一個Vue源碼學習系列。打算開個手寫Vue的坑,但願能在寫代碼的同時能把其中的細節講清楚,最終目的是實現一個簡版的vue。不知道本身能寫到哪一步,總之盡力而爲。若是能完成的話,應該是自我超越了和無限的自信了。放個倉庫的 傳送門。javascript
場景:最近由於快到暑假了,產品韓梅梅提早一個月在大象拉了一個需求羣,把研發李雷、小明拉進了羣。她跟兩位研發說,「咱們過幾天要開發一個新需求,須要兩位研發的支持,是關於暑假歡樂谷門票活動的需求,等產品邏輯梳理完,我們就進入開發。」。一個禮拜以後,韓梅梅在羣裏發出了需求文檔,兩位研發開始加班加點幹活,需求完美上線。html
頗費特~ 好的,觀察者模式講完了。vue
納尼?等等...等等...,您這是講了個啥。java
咳咳...很差意思,從新來。react
咱們看一下上面這個場景,它分了幾步git
總結下來,咱們發現有兩個角色,一個發佈者(產品韓梅梅),一個觀察者(李雷等研發),當發佈者的狀態更新後,會進行通知觀察者,觀察者開始執行對應的動做。github
ok,讓咱們試着寫一下面試
class Dep {
constructor(state) {
this.watchers = []
this.state = state
}
// 添加觀察者(研發)
add(watcher) {
!this.watchers.includes(watcher) && this.watchers.push(watcher)
}
// 移除觀察者(研發)
remove(watcher) {
let index = this.watcher.indexOf(watcher)
if (~index) this.watcher.splice(index, 1)
}
// 狀態更新, 通知所有觀察者
notify() {
for (let watcher of this.watchers) watcher.update(this)
}
}
class Watcher {
constructor(value) {
this.value = value
}
// 更新
update() {
console.log('開始開發!')
}
}
const HanMeiMei = new Dep()
const XiaoMing = new Watcher()
const LiLei = new Watcher()
// 拉羣!
HanMeiMei.add(XiaoMing)
HanMeiMei.add(LiLei)
await new Promise(resolve => setTimeout(resolve, 7 * 24 * 60 * 60 * 1000, '一週過去了')))
// 過了一週開始通知研發開發
HanMeiMei.notify()
複製代碼
問:那若是換成Vue中的視圖與數據之間的關係呢?哪一個是個發佈者,哪一個是觀察者。segmentfault
答:顯而易見,數據是發佈者,視圖是觀察者。當數據改變時,會通知視圖,視圖從新進行渲染。數組
這裏有幾個問題
ok,帶着這些問題我們繼續往下看
首先,明確一點,Vue實例中的響應數據,幾乎所有都來源於data,就是那個Option API中的data。不論是props,computed這些都是基於data的。
其次,Vue中的Watcher分爲了三種,
因此,what的答案就有了 數據收集了這三種觀察者
說個面試的段子,面試官:vue怎麼收集依賴的?
這個其實老生常談,getter/setter的存儲器嘛
誒,那你知道Array是怎麼收集的嗎?
知道知道,不就是hack的一些原生方法嘛
哦,那爲何要hack呢,咋hack的呢,hack了哪些呢,不一樣的方法之間又都是怎麼處理的數據呢?
......
好了,回去等通知吧(一面掛)
這裏信息量太大!關於爲何要hack方法,尤大是給出了回答的,主要緣由是由於性能和使用方便之間的取捨,這篇文章有寫道:segmentfault.com/a/119000001…
可是你要問我爲啥數組附個值還能跟性能扯上關係,咱也不懂,咱也不敢問。
在vue實例化的時候,在beforeCreate和create之間,會有一個初始化數據的過程,這裏會將data、computed所有初始化好,經過getter,哪裏用到就在哪裏收集觀察者。
先從轉換數據開始,咱們來簡單實現一個,很簡單就是迭代加遞歸,兩個函數搞定。
// 咱們先來實現第一個函數observe
function observe(data) {
if (typeof data !== 'object') return
for (let key of Object.keys(data)) {
defineReactive(data, key)
}
return data
}
// 而後是defineReactive
function defineReactive(data, key) {
let val = data[key]
const dep = new Dep()
observe(val)
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
dep.depend()
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
observe(val)
dep.notify()
}
})
}
複製代碼
這裏的邏輯很簡單就是經過迭代+遞歸,將全部值都改成存取器。
這裏注意defineReactive方法,我並無直接把data[key] 的這個value經過參數傳進去,而是在函數內部取值,之因此爲何作,這裏先留一個懸念
。
這裏出現了一個class Dep,這裏其實Dep就是來收集Watcher的。
好的,咱們繼續來實現Dep
class Dep {
constructor() {
this.watchers = new Set()
}
depend() {
if (Dep.Target) this.watchers.add(Dep.Target)
}
notify() {
let watchers = this.watchers
for (let watcher of watchers) {
watcher.update()
}
}
}
複製代碼
這裏Dep的實現也很簡單,就是收集watchers
,使用Set
確保watcher
的惟一。
可是!這裏又雙叒出現了一個新的東西,Dep.Target
。這東西是個啥,其實看代碼也能差很少發現,Dep.Target
確定是個Watcher
實例。
誒~,這麼多Watcher實例它究竟是哪一個呢?
好問題!咱們先想一想一個場景,咱們有個數據好比是data
,咱們還有個渲染函數
,而後呢~這個渲染函數用到了這個data。
用到data了確定就會觸發data的getter,從而收集依賴,那咱們要收集的依賴確定就是這個渲染函數
了。
相應的Dep.Target的值也就是這個渲染函數
。
噠嘎!
你覺得這樣就結束了嗎,No,No,No,嘛噠噠!
要是渲染函數裏面還有個渲染函數咋整。
納尼!還有這種操做嗎!
有的,並且不少,當咱們組件裏面嵌入了組件的時候就會出現。
我去,那不是很常見嗎!那可怎麼辦。
別慌,咱們只要實現一個棧,有新的函數要執行,咱們就push進來,當函數執行結束,給他pop出去就行了。
ok,那咱們開始實現一下。
Dep.Target = null
const watcherStack = []
// 入棧
function pushTarget(watcher) {
Dep.Target = watcher
watcherStack.push(watcher)
}
// 出棧
function popTarget() {
watcherStack.pop()
Dep.Target = watcherStack[watcherStack.length - 1]
}
複製代碼
完美解決~ 那麼最後剩下的的就是watcher的實現了。
上面說過,watcher一共有三種,咱們先實現最簡單、最基礎的renderWatcher。
class Watcher {
constructor(getter) {
this.getter = getter
this.value = undefined
this.value = this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget()
}
update() {
this.value = this.get()
}
}
複製代碼
這裏的邏輯很簡單,參數getter
就是要執行的函數。對於RenderWatcher
來講getter
就是渲染函數。 好的!萬事具有,咱們來試着跑個例子。
<body>
<div id="app"></div>
<script src="./reactive/reactive.js"></script>
<script> const data = observe({ age: 12, name: 'Sunyanzhe' }) // 渲染函數 function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}歲` } // renderWatcher const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
複製代碼
來,咱們捋一下流程:
getter/setter
new Watcher
的時候,這時候Dep.Target
是renderWatcher
renderFunction
,讀取到name
和age
的屬性時,觸發getter
收集Dep.Target
,也就是renderWatcher
。此時name
和age
中的Dep
實例都存了renderWatcher
setter
,觸發age
的Dep
中保存的watcher
的update
方法。此時更新視圖。renderFunction
執行,讀取到age
時,值爲25
。別問爲啥兩秒,一我的就從12變成25了,經歷痛苦會讓人瞬間成長😂
總聽文章裏說,computed有什麼懶加載,緩存。那是個什麼玩意啊
好說,由於computed實際上是一個getter,是函數就要執行嘛。懶加載的意思就是它何時被用到了,它何時執行這個函數。
那緩存又是什麼呢?
也很簡單,就是computed中用到的值若是沒發生改變的話,它的getter函數不進行計算,而是直接用上一次得出的結果。
ok,先到這裏,咱們先捋一捋思路
首先,剛剛說到,computed能夠被緩存,當它用到的值沒有發生改變時,getter不須要執行。
也就是說computed自己也是要有Dep,用來收集數據。
其次,他是lazy的,因此即便數據發生了改變也不用當即執行函數,獲取結果。而是能夠等到,何時再次用到這個computed的值再去計算。好比在render函數中用到
最後,computed中的數據改變後不能只通知computed的值須要從新更新,還須要通知用到computed的地方也要進行一次更新
總結下來就是,若是一個render函數中有用到computed,那麼computed中的數據更新,不只要通知computed的值要改變,還要告訴render函數進行從新執行。而當render函數從新執行的時候,就會再次獲取computed。這時computed纔會執行他的getter函數
好了,思路捋清了,咱們實現一下。爲此咱們要修改一下以前的Dep和Watcher,而且咱們還要實現一個computed方法。
class Dep {
constructor() {
this.watchers = new Set()
}
// 這裏發生了變化
addSub(watcher) {
this.watchers.add(watcher)
}
// 這裏發生了變化
depend() {
if (Dep.Target) {
Dep.Target.addDep(this)
}
}
notify() {
let watchers = this.watchers
for (let watcher of watchers) {
watcher.update()
}
}
}
class Watcher {
constructor( getter, options ) {
this.getter = getter
this.deps = new Set()
this.value = undefined
this.lazy = undefined
this.dirty = undefined
if (options) {
this.lazy = !!options.lazy
}
this.dirty = this.lazy
this.value = this.lazy
? undefined
: this.get()
}
get() {
pushTarget(this)
let value = this.getter()
popTarget()
return value
}
addDep(dep) {
dep.addSub(this)
this.deps.add(dep)
}
depend() {
let deps = this.deps
for (let dep of deps) {
dep.depend()
}
}
evalute() {
this.value = this.get()
this.dirty = false
}
update() {
if (this.lazy) {
this.dirty = true
} else {
Promise.resolve().then(() => {
this.run()
})
}
}
run() {
this.value = this.get()
}
}
function computed(computedGetter) {
const options = { lazy: true }
const computedWathcer = new Watcher(computedGetter, options)
const result = {}
Object.defineProperty(result, 'value', {
get() {
if (computedWathcer.dirty) {
computedWathcer.evalute()
}
if (Dep.Target) {
computedWathcer.depend()
}
return computedWathcer.value
}
})
return result
}
複製代碼
看到這裏確定很暈,不要緊,我們再舉一個🌰,結合🌰來看懂這塊邏輯。你們目前只須要關注一點,就是update中咱們用了微任務。
ok,先看例子
<body>
<div id="app"></div>
<script src="./reactive/reactive.js"></script>
<script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}歲,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
複製代碼
咱們仍是捋一下執行順序
dirty
爲true
。這個很重要!棧
。此時的棧中有RenderWatcher。棧
中推入了ComputedWatcher。重點來了,RenderWatcher還沒執行完!因此目前棧中有兩個watcher。computedWatcher的get執行完,出棧,將獲得的值賦給value屬性。修改dirty屬性爲false這裏爲何使用了微任務,是由於執行順序的問題,咱們的computed的計算必需要在renderWatcher的更新以後,這樣才能收集到對應的依賴。在Vue源碼中,有一個執行更新的隊列,它會將全部的watcher進行排序,避免報錯。
其實,watch也很簡單,就是加了個callback。
watch比較迷惑的地方其實它的getter是什麼,在renderWatcher中,getter是render函數;在computedWatcher中,getter是getter函數;那麼watch是什麼呢。
其實很簡單就是個travers函數,想一想咱們是怎麼寫watch的
watch: {
prop1(val) {
console.log(val)
}
}
// 轉換爲
$watch(() => {vm._data.prop1}, console.log)
複製代碼
這裏面第一個函數是getter,用來收集依賴,第二個就是callback了
那deep呢? deep其實就是深度遍歷
廢話少說,直接開始實現!
其實很簡單,咱們只須要加個callback,找個地方調用一下就行了。
因此咱們就改一下constructor和run這兩個
class Watcher {
constructor( getter, options, cb ) {
//...
this.cb = cb
this.user = undefined
if (options) {
this.user = !!options.user
}
// ...
}
run() {
let newValue = this.get()
if (this.user && newValue !== this.value) {
// 調用回調
this.cb(newValue, this.value)
this.value = newValue
}
}
}
function watch(watcheGetter, callback) {
const options = { user: true }
new Watcher(watcheGetter, options, callback)
}
複製代碼
就是如此的簡單,比computed簡單多了~
最後看一下效果
<body>
<div id="app"></div>
<script src="./reactive/reactive.js"></script>
<script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}歲,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) watch( () => data.name, (val, oldVal) => { console.log('new---', val) console.log('old---', oldVal) }) setTimeout(() => { data.name = 'yanzhe' }, 1000) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
複製代碼
在上文中談到的爲何DefineReactive不傳value的緣由,在這個issue中:github.com/vuejs/vue/p…,主要緣由是,數據自己就能夠是getter/setter
Vue中的源碼思路與本文一致,主要多了邊界問題的處理,以及數組的hack,有關數組的處理須要你們去看源碼去理解