Publish-subscribe pattern vs. the observer pattern | small handsome の technology blog (ssscode.com)

background

A recent study of zustand, the React state manager, found that the component registry binding was updated using observer mode in conjunction with React hooks while exploring the source code. While lenovo before writing vue, often use vue built-in custom events to communicate components ($emit/on), and this should be a subscription model, make I have nodded, feeling the two models is very similar, he was a little confused, feel not understand, therefore, it is conveniently in-depth study of the two modes, Try writing your own hand to deepen your understanding. This article is my personal combing experience, if there are mistakes welcome to correct, common progress ~

contrast

The difference between

Observer mode: An object in software design that maintains a list of dependencies and automatically notifies them when any state changes.

The publish-subscribe design pattern: Rather than sending messages directly to a specific recipient (called a subscriber), the sender (publisher) of the message filters and allocates the message through an information mediation.

Popular image point is:

  • The observer model has no middlemen to make the difference, and the publish and subscribe model has middlemen to make the difference.
  • The observer model is a one-size-fits-all model that treats all subscribers equally, while the publish/subscribe model can be colored, with a layer of filtering or black-box operation.

Let me put up a picture for you to feel

To summarize

  • In the observer mode, the observer is aware of the Subject, and the Subject keeps a record of the observer. However, in the publish-subscribe model, publishers and subscribers are unaware of each other’s existence. They communicate only through the message broker.

  • In the publish-subscribe pattern, components are loosely coupled, as opposed to the observer pattern.

  • The observer pattern is synchronous most of the time, such that when an event is triggered, the Subject calls the observer’s methods. The publish-subscribe pattern is asynchronous most of the time (using message queues).

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

The concepts seem clear and the differences between them are easy to understand. Let’s start implementing it ourselves, getting into its internals and operating logic.

Publish and subscribe model

The VUE Event Bus is an implementation of the publish-subscribe model, as well as Nodejs’ Emitter Events.

Implement a publish subscription that supports subscribing, unbinding, publishing, and multiple bindings for the same type of event.

Let’s do a simple implementation

In the code

// Subscribe center
const subscribers = {}
/ / subscribe
const subscribe = (type, fn) = > {
  // Add queues in array mode, so that multiple bindings of the same type are supported
  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 the subscription
const unsubscribe = (type, fn) = > {
  if(! subscribers[type] || ! subscribers[type].length)return
  subscribers[type] = subscribers[type].filter((n) = >n ! == fn) }Copy the code

Verification test

// console test ======>
subscribe("topic-1".() = > console.log("Suber-a has subscribed to topic-1"))
subscribe("topic-2".() = > console.log("Suber-b has subscribed to Topic-2"))
subscribe("topic-1".() = > console.log("Suber-c subscribed to topic-1"))

publish("topic-1") // Notification subscribed to A and C of topic-1

// Output the result
// Suber-a subscribed topic-1
// Suber-c subscribed topic-1
Copy the code

Implement an Emitter class

In the code

class Emitter {
  constructor() {
    // Subscribe center
    this._event = this._event || {}
  }
  // Register for a subscription
  addEventListener(type, fn) {
    const handler = this._event[type]

    if(! handler) {this._event[type] = [fn]
    } else {
      handler.push(fn)
    }
  }
  // Unsubscribe
  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))
    }
  }
}
Copy the code

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)}
Copy the code

Vue Event Bus implementation

Comb structure

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. Mount event center _events to Vue instance:

    vm._events = {}

  2. Mount all the methods: $ON, $once, $OFF, $emit to the Vue prototype

This has the advantage of being able to emit this.$on, this.$emit directly when used in Vue components

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

Look at the code

  1. $on adds registration

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

    // $once
    Vue.prototype.$once = function (event, fn) {
      const vm = this
    
      // The on method is called when the event event is emitted
      function on() {
    
        // First execute the $off method to uninstall the callback method
        vm.$off(event, on)
    
        // Execute this callback method again
        fn.apply(vm, arguments)}// This assignment will be used in $off: cb.fn === fn
      // Because the $once method calls the $ON add callback, but adds the wrapped ON method instead of the fn method
      $off === fn; $off == cb.fn
      on.fn = fn
    
      // Call the $on method to add the callback to the queue
      vm.$on(event, on)
    
      return vm
    }
    Copy the code
  3. $off Uninstall delete

    // $off
    Vue.prototype.$off = function (event, fn) {
      const vm = this
    
      If no arguments are passed, clear all events
      if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
      }
    
      // If the event is an array, repeat the $on logic to recursively unload the event
      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 the event does not have a binding callback, it is not processed
      if(! cbs) {return vm
      }
    
      // If no unbind callback is passed for the corresponding event, all of that event is cleared
      if(! fn) { vm._events[event] =null
        return vm
      }
    
      // event Both the event type and the callback are present, and the specified callback is removed by traversal
      let cb
      let i = cbs.length
      while (i--) {
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1)
          break}}return vm
    }
    Copy the code
  4. $emit triggers the event

    // $emit
    Vue.prototype.$emit = function (event) {
      const vm = this
    
      // Callback list
      let cbs = vm._events[event]
    
      // Check whether the event has a callback
      if (cbs) {
    
        // the $emit method can pass arguments, which will be passed when the callback function is called
        // Exclude other parameters of the event parameter
        // toArray is a method that converts class arrays into arrays and supports interception
        const args = toArray(arguments.1)
    
        // Iterate over the callback function
        for (let i = 0, l = cbs.length; i < l; i++) {
          cbs[i].apply(vm, args)
        }
      }
      return vm
    }
    Copy the code
    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
    }
    Copy the code

