我們將實(shí)現(xiàn) MobX 的的主要功能:
- observable
- autoRun
- computed
至于 decorator(修飾器)且更多的是依賴于 ES 新的特性,在這里不過多分析
MobX 的特性
MobX: Simple, scalable state management
簡單,可擴(kuò)展的狀態(tài)管理工具
我們先看一下 MobX 的一些特性和使用
import { observable, autorun, computed } from 'mobx'
const todoStore = observable({
/* 一些觀察的狀態(tài) */
todos: [],
/* 推導(dǎo)值 */
get completedCount() {
return this.todos.filter(todo => todo.completed).length
}
})
/* 推導(dǎo)值 */
const finished = computed(() => {
return todoStore.todos.filter(todo => todo.completed).length
})
/* 觀察狀態(tài)改變的函數(shù) */
autorun(function() {
console.log('Completed %d of %d items', finished, todoStore.all)
})
/* ..以及一些改變狀態(tài)的動(dòng)作 */
todoStore.todos[0] = {
title: 'Take a walk',
completed: false
}
// -> 同步打印 'Completed 0 of 1 items'
todoStore.todos[0].completed = true
// -> 同步打印 'Completed 1 of 1 items'
我們分析一下 MobX 做了什么:
1.封裝 observable 對(duì)象:監(jiān)聽對(duì)象的屬性和值的變化,這個(gè)過程一般是通過
Object.defineProperty和 getter,setter 進(jìn)行攔截。或者Proxy進(jìn)行攔截。如果是學(xué)習(xí)過 vue,那 vue2.0 采用的就是前者,而最新的 vue3.0(vue-next)采用的后者 Proxy。2.依賴收集:使用 autoRun 進(jìn)行依賴收集,這是一個(gè)什么樣的過程呢?比如
a = {collect: 1, noCollect: 2},當(dāng)我對(duì) a 的 collect 進(jìn)行依賴收集autoRun(()=>console.log(a.collect)),當(dāng)a.collect++,就會(huì)立即輸出 2,但是當(dāng)我對(duì)a.noCollect++,由于 noCollect 未進(jìn)行依賴收集,因此不會(huì)執(zhí)行運(yùn)行輸出。3.自動(dòng)計(jì)算 computed:即自動(dòng)執(zhí)行代碼
const finished = computed(() => (todoStore.todos.filter(todo => todo.completed).length)),當(dāng) todos 發(fā)生變化的時(shí)候自動(dòng)更新finished這個(gè)變量的值。
原理探究
說了那么多,除了第一個(gè)可能有稍微聽過,其他的感覺是不是都挺陌生,其實(shí)原理相對(duì)簡單。整個(gè)大程序的實(shí)現(xiàn)可以分成三個(gè)大部分。
- 觀察者模式(dep)
- 攔截器(Proxy)
- 對(duì)象原始值(Symbol.toPrimitive)
什么是觀察者模式(EventBus)
一對(duì)多關(guān)系時(shí),使用觀察者模式(Observer Pattern)。只要當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新,解決了主體對(duì)象與觀察者之間功能的耦合,即一個(gè)對(duì)象狀態(tài)改變給其他對(duì)象通知的問題。

