Publish subscribe mode vs observer mode

background

Recently, when studying the source code of zustand, the state manager of react, it was found that its component registration binding was updated through observer mode combined with react hooks. When Lenovo wrote vue before, it often used the built-in user-defined events of vue for component communication ($emit/on). This should be the publish and subscribe mode, which made me nod. I felt that the two modes were very similar, and I was a little confused and didn't understand them thoroughly. Therefore, this time, I took the opportunity to study these two modes in depth, Try writing by yourself to deepen your understanding. This article is my personal experience. If there are mistakes, please correct them and make common progress~

contrast

difference

Observer mode: in software design, it is an object that maintains a dependency list and automatically notifies them when any state changes.

Publish subscribe design pattern: the sender (publisher) of a message will not directly send it to a specific receiver (called a subscriber), but will filter and allocate messages through an information intermediary.

The popular image is:

  • In the observer mode, no middlemen earn the price difference, while in the publish and subscribe mode, middlemen earn the price difference.
  • The observer mode is a one size fits all mode, which treats all subscribers equally. The publish and subscribe mode can wear colored glasses and have a layer of filtering or black box operation.

Post a picture and let's feel it

[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-qqz66s20-1631541451800)( https://user-gold-cdn.xitu.io/2017/11/22/15fe1b1f174cd376?imageView2/0/w/1280/h/960/format/webp/ignore -error/1)]

To sum up

  • In the observer mode, the observer knows the Subject, and the Subject keeps a record of the observer. However, in the publish subscribe mode, the publisher and subscriber do not know the existence of each other. They communicate only through the message broker.

  • In publish subscribe mode, components are loosely coupled, as opposed to observer mode.

  • The observer mode is synchronized most of the time. For example, when an event is triggered, the Subject will call the observer's method. The publish subscribe mode is asynchronous most of the time (using message queuing).

  • The observer pattern needs to be implemented in a single application address space, while publish subscribe is more like a cross application pattern.

The concepts seem quite clear, and the differences between them are easy to understand. Next, we begin to implement it ourselves and go deep into its internal principle and operation logic.

Publish subscribe mode

vue custom event Event Bus is the implementation of publish subscribe mode and the Emitter Event of Nodejs.

Implement a publication subscription that supports subscription, unbinding, publishing, and multiple binding of events of the same type.

Let's have a simple implementation

Upper code

// Subscription Center 
const subscribers = {}
// subscribe
const subscribe = (type, fn) => {
  // Add a queue in array mode to support multiple bindings of the same type
  if (!subscribers[type]) subscribers[type] = []
  subscribers[type].push(fn)
}
// release
const publish = (type, ...args) => {
  if (!subscribers[type] || !subscribers[type].length) return
  subscribers[type].forEach((fn) => fn(...args))
}
// Unbind subscription
const unsubscribe = (type, fn) => {
  if (!subscribers[type] || !subscribers[type].length) return
  subscribers[type] = subscribers[type].filter((n) => n !== fn)
}

Verification test

// console test ======>
subscribe("topic-1", () => console.log("suber-A Subscribed topic-1"))
subscribe("topic-2", () => console.log("suber-B Subscribed topic-2"))
subscribe("topic-1", () => console.log("suber-C Subscribed topic-1"))

publish("topic-1") // Notify A and C who have subscribed to topic-1

// Output results
// suber-A subscribes to topic-1
// suber-C subscribed to topic-1

Implement an Emitter class

Upper code

class Emitter {
  constructor() {
    // Subscription Center 
    this._event = this._event || {}
  }
  // Register Subscriber 
  addEventListener(type, fn) {
    const handler = this._event[type]

    if (!handler) {
      this._event[type] = [fn]
    } else {
      handler.push(fn)
    }
  }
  // Uninstall subscription
  removeEventListener(type, fn) {
    const handler = this._event[type]

    if (handler && handler.length) {
      this._event[type] = handler.filter((n) => n !== fn)
    }
  }
  // notice
  emit(type, ...args) {
    const handler = this._event[type]

    if (handler && handler.length) {
      handler.forEach((fn) => fn.apply(this, args))
    }
  }
}

Verification test

// console test ======>
const emitter = new Emitter()

emitter.addEventListener("change", (obj) => console.log(`name is ${obj.name}`))

emitter.addEventListener("change", (obj) => console.log(`age is ${obj.age}`))

const sex = (obj) => console.log(`sex is ${obj.sex}`)

emitter.addEventListener("change", sex)

emitter.emit("change", { name: "xiaoming", age: 28, sex: "male" })

console.log("event-A", emitter._event)

emitter.removeEventListener("change", sex)

console.log("====>>>>")

emitter.emit("change", { name: "xiaoming", age: 28, sex: "male" })

console.log("event-B", emitter._event)

// output
// name is xiaoming
// age is 28
// sex is male
// event-A {change: Array(3)}

// ====>>>>

// name is xiaoming
// age is 28
// event-B {change: Array(2)}

vue Event Bus implementation

Structure combing

Source location: src/core/instance/events.js

First, we analyze the structure according to the source code and sort out the event implementation logic of vue

  1. Put the event center_ Mount events to Vue instance:

    vm._events = {}

  2. Mount all methods: $on, $once, $off, $emit to the Vue prototype

The advantage of this is that this.$on and this.$emit can be used directly in Vue components

// $on
Vue.prototype.$on = function(){}
// $once
Vue.prototype.$once = function(){}
// $once
Vue.prototype.$off = function(){}
// $once
Vue.prototype.$emit = function(){}

Look at the code

  1. $on add registration

    // $on
    Vue.prototype.$on = function (event, fn) {
      const vm = this
    
      // If the type of the event listening event passed in is array, call the $on method recursively
      if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$on(event[i], fn)
        }
      } else {
    
        // If there is a direct add, there is no add after new
        ;(vm._events[event] || (vm._events[event] = [])).push(fn)
      }
    
      // Returns this for chained calls
      return vm
    }
    
  2. $once single execution

    // $once
    Vue.prototype.$once = function (event, fn) {
      const vm = this
    
      // When the event event is triggered, the on method is called
      function on() {
    
        // First, execute the $off method to unload the callback method
        vm.$off(event, on)
    
        // Then execute this callback method
        fn.apply(vm, arguments)
      }
    
      // This assignment will be used in $off: cb.fn === fn
      // Because the $once method calls the $on callback, but the wrapped on method is added instead of the fn method
      // Therefore, when we call the $off method alone to delete the fn callback, we cannot find it. In this case, we can judge by cb.fn === fn
      on.fn = fn
    
      // Call the $on method to add the callback to the queue
      vm.$on(event, on)
    
      return vm
    }
    
  3. $off uninstall delete

    // $off
    Vue.prototype.$off = function (event, fn) {
      const vm = this
    
      // If no parameters are passed in, all events will be cleared
      if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
      }
    
      // If the event is an array, it is the same as the $on logic, and the event is unloaded recursively
      if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$off(event[i], fn)
        }
        return vm
      }
    
      // callback list 
      const cbs = vm._events[event]
    
      // If there is no binding callback for this event, it will not be processed
      if (!cbs) {
        return vm
      }
    
      // If the unbinding callback of the corresponding event is not passed in, all the events of the event will be cleared
      if (!fn) {
        vm._events[event] = null
        return vm
      }
    
      // Event event type and callback exist. Traverse to find and delete the specified callback
      let cb
      let i = cbs.length
      while (i--) {
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1)
          break
        }
      }
      return vm
    }
    
  4. $emit trigger event

    // $emit
    Vue.prototype.$emit = function (event) {
      const vm = this
    
      // callback list 
      let cbs = vm._events[event]
    
      // Judge whether there is an execution callback for the event
      if (cbs) {
    
        // The $emit method can pass parameters, which will be passed in when calling the callback function
        // Exclude other parameters of the event parameter
        // toArray is a method that converts a class array into an array and supports interception
        const args = toArray(arguments, 1)
    
        // Traversal callback function
        for (let i = 0, l = cbs.length; i < l; i++) {
          cbs[i].apply(vm, args)
        }
      }
      return vm
    }
    
    toArray method
    // Convert an Array-like object to a real Array.
    function toArray (list, start) {
      start = start || 0;
      var i = list.length - start;
      var ret = new Array(i);
      while (i--) {
        ret[i] = list[i + start];
      }
      return ret
    }
    