Under test

Let’s start with a simulated Vue class test

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

  // provide an interface to get _events externally
  get event() {
    return this._events
  }
}
Copy the code

Verify the results

/ / instantiate
const myVue = new Vue()

// Add a 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

// Print the output
console.log("events:", myVue.event)
/ / events: {user: [(args) = > console. The log (" user: ", args), ƒ on (the)]}

// 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}

// Unsubscribe
myVue.$off("user", once_update_user)
console.log("events:", myVue.event)
// events: {user: []}
Copy the code

Under the small summary

Vue encapsulates the publish and subscribe model, which can be said to be very perfect. This is completely independent of the code used in other projects, and then adjust the location of the event memory according to their own needs (Vue is placed on the instance).

From the simplest few lines of code to the detailed and complete implementation in the framework, we can find that: in fact, as long as we have the right idea and the core method is understood, it is easy to understand its implementation principle, and the rest is mostly the judgment and processing of various abnormal situations.

Observer model

Whenever an object’s state changes, all objects that depend on it are notified and automatically updated.

Let’s do a simple implementation

// List of observers
const observers = []

/ / add
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"}]
Copy the code

Implement an observer class

In the code

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

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

// Observed
class Subject {
  constructor() {
    // List of observers
    this._observers = []
  }

  // Get the list of observers
  get obsers() {
    return this._observers
  }

  / / add
  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())
  }
}
Copy the code

Verify the test results

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

// Observed
const subject = new Subject()

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

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

/ / remove
subject.remove(obserA)

/ / notice
subject.notify()
console.log("Observer List:", subject.obsers)
// Observer: obser-b
// Observer list: [Observer]
Copy the code

Vue bidirectional data binding

Vue’s bidirectional data binding is an implementation of the observer pattern.

Object.defineproperty () is used to hijack the data and set up a listener Observer to listen for all attributes. If the attributes change, Watcher needs to be told to update the data. Finally, Compile interprets the corresponding directives. The corresponding update function is then executed to update the view, implementing bidirectional binding ~

The core of vue2. X is hijacking data through object.defineProperty () and redefining set and get methods to update views when data changes.

When Vue is initialized, there is a dependency collection process. By traversing properties and instructions (the properties include props and data, etc., and the instructions are filtered by compile), the properties that need to be processed in a responsive manner can be obtained. Then through Observer, Dep, Watcher to implement listening, dependency collection, subscription.

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

Here’s a quick look at how Vue handles data. Other rendering processes will not be analyzed.

Initialize the initData

// $options is where we write Vue
// Attributes such as props, data, method, computed, etc.
function initData(vm) {
  // Process data, functions/objects
  let data = vm.$options.data
  // Why do we recommend that data in vue be written functionally?
  // When a component is defined, data must be declared as a function that returns an initial data object, since the component may be used to create multiple instances
  // If data remains a pure object, all instances will share references to the same data object
  // By providing the data function, we can call the data function each time a new instance is created,
  // Returns a new copy of the original data.
  // When you assign an object, it is the same memory address. So this approach is used for data independence of each component.
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}

  // observe data
  observe(data, true /* asRootData */)}Copy the code

Create observer observe

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

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 for reactive 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])
    }
  }
}
Copy the code

Data hijacking, wrapping set methods, listening for data updates to defineReactive

Object.defineproperty does not listen for array subscripts, so Vue overwrites the array’s original methods, such as push and pop, to perform the original logic first, and to add elements to the array to make the new elements responsive.

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

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

      return val
    },
    set: function reactiveSetter(newVal) {
      // ...
      // Data changes ==> Trigger the set method ==> call dep.notify() to notify updates
      dep.notify()
    },
  })
}
Copy the code

Rely on the collection class Dep

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

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

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

  / / Dep. Target = = = watcher watcher. Namely addDep
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}notify() {
    // subs
    const subs = this.subs.slice()
    // Call watcher's update
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Store a 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]}Copy the code

