60 行代碼實(shí)現(xiàn)一個(gè)簡易MobX

我們將實(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ì)象通知的問題。

event-proxy.png

一個(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.nameautoRun 的方法進(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 行代碼。
在回顧一下流程圖。

processon.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Mobx 思想的實(shí)現(xiàn)原理 Mobx 最關(guān)鍵的函數(shù)在于 autoRun,舉個(gè)例子,它可以達(dá)到這樣的效果: 我們發(fā)現(xiàn)這...
    黃子毅閱讀 9,309評(píng)論 6 15
  • Mobx解決的問題 傳統(tǒng)React使用的數(shù)據(jù)管理庫為Redux。Redux要解決的問題是統(tǒng)一數(shù)據(jù)流,數(shù)據(jù)流完全可控...
    光哥很霸氣閱讀 13,205評(píng)論 2 21
  • MobX 簡單、可擴(kuò)展的狀態(tài)管理(可觀察的數(shù)據(jù)) 使用: 安裝: npm install mobx --save。...
    jevons_lee_閱讀 756評(píng)論 0 1
  • 任何一次技術(shù)革命,都伴隨著文明的沖突、社會(huì)的沖突。大家相不相信這個(gè),工業(yè)革命很好,蒸汽機(jī)火車來了,隨后的沖突就是第...
    Molly_zhang閱讀 151評(píng)論 0 0

友情鏈接更多精彩內(nèi)容