Under test

Let's simulate a Vue class to test

class Vue {
  constructor() {
    this._events = {}
  }

  // Provide an external access_ events interface
  get event() {
    return this._events
  }
}

Verify the results

// instantiation 
const myVue = new Vue()

// Add subscription
const update_user = (args) => console.log("user: ", args)
const once_update_user = (args) => console.log("once_user: ", args)

myVue.$on("user", update_user)
myVue.$once("user", once_update_user) // The subscription is automatically uninstalled when triggered

// Output printing
console.log("events: ", myVue.event)
// events:  {user: [(args) => console.log("user: ", args), ƒ on()]}

// Trigger notification
myVue.$emit("user", { name: "xiaoming", age: 18 })
console.log("events: ", myVue.event)
// events:  {user: [(args) => console.log("user: ", args)]}
// user:  {name: "xiaoming", age: 18}
// once_user:  {name: "xiaoming", age: 18}

// Uninstall subscription
myVue.$off("user", once_update_user)
console.log("events: ", myVue.event)
// events: {user: []}

Little summary

The publish subscribe mode encapsulated by Vue can be said to be very perfect. It is a code that can be extracted independently and used in other projects, and then adjust the location of the event memory according to its own needs (Vue is placed on the instance).

From the simplest lines of code to the detailed and complete implementation in the framework, we can find that as long as we have the right idea and master and understand the core methods, we can easily understand the implementation principle, and most of the rest are the judgment and handling of various abnormal situations.