The subscriber watcher

// 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 for nested properties
      traverse(value)
    }

    popTarget()
    this.cleanupDeps()

    return value
  }

  // Add dependencies
  // dep === class Dep
  addDep(dep) {
    this.newDeps.push(dep)
    dep.addSub(this)}// Clear dependencies
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      dep.removeSub(this)}this.deps = this.newDeps
  }

  // Provide an updated interface
  update() {
    this.run()
  }

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

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

We can see from the above code that Vue adds subscriptions to every attribute in the whole data object when it is initialized, and we can trigger notification when we modify data through rewriting set, so that all the attributes added subscriptions can be updated. Then the view layer can be updated by rendering with Vue compiler.

At this point, you can notify the data of updates, but we all know that VUE is two-way data bound and continues to notify the view of updates as data changes. Observe Watcher <===> Watcher <===> complie <===> complie <===>

Watcher source: github1s.com/vuejs/vue/b…

Here I will not continue to go deep, feel a little said not over 🤣, really let head big, involved in the content a little more, if you have the opportunity to engage in reading source code series… Back to this article we are mainly thrown observer mode usage scenarios, and the initialization process of Vue and bidirectional binding principle discussion, close ~

For those interested, check out this article: Observer Pattern implements VUE bidirectional data binding

Zustand Status manager

Let’s look at the usage first

Create the store

// store
import create from 'zustand'

// Create a responsive store with the create method
const useStore = create(set= > ({
  bears: 0.increasePopulation: () = > set(state= > ({ bears: state.bears + 1 })), // function
  removeAllBears: () = > set({ bears: 0 }) // Object notation
}))
Copy the code

A component reference

// UI component, display bears state, when the state changes can realize component synchronization update
function BearCounter() {
  const bears = useStore(state= > state.bears)
  return <h1>{bears} around here ...</h1>
}