一個(gè)簡單的觀察者模式
const dep = {
event: {},
on(key, fn) {
this.event[key] = this.event[key] || []
this.event[key].push(fn)
},
emit(key, args) {
if (!this.event[key]) return
this.event[key].forEach(fn => fn(args))
}
}
dep.on('print', args => console.log(args))
dep.emit('print', 'hello world')
// output: hello world
仔細(xì)對(duì)比
// 觀察者模式
dep.on('print', args => console.log(args))
dep.emit('print', 'hello world')
// MobX
autorun(() => console.log(todoStore.todos.length'))
todoStore.todos[0] = {
title: 'Take a walk',
completed: false
}
是不是非常的相識(shí),只是一個(gè)顯式觸發(fā),一個(gè)隱式觸發(fā)。
那如何進(jìn)行隱式觸發(fā)?
1.攔截器(Proxy)
其實(shí)除了 Proxy 我們還有一種選擇Object.defineProperty,我們先看一下 Object.defineProperty 的實(shí)現(xiàn)方式。
const px = {}
let val = ''
Object.defineProperty(px, 'proxy', {
get() {
console.log('get', val)
// dep.on('proxy', fn)
return val
},
set(args) {
console.log('set', args)
// dep.emit('proxy')
val = args
}
})
px.proxy = 1
// output set 1
console.log(px.proxy)
// output get 1
// output 1
沒錯(cuò)注冊和觸發(fā)的方式通過,get set的方式進(jìn)行隱式的注冊和觸發(fā)。
但是Object.defineProperty存在著一些缺陷。
- 對(duì)數(shù)組支持不友好
- 封裝相對(duì)復(fù)雜
Proxy
我們將上面的代碼改寫成 Proxy 的方式,注冊和觸發(fā)的位置還是用于get set
const printFn = () => console.log('emit print key')
const handler = {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// dep.emit(key, target) 觸發(fā)事件
if (key === 'key') dep.emit('key')
return result
},
get(target, key, value, receiver) {
if (key === 'key') {
//注冊事件
dep.on(key, printFn)
}
return Reflect.get(target, key, value, receiver)
}
}
// 遞歸封裝Proxy
const observable = obj => {
Object.entries(obj).forEach(([key, value]) => {
if (typeof value !== 'object' || value === null) return
obj[key] = observable(value)
})
return new Proxy(obj, handler)
}
const obj = observable({})
obj.key // 運(yùn)行g(shù)et方法注冊 printFn
obj.key = 'print' // 運(yùn)行set觸發(fā)事件 執(zhí)行 printFn
// output 'emit print key'
這時(shí)候我們就完成了自動(dòng)響應(yīng)運(yùn)行。
這時(shí)候我們 autoRun 就該上場了。
2.依賴收集
會(huì)看上面的代碼,注冊的方法(printFn)是直接寫死的,但是實(shí)際場景,我們需要有一個(gè)注冊器,就像 autoRun。
const printFn = () => console.log('emit print key')
// 非常簡單
const autoRun = (key, fn) => {
dep.on(key, fn)
}
// 簡單修改一下我們的代理器
const handler = {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
dep.emit(key)
return result
},
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
}
}
// 遞歸封裝Proxy
const observable = obj => {
Object.entries(obj).forEach(([key, value]) => {
if (typeof value !== 'object' || value === null) return
obj[key] = observable(value)
})
return new Proxy(obj, handler)
}
const obj = observable({})
autoRun('key', printFn)
obj.key = 'print' // 運(yùn)行set觸發(fā)事件 autoRun 執(zhí)行 printFn
// output emit print key
這時(shí)候你可能就會(huì)問,這邊注冊的方式還是通過key來完成的啊,說好的依賴收集呢?說好的自動(dòng)注冊呢?
當(dāng)我們運(yùn)行一段代碼時(shí),我們是如何得知這段代碼里面用了什么變量?用了幾次變量?怎么將方法和和變量進(jìn)行關(guān)聯(lián)?
比如:想一想如何將ob.name和 autoRun 的方法進(jìn)行關(guān)聯(lián)
const ob = observable({})
autoRun(() => {
console.log(`print ${ob.name}`)
})
ob.name = 'hello world'
// print hello world
依賴收集原理: <strong> 通過全局變量和運(yùn)行 </strong>(敲黑板)
我們將上面的代碼改一改。
// 全局唯一的 id
let obId = 0
const dep = {
event: {},
on(key, fn) {
if (!this.event[key]) {
this.event[key] = new Set()
}
this.event[key].add(fn)
},
emit(key, args) {
const fns = new WeakSet()
const events = this.event[key]
if (!events) return
events.forEach(fn => {
if (fns.has(fn)) return
fns.add(fn)
fn(args)
})
}
}
// 全局變量
let pendingDerivation = null
// 依賴收集
const autoRun = fn => {
pendingDerivation = fn
fn()
pendingDerivation = null
}
const handler = {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
dep.emit(`${target.__obId}${key}`)
return result
},
get(target, key, value, receiver) {
if (target && key && pendingDerivation) {
dep.on(`${target.__obId}${key}`, pendingDerivation)
}
return Reflect.get(target, key, value, receiver)
}
}
const observable = obj => {
obj.__obId = `$$obj${++obId}__`
Object.entries(obj).forEach(([key, value]) => {
if (typeof value !== 'object' || value === null) return
obj[key] = observable(value)
})
return new Proxy(obj, handler)
}
縱觀上面的代碼,其實(shí)關(guān)鍵的修改大概就兩處:
// 全局變量
let pendingDerivation = null
// 收集依賴 step 1
const autoRun = fn => {
pendingDerivation = fn
fn()
pendingDerivation = null
}
// 收集依賴 step 2
const handler = {
get(target, key, value, receiver) {
if (target && key && pendingDerivation) {
dep.on(`${target.__obId}${key}`, pendingDerivation)
}
return Reflect.get(target, key, value, receiver)
}
}
原理:
<strong>就是通過全局變量和立即執(zhí)行一次,進(jìn)行變量的確認(rèn)和觀察者模式里的事件注冊</strong>
我們回顧一下 MobX 的描述:
當(dāng)使用 autorun 時(shí),所提供的函數(shù)總是立即被觸發(fā)一次,然后每次它的依賴關(guān)系改變時(shí)會(huì)再次被觸發(fā)。 --MobX
在執(zhí)行 autoRun 的 fn 的時(shí)候,就會(huì)觸發(fā)到 Proxy 里的各個(gè)屬性的 get 方法,這時(shí)候通過全局的變量將屬性和方法進(jìn)行映射。
computed:對(duì)象原始值(Symbol.toPrimitive)
其實(shí) MobX 關(guān)于 computed 的實(shí)現(xiàn)還是通過事件來觸發(fā)的,但是在閱讀源碼的時(shí)候,突發(fā)奇想,是不是也可以通過Symbol.toPrimitive來實(shí)現(xiàn)。
const computed = fn => {
return {
_computed: fn,
[Symbol.toPrimitive]() {
return this._computed()
}
}
}
代碼很簡單,通過 computed 封裝一個(gè)方法,然后直接返回一個(gè)對(duì)象,這個(gè)對(duì)象通過復(fù)寫Symbol.toPrimitive,實(shí)現(xiàn)方法的緩存,然后在 get 的時(shí)候進(jìn)行運(yùn)行。
完整代碼
代碼只是對(duì)主要邏輯進(jìn)行梳理,缺乏代碼細(xì)節(jié)
let obId = 0
let pendingDerivation = null
const dep = {
event: {},
on(key, fn) {
if (!this.event[key]) {
this.event[key] = new Set()
}
this.event[key].add(fn)
},
emit(key, args) {
const fns = new WeakSet()
const events = this.event[key]
if (!events) return
events.forEach(fn => {
if (fns.has(fn)) return
fns.add(fn)
fn(args)
})
}
}
const autoRun = fn => {
pendingDerivation = fn
fn()
pendingDerivation = null
}
const handler = {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
dep.emit(target.__obId + key)
return result
},
get(target, key, value, receiver) {
if (target && key && pendingDerivation) {
dep.on(target.__obId + key, pendingDerivation)
}
return Reflect.get(target, key, value, receiver)
}
}
const observable = obj => {
obj.__obId = `__obId${++obId}__`
Object.entries(obj).forEach(([key, value]) => {
if (typeof value !== 'object' || value === null) return
obj[key] = observable(value)
})
return new Proxy(obj, handler)
}
const computed = fn => {
return {
computed: fn,
[Symbol.toPrimitive]() {
return this.computed()
}
}
}
// demo
const todoObs = observable({
todo: [],
get all() {
return this.todo.length
}
})
const compuFinish = computed(() => {
return todoObs.todo.filter(t => t.finished).length
})
const print = () => {
const all = todoObs.all
console.log(`print: finish ${compuFinish}/${all}`)
}
autoRun(print)
todoObs.todo.push({
finished: false
})
todoObs.todo.push({
finished: true
})
// print: finish 0/0
// print: finish 0/1
// print: finish 1/2
以上代碼去除 demo,僅僅 60 行代碼。
在回顧一下流程圖。