Observer mode

As long as the state of an object changes, all objects that depend on it are notified and updated automatically.

Also a simple implementation

// Observer list
const observers = []

// add to
const addob = (ober) => {
  observers.push(ober)
}

// notice
const notify = (...args) => {
  observers.forEach((fn) => fn(args))
}

// Test ========>
const subA = () => console.log("I am sub A")
const subB = (args) => console.log("I am sub B", args)

addob(subA)
addob(subB)
notify({ name: "sss", site: "ssscode.com" })
// I am sub A
// I am sub B [{name: "sss", site: "ssscode.com"}]

Implement an observer class

Upper code

// Observer
class Observer {
  constructor(name) {
    // Observer name
    this.name = name
  }

  // trigger
  update() {
    console.log("Observer:", this.name)
  }
}

// Observed
class Subject {
  constructor() {
    // Observer list
    this._observers = []
  }

  // Get observer list
  get obsers() {
    return this._observers
  }

  // add to
  add(obser) {
    this._observers.push(obser)
  }

  // remove
  remove(obser) {
    this._observers = this._observers.filter((n) => n !== obser)
  }

  // Notify all observers
  notify() {
    this._observers.forEach((obser) => obser.update())
  }
}

Verification test results

// Observer
const obserA = new Observer("obser-A")
const obserB = new Observer("obser-B")

// Observed
const subject = new Subject()

// Add to observer list
subject.add(obserA)
subject.add(obserB)

// notice
subject.notify()
console.log("Observer list:", subject.obsers)
// Observer: observer-a
// Observer: observer-b
// Observer list: (2) [Observer, Observer]

// remove
subject.remove(obserA)

// notice
subject.notify()
console.log("Observer list:", subject.obsers)
// Observer: observer-b
// Observer list: [observer]

Vue bidirectional data binding

The bidirectional data binding of vue is the implementation of observer mode.

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-nwg3m622-1631541451802)( https://user-gold-cdn.xitu.io/2018/10/23/166a031209fc8da5?imageView2/0/w/1280/h/960/format/webp/ignore -error/1)]

Use Object.defineProperty() to hijack data and set up a listener Observer to listen to all properties. If the properties change, you need to tell the subscriber Watcher to update the data. Finally, the instruction parser Compile parses the corresponding instructions and executes the corresponding update function to update the view and realize two-way binding~

The core of vue2.x is to hijack the data through the Object.defineProperty() method, redefine the set and get methods, and update the view once the data changes.

Vue will have a dependency collection process during initialization. Through the traversal process of attributes and instructions (the attributes here include props, data, etc., and the instructions are filtered through compile compilation), Vue will get the attributes that need to be processed responsively, and then realize monitoring, dependency collection and subscription through Observer, Dep and Watcher.

Interested friends can try to download the source code, and then use the browser breakpoint debugging to see the whole initialization process of Vue, which is very helpful for everyone to understand the running logic and process of Vue.

Here, let's take a brief look at Vue's processing of data. Other rendering passes will not be analyzed.

Initialize initData

// The $options here is actually when we write Vue
// props, data, method, computed and other attributes.
function initData(vm) {
  // data processing, function / object
  let data = vm.$options.data
  // Why do you suggest that data in vue be written in functional form?
  // When a component is defined, data must be declared as a function that returns an initial data object, because the component may be used to create multiple instances
  // If data is still a pure object, all instances will share and reference the same data object
  // By providing the data function, we can call the data function every time a new instance is created,
  // This returns a new copy of the original data object.
  // (js directly assigns the same memory address when assigning the object object. Therefore, this method is adopted for the data independence of each component.)
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}

  // observe data
  observe(data, true /* asRootData */)
}