// Control component, which executes click events via the increasePopulation method created within the store, triggering data and UI component updates
function Controls() {
  const increasePopulation = useStore(state= > state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
Copy the code

With the official example, we can confirm that zustand internally adds the registered subscriber queue to components bound by state by default. At this point, the Bears property acts as an observed. When the bears status changes, all components subscribed to the bears property are notified to update. (We can speculate about the set method.)

Without further ado, let’s look at the code and follow the logic of creating a store:

That is, create takes a function (not a non-function for now) that returns a defined state and method that provides a set method that must trigger an update notification.

Go straight to code

  • createmethods
function create(createState) {
  // Initialize createState
  const api = typeof createState === "function" ? createImpl(createState) : createState
}
Copy the code
  • I’m introducing one herecreateImplMethod, let’s look at this method rightcreateStateAnd return values.
function createImpl(createState) {
  // Used to cache the last state
  let state
  // Listen to the queue
  const listeners = new Set(a)const setState = (partial, replace) = > {
    // if function injects state and gets the result of execution, otherwise it takes the value
    SetCount: ()=> set(state=> ({state: state.count +1}))
    // For example: setCount: ()=> set({count: 10})
    const nextState = typeof partial === "function" ? partial(state) : partial
    // Optimize: determine whether the state has changed and then update the component state
    if(nextState ! == state) {// Last status
      const previousState = state
      // Current status Latest status
      state = replace ? nextState : Object.assign({}, state, nextState)
      // Notify each component in the queue
      listeners.forEach((listener) = > listener(state, previousState))
    }
  }

  // The function gets state
  const getState = () = > state

  // The subscription method is processed in the presence of selector or equalityFn
  const subscribeWithSelector = (listener, selector = getState, equalityFn = Object.is) = > {
    // The current value
    let currentSlice = selector(state)
    // The listenerToAdd 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 are not equal before and after the comparison, the update notification will be triggered
      if(! equalityFn(currentSlice, nextSlice)) {// Last value
        const previousSlice = currentSlice
        // Execute the added subscription function
        Subscribe (console.log, state => state.paw)
        / / in the console. The log
        listener((currentSlice = nextSlice), previousSlice)
      }
    }
    // add listenerToAdd
    listeners.add(listenerToAdd)
    // Unsubscribe
    return () = > listeners.delete(listenerToAdd)
  }

  // Add a subscription
  Subscribe (console.log, state => state.paw)
  // Effect: only monitor paw changes, notification updates
  const subscribe = (listener, selector, equalityFn) = > {
    // If the selector or equalityFn parameter exists, use this logic to add the specified subscription notification
    if (selector || equalityFn) {
      return subscribeWithSelector(listener, selector, equalityFn)
    }
    // Otherwise add subscription notifications for all changes
    listeners.add(listener)
    // Unsubscribe
    // The result is to delete the subscriber function
    // that is, const unsubscribe= subscribe() = () => listeners.
    return () = > listeners.delete(listener)
  }

  // Clear the subscription
  const destroy = () = > listeners.clear()
  // Returns the result of the create method, that is, four methods are returned
  const api = { setState, getState, subscribe, destroy }
  // It injects three parameters setState, getState, and API into the createState function passed in
  // When creating a store, you can use methods in the callback function parameters to process data
  / / such as: the create (set = > ({count: 0, setCount: () = > set (state = > ({state: the state. The count + 1}))}))
  // and calls and then returns the API = {setState, getState, subscribe, destroy} property methods
  state = createState(setState, getState, api)

  return api
}
Copy the code

You can get the result of createImpl

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

And then we’ll come back and look at the CREATE method

  • A brief introduction to the useEffect/useLayoutEffect differences used in code

    • useEffectIs executed asynchronously, whileuseLayoutEffectIt is executed synchronously.
    • useEffectIs executed after the browser has finished rendering, anduseLayoutEffectIs executed before the browser actually renders the content into the interface, andcomponentDidMountEquivalence.
  • The create method

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

// Whether it is 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), not useLayoutEffect
const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect

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

  // Return useStore for external use
  // Closures enable the API to act as an execution context for internal use with useStore, ensuring data isolation
  const useStore = (selector, equalityFn = Object.is) = > {
    // Used to trigger component updates
    const [, forceUpdate] = useReducer((c) = > c + 1.0)

    / / for the state
    const state = api.getState()
    // Mount state to useRef to prevent side effects from updating it
    const stateRef = useRef(state)
    // Mounts the specified selector method to useRef
    // column like: const bears = useStore(state => state. Bears)
    const selectorRef = useRef(selector)
    // Equivalent method
    const equalityFnRef = useRef(equalityFn)
    // Mark error
    const erroredRef = useRef(false)

    // The current state property (state.bears)
    const currentSliceRef = useRef()
    // Null 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)
      // Whether the old and new values are equalhasNewStateSlice = ! 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
    })

    / / the temporary state
    const stateBeforeSubscriptionRef = useRef(state)
    / / initialization
    useIsomorphicLayoutEffect(() = > {
      const listener = () = > {
        try {
          // The latest fetch state when the update is triggered
          const nextState = api.getState()
          // Inject nextState to execute the selector method passed in and get the value, state.bears
          const nextStateSlice = selectorRef.current(nextState)
          // Comparison is not equal ==> 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 the component
            forceUpdate()
          }
        } catch (error) {
          // Register error
          erroredRef.current = true
          // Update the component
          forceUpdate()
        }
      }
      // Add a listener subscription
      const unsubscribe = api.subscribe(listener)
      // State has been changed
      if(api.getState() ! == stateBeforeSubscriptionRef.current) { listener()// state has changed before subscription
      }
      // Clear subscriptions when uninstalling
      return unsubscribe
    }, [])

    return hasNewStateSlice ? newStateSlice : currentSliceRef.current
  }

  // Merge API attributes to useStore
  Object.assign(useStore, api)

  // Closures expose only methods for external use
  return useStore
}
Copy the code
  • So just to summarize
  1. Create store to expose the unique interface useStore and define the global state.

  2. Get the state from const bears = useStore(state => state.bears) and bind it to the component.

    • This stepstoreWill performsubscribe(listener)Add a subscription operation, and the method has built-inforceUpdate()The function is used to trigger component updates.
  3. Use the set hook function to change the state.

    • The call ofsetStateMethod, which executeslisteners.forEach((listener) => listener(state, previousState))Notify all subscribers to perform updates.

conclusion

The Observer and published-subscribe patterns are very common in real projects, and many good third-party libraries have borrowed ideas from these two design patterns — Vue, Vue Event, React Event, RxJS, Redux, Zustand, etc.

It is useful for logical decoupling or solving asynchronous problems in a project. It is no exaggeration to say that the publish-subscribe/observer model solves most of the decoupling problems.

In general, reading some excellent library (including some of their internal packaging tools function, there are a lot of clever design and implementation), study the source code for our own growth and technology development are of great help/very helpful, many times we succumb to bosses of unique ideas and designs, and through the further understanding to master, for we can absorb use, In the actual combat project in the future, it is not beautiful! 😎

Use cases

Github.com/JS-banana/s…

reference

  • Observer vs. publish-subscribe
  • What is the difference between the observer and subscription-publish modes
  • The observer model is really different from the publish-subscribe model
  • Publish and subscribe model
  • Deep publish subscribe model
  • JavaScript design pattern observer pattern
  • JS Design pattern observer pattern
  • Observer mode (JavaScript implementation)
  • vue
  • zustand
  • Difference between useLayoutEffect and useEffect