Create observe r

function observe(value, asRootData) {
  let ob
  // Observer
  ob = new Observer(value)
  // asRootData = true
  if (asRootData && ob) {
    ob.vmCount++
  }
  //
  return ob
}

Observer class observer

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    if (Array.isArray(value)) {
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // Process all attributes and perform responsive processing
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // Array traversal processing
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Data hijacking, packing set method, monitoring data update, defineReactive

Because Object.defineProperty cannot listen to array subscripts, Vue actually rewrites the original methods of the array, such as push and pop. First, execute the original logic function. If you add elements to the array, the new elements will become responsive.

function defineReactive(obj, key, val) {
  // Dependency collection
  const dep = new Dep()

  // Data hijacking, wrapper set method, add notify notification
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // If the watcher exists, the dependency collection is triggered
      if (Dep.target) {
        dep.depend()
      }

      return val
    },
    set: function reactiveSetter(newVal) {
      // ...
      // Data change = = > trigger set method = = > call dep.notify() to notify update
      dep.notify()
    },
  })
}

Dependency collection class Dep

class Dep {
  constructor() {
    this.id = uid++
    this.subs = [] // Used to store subscriber Watcher
  }

  addSub(sub) {
    // sub ===> Watcher
    // This method is executed when the watcher adds a subscription
    this.subs.push(sub)
  }

  removeSub(sub) {
    // sub ===> Watcher
    remove(this.subs, sub)
  }

  // Dep.target===watcher, i.e. watcher.addDep
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    // subs
    const subs = this.subs.slice()
    // Call the update of the watcher
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Store unique watcher
Dep.target = null
const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Subscriber watcher

// Deleted part, just look at the core code
class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    vm._watchers.push(this)

    this.cb = cb
    this.deps = []
    this.newDeps = []
    this.value = this.get()
    this.getter = expOrFn
  }

  // Get the latest value and collect dependencies
  get() {
    pushTarget(this)

    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)

    if (this.deep) {
      // Collect each dependency of nested attributes
      traverse(value)
    }

    popTarget()
    this.cleanupDeps()

    return value
  }

  // Add dependency
  // dep === class Dep
  addDep(dep) {
    this.newDeps.push(dep)
    dep.addSub(this)
  }

  // Clear dependency
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      dep.removeSub(this)
    }
    this.deps = this.newDeps
  }

  // Provide updated interfaces
  update() {
    this.run()
  }

  // Notification execution update
  run() {
    const value = this.get()
    this.cb.call(this.vm, value, oldValue)
  }

  // Collect all dependencies through the watcher
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Through the above code, we can find that Vue adds subscription listening to each attribute in the whole data object during initialization, and rewrites the set so that we can trigger a notification when modifying the data, so that all the added subscribed attributes can be updated, Then, the view layer can be updated by render ing in combination with Vue's compiler compilation.

At this stage, we can notify and update the data, but we all know that vue is two-way data binding. When the data changes, we will continue to notify the view to update. That is, when the template compiler complie, it will filter the instructions (v-bind, v-mode, etc.) and add Watcher subscription to realize the binding and communication between observe < = = = > Watcher < = = > complie.

watcher source code: https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/watcher.js

I won't go further here. I feel a little indescribable 🤣, It's really a big head. It involves a lot of content. If you have the opportunity, you can read the source code series..., It's a little far away. Back to the article, we mainly throw out the use scenario of observer mode, and discuss the initialization process of Vue and the principle of two-way binding~

Interested students can read this article: Observer mode to achieve vue bidirectional data binding

zustand status manager

Let's look at the usage first

Create store

// store
import create from 'zustand'

// create a responsive store through the create method
const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })), // Function writing
  removeAllBears: () => set({ bears: 0 }) // Object writing
}))

Component reference

// The UI component displays the bearings status. When the status changes, the component can be updated synchronously
function BearCounter() {
  const bears = useStore(state => state.bears)
  return <h1>{bears} around here ...</h1>
}

// The control component executes the click event through the increasePopulation method created in the store to trigger the update of data and UI components
function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

Combined with the official example, it can be determined that zustand adds and registers the components bound through state to the subscriber queue by default. At this time, the bearings attribute is equivalent to an observer. When the bearings status changes, all components subscribed to the attribute will be notified to update. (we can roughly speculate about this set method)

No more nonsense. Looking at the code, let's first analyze according to the logic of creating the store:

That is, create accepts a function (non function cases will not be studied temporarily) and returns the state and method defined by us. The function provides a set method for us to use, and this set method must be able to trigger the update notification.

Direct code

  • create method
function create(createState) {
  // Initialization processing createState
  const api = typeof createState === "function" ? createImpl(createState) : createState
}
  • A createImpl method is introduced here. Let's take a look at the processing and return value of createState.
function createImpl(createState) {
  // Used to cache the last state
  let state
  // listen queue 
  const listeners = new Set()

  const setState = (partial, replace) => {
    // If it is a function, inject state and obtain the execution result; otherwise, take the value directly
    // For example: setcount: () = > set (state = > ({state: state. Count + 1})
    // For example: setcount: () = > set ({count: 10})
    const nextState = typeof partial === "function" ? partial(state) : partial
    // Optimization: judge whether the status has changed, and then update the component status
    if (nextState !== state) {
      // Last status
      const previousState = state
      // Current status latest status
      state = replace ? nextState : Object.assign({}, state, nextState)
      // Each component in the notification queue
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  // Function to get state
  const getState = () => state

  // The subscription method is processed when there is a selector or equalityFn parameter
  const subscribeWithSelector = (listener, selector = getState, equalityFn = Object.is) => {
    // Current value
    let currentSlice = selector(state)
    // The listener toadd method is actually added to the queue,
    function listenerToAdd() {
      // The value when the subscription notification is executed, that is, the value of the next update
      const nextSlice = selector(state)
      // If the values before and after comparison are not equal, an update notification is triggered
      if (!equalityFn(currentSlice, nextSlice)) {
        // Last value
        const previousSlice = currentSlice
        // Execute the added subscription function
        // For example: usestore. Subscribe (console. Log, state = > state. Paw)
        // console.log in
        listener((currentSlice = nextSlice), previousSlice)
      }
    }
    // add listenerToAdd
    listeners.add(listenerToAdd)
    // Unsubscribe
    return () => listeners.delete(listenerToAdd)
  }

  // Add subscription 
  // Columns such as usestore.subscribe (console.log, state = > state. Paw)
  // Effect: only listen for changes in paw and notify updates
  const subscribe = (listener, selector, equalityFn) => {
    // If the selector or equalityFn parameter exists, follow this logic to add the specified subscription notification
    if (selector || equalityFn) {
      return subscribeWithSelector(listener, selector, equalityFn)
    }
    // Otherwise, add subscription notifications to all changes
    listeners.add(listener)
    // Unsubscribe
    // The execution result is to delete the subscriber function
    // Namely: const unsubscribe = subscribe() = () = > listeners.delete (listener)
    return () => listeners.delete(listener)
  }

  // Clear subscription
  const destroy = () => listeners.clear()
  // The processing result returned to the create method, that is, four processing methods are returned
  const api = { setState, getState, subscribe, destroy }
  // It injects three parameters setstate, getstate and API into the passed createState function 
  // When creating a store in create, you can use methods in the parameters of the callback function to process the data
  // For example: create (set = > ({count: 0, setcount: () = > set (state = > ({state: state. Count + 1}))}))
  // Call and return API = {setstate, getstate, subscribe, destroy} attribute method
  state = createState(setState, getState, api)

  return api
}

You can get the execution result of createImpl

const api = { setState, getState, subscribe, destroy }

Then we'll come back and continue to analyze the create method

  • Briefly introduce the difference between useeffect / uselayouteeffect used in the code

    • useEffect is executed asynchronously, while uselayouteeffect is executed synchronously.
    • The execution time of useEffect is after the browser finishes rendering, while the execution time of uselayouteeffect is before the browser actually renders the content to the interface, which is equivalent to componentDidMount.
  • create method

import { useReducer, useLayoutEffect, useRef } from "react"

// Is it a non browser environment
const isSSR =
  typeof window === "undefined" ||
  !window.navigator ||
  /ServerSideRendering|^Deno\//.test(window.navigator.userAgent)

// useEffect can be executed on the server (NodeJs), but uselayouteeffect cannot
const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect

export default function create(createState) {
  const api = typeof createState === "function" ? createImpl(createState) : createState

  // Returns the useStore function for external use
  // Closures enable the api to be used internally by useStore as an execution context to ensure data isolation
  const useStore = (selector, equalityFn = Object.is) => {
    // Used to trigger component updates
    const [, forceUpdate] = useReducer((c) => c + 1, 0)

    // Get state
    const state = api.getState()
    // Mount the state to useRef to avoid side effects and update
    const stateRef = useRef(state)
    // Mount the specified selector method to useRef
    // Columns such as const bears = usestore (state = > state. Bears)
    const selectorRef = useRef(selector)
    // Equivalent method
    const equalityFnRef = useRef(equalityFn)
    // Marking error
    const erroredRef = useRef(false)

    // Current state attribute (state.bears)
    const currentSliceRef = useRef()
    // Null value processing
    if (currentSliceRef.current === undefined) {
      currentSliceRef.current = selector(state)
    }

    let newStateSlice
    let hasNewStateSlice = false

    // The selector or equalityFn need to be called during the render phase if
    // they change. We also want legitimate errors to be visible so we re-run
    // them if they errored in the subscriber.
    if (
      stateRef.current !== state ||
      selectorRef.current !== selector ||
      equalityFnRef.current !== equalityFn ||
      erroredRef.current
    ) {
      // Using local variables to avoid mutations in the render phase.
      newStateSlice = selector(state)
      // Are the old and new values equal
      hasNewStateSlice = !equalityFn(currentSliceRef.current, newStateSlice)
    }

    // Syncing changes in useEffect.
    useIsomorphicLayoutEffect(() => {
      if (hasNewStateSlice) {
        currentSliceRef.current = newStateSlice
      }
      stateRef.current = state
      selectorRef.current = selector
      equalityFnRef.current = equalityFn
      erroredRef.current = false
    })

    // Staging state
    const stateBeforeSubscriptionRef = useRef(state)
    // initialization
    useIsomorphicLayoutEffect(() => {
      const listener = () => {
        try {
          // Latest get state when the update is triggered
          const nextState = api.getState()
          // Inject nextState, execute the passed in selector method, and get the value, that is, state.bears
          const nextStateSlice = selectorRef.current(nextState)
          // Comparison inequality = = > Update
          if (!equalityFnRef.current(currentSliceRef.current, nextStateSlice)) {
            // Update stateRef to the latest state
            stateRef.current = nextState
            // Update currentSliceRef to the latest property value, i.e. state.bears
            currentSliceRef.current = nextStateSlice
            // Update component
            forceUpdate()
          }
        } catch (error) {
          // Registration error
          erroredRef.current = true
          // Update component
          forceUpdate()
        }
      }
      // Add listener subscription
      const unsubscribe = api.subscribe(listener)
      // The state has been changed. Please notify me of the update
      if (api.getState() !== stateBeforeSubscriptionRef.current) {
        listener() // state has changed before subscription
      }
      // Clear subscription on uninstall
      return unsubscribe
    }, [])

    return hasNewStateSlice ? newStateSlice : currentSliceRef.current
  }

  // Merge api properties to useStore
  Object.assign(useStore, api)

  // Closures expose the only way for external use
  return useStore
}
  • In a brief summary
  1. Create a store, get the unique interface useStore, and define the global state.

  2. Obtain the status through const bears = usestore (state = > state. Bears) and bind it to the component.

    • In this step, the store will perform the subscribe(listener) add subscription operation, and the method has a built-in forceUpdate() function to trigger the component update.
  3. Use the set hook function to modify the state.

    • That is, the called setState method, which will execute listeners. Foreach ((listener) = > listener (state, previousstate)) to notify all subscribers to update.

epilogue

Observer mode and publish subscribe mode are very common in actual projects. Many excellent third-party libraries also learn from the ideas of these two design modes - such as Vue, Vue Event, React Event, RxJS, Redux, zustand, etc.

It is very helpful for decoupling some logic or solving some asynchronous problems in the project. It is no exaggeration to say that publish subscribe mode / observer mode can solve most decoupling problems.

Generally speaking, reading and learning some excellent libraries (including some tool functions encapsulated in them, with many ingenious designs and implementations) is very helpful for our own growth and technological expansion. Many times, we will be convinced by the unique ideas and designs of the big guys, and through further understanding and mastery, we can fully absorb them for our own use, It's not beautiful to show your skills in practical projects in the future! 😎

Use case

https://github.com/JS-banana/subob-subpub

reference resources

Keywords: Javascript Front-end Design Pattern

Added by zemerick on Mon, 13 Sep 2021 21:20:16 +0300