Vuex regret

Vuex is designed based on the Option API of Vue2. Due to some congenital problems of optionAPI, Vuex has to remedy them in various ways. Getter, mutations, action, Module, and mapXXX are looped around. To use Vuex, you must first understand these additional functions.

After the release of Vue3, Vuex4 only supported the writing method of Vue3 for downward compatibility, but did not take advantage of composition API and still adopted the original design idea. This feels like a waste of the compositionAPI.

If Vuex is too much trouble for you, welcome to see my implementation.

Lightweight state (NF-State) :

CompositionAPI provides useful, responsive methods like Reactive and Readonly, so why not just use computed? You don’t have to do the math. Wouldn’t it be cool if we went straight to Reactive?

Some students may say that the key is to track the status, to know who changed the status, so that it is easy to manage and maintain.

It doesn’t matter, we can use proxy to set a child, that is, to achieve the interception of SET, so that various functions of Vuex mutations can be realized in the interception function, including but not limited to:

  • Log state changes: function, component, code location (development mode), modification time, state, attribute name (including path), original value, new value.
  • Set hook functions to persist state and intercept state changes.
  • Persistence of state: to indexedDB, to the back end, or otherwise.
  • Other features

In other words, we don’t need to write mutations to change the state, but simply assign a value to the state.

We used to put global and local states together, but after a while, we didn’t need to put them together.

Global state requires a unified setting to avoid naming conflicts and duplicate Settings, but local state is only valid locally and does not affect other states, so there is no need for a unified setting.

So in the new design, the local state is separated and managed separately.

Since proxy only supports object types and does not support base types, the state must be designed as an object and does not accept the state of the base type. Ref is also not supported.

Lightweight state of the overall structure design

The overall MVC design pattern is adopted, and the state (Reactive and proxy) is used as the model. Then we can write the Controller function in a separate JS file, which is very flexible and easy to reuse.

To make things more complicated, you could add a service that exchanges data with back-end apis and front-end stores such as indexedDB.

You can call controller directly from the component, or you can get the state directly.

Define various states

Ok, let’s get started and see how to implement the above design.

Let’s first define a structure for states:

const info = { // The status name must be unique
   // Global status, no trace, hook, log support
   state: {
     user1: { // Each state must be an object. Base types are not supported
       name: 'jyk' //}},// It is read-only and cannot support trace, hook, or log. It can only be modified with the parameters of the initialization callback function
   readonly: {
     user2: { // Every constant must be an object. Base types are not supported
       name: 'jyk' //}},// Can trace state, support trace, hook, log
   track: {
     user3: { // Each state must be an object. Base types are not supported
       name: 'jyk' //}},// Initialization function, can get data setting state from the back end, front end, etc
   // After setting the container of the state, you can get the writable parameters of the read-only state
   init(state, _readonly) {}
Copy the code

State is divided into three categories: global state, read-only state, and trace state.

  • Global status: Direct use of Reactive is simple and fast. It is suitable for environments that can change and respond to changes regardless of how the status changes.

  • Read-only state: can be divided into two types, one is global constant, after the initial setting, everywhere else is read-only; For example, the status of the current logged-in user can be changed only in the logged-in and logged-out places. Other places can only be read-only.

  • Can track state: proxy nesting implementation reactive, because another layer, hooks, logging and other operations, so the performance is a little bit worse, well, it should not be much worse.

The separation of state into traceable and non-traceable is based on various requirements. Sometimes we care about how the state changes, or we need to set up hook functions, and sometimes we don’t. The implementation of the two requirements is somewhat different, so simply set the two states so that you can choose flexibly.

Implement various states


import { reactive, readonly } from 'vue'
import trackReactive from './trackReactive.js'
/** * make a lightweight state */
export default {
  // Reactive is a container of state
  state: {},
  // Global status trace log
  changeLog: [].// Internal hook, key: array
  _watch: {},
  // Set hook, key: callback function
  watch: {},
  // State initialization callback function async
  init: () = > {},

  createStore (info) {
    // store state into state
    for (const key in info.state) {
      const s = info.state[key]
      // Set the external empty hook
      this.watch[key] = (e) = > {}
      this.state[key] = reactive(s)
    }
    // Store readOnly into state
    const _readonly = {} // The state that can be changed
    for (const key in info.readonly) {
      const s = info.readonly[key]
      _readonly[key] = reactive(s) // Set up a reactive to change the state
      this.state[key] = readonly(_readonly[key]) // Return a read-only state
    }
    // add track to state
    for (const key in info.track) {
      const s = reactive(info.track[key])
      // Specifies the state to add listening hooks in the form of arrays
      this._watch[key] = []
      // Set the external hook
      this.watch[key] = (e) = > {
        // Add the hook
        this._watch[key].push(e)
      }
      this.state[key] = trackReactive(s, key, this.changeLog, this._watch[key])
    }
   
    // Call the initialization function
    if (typeof info.init === 'function') {
      info.init(this.state, _readonly)
    }

    const _store = this
    return {
      // Install the plug-in
      install (app, options) {
        // Set the template to use state directly
        app.config.globalProperties.$state = _store.state
      }
    }
  }
}

Copy the code

The code is very simple, with no more than 100 lines of code including comments, and is largely reactive or proxy.

Finally, return a plugin for vue to facilitate direct access to global state inside the template.

The global state does not use provide/inject, but “static object” mode. In this way, any location can be directly accessed, which is more convenient.

Implement tracking status


import { isReactive, toRaw } from 'vue'

// Record the attribute path when modifying deep attributes
let _getPath = []

/** * Reactive with tracking. Use proxy dolls *@param {reactive} _target Target to be intercepted Reactive *@param {string} Flag State name *@param {array} Log An array of trace logs *@param {array} Watch listener function *@param {object} Base Root object *@param {array} _path Path of the attribute names of each level of the nested attribute */
export default function trackReactive (_target, flag, log = [], watch = null, base = null, _path = []) {
  // Record the root object
  const _base = toRaw(_target)
  // When modifying nested attributes, record the path of the attributes
  const getPath = () = > {
    if(! base)return []
    else return _path
  }
  
  const proxy = new Proxy(_target, {
    // get does not log, does not hook, does not intercept
    get: function (target, key, receiver) {
      const __path = getPath(key)
      _getPath = __path
      // Call the prototype method
      const res = Reflect.get(target, key, receiver)
      / / record
      if (typeofkey ! = ='symbol') {
        // console.log(`getting ${key}! `, target[key])
        switch (key) {
          case '__v_isRef':
          case '__v_isReactive':
          case '__v_isReadonly':
          case '__v_raw':
          case 'toString':
          case 'toJSON':
            / / not to record at all
            break
          default:
            // For nested attributes, record the path of the attribute name
            __path.push(key) 
            break}}if (isReactive(res)) {
        // Nested attributes
        return trackReactive(res, flag, log, watch, _base, __path)
      }
      return res
    },
    set: function (target, key, value, receiver) {
      const stack = new Error().stack
      const arr = stack.split('\n')
      const stackstr = arr.length > 1 ? arr[2] :' ' // Record the function called

      const _log = {
        stateKey: flag, / / state name
        keyPath: base === null ? ' ' : _getPath.join(', '), // Attribute path
        key: key, // The property to be modified
        value: value, / / the new values
        oldValue: target[key], / / value
        stack: stackstr, // Modify state functions and components
        time: new Date().valueOf(), // Change the time
        // targetBase: base, // root
        target: target // Parent attribute/object
      }
      // Log
      log.push(_log)
      if (log.length > 100) {
        log.splice(0.30) // Remove the first 30 to avoid an array that is too large
      }

      // Set the hook, depending on the callback function
      let reValue = null
      if (typeof watch === 'function') {
        const re = watch(_log) // Execute the hook function to get the return value
        if (typeofre ! = ='undefined')
          reValue = re
      } else if (typeofwatch.length ! = ='undefined') {
        watch.forEach(fun= > { // Support multiple hooks
          const re = fun(_log) // Execute the hook function to get the return value
          if (typeofre ! = ='undefined')
            reValue = re
        })
      } 

      // Record the value returned by the hook
      _log.callbackValue = reValue
      // null: can be modified, using value; Others: Force changes, using hook return values
      const _value = (reValue === null)? value : reValue _log._value = _value// Call the prototype method
      const res = Reflect.set(target, key, _value, target)
      return res
    }
  })
  // Return the instance
  return proxy
}

Copy the code

You can use a proxy to “inherit” the responsiveness of Reactive, and then intercept the SET operation to log and change the state of functions, components, and locations.

  • Why intercept GET?

The main purpose is to support nested properties. When we modify a nested property, we actually get the first level property (object), then read its property, and then trigger the set operation. If you have multiple levels of nested properties, you recurse many times, and at the end of the set, the modified property becomes the base type.

  • How do I know the function that changes the state?

This thanks to deny friend (no child’s www.zhihu.com/people/frus…) New Error() can be used to obtain the function name, component name and location of each level to change the state. So we can record it so we know who changed the state.

Print it in concole.log(stackstr), and in F12 you can click to go to the code location. The development environment is very convenient, and the production mode is compressed, so the effect is…

const stack = new Error().stack
const arr = stack.split('\n')
const stackstr = arr.length > 1 ? arr[2] :' ' // Record the function called
Copy the code

The way it is used in Vue3 projects

We can emulate Vuex by first designing a defined JS function and then mounting it to the instance in main.js. Then set the Controller, and you can use it in the component.

define

store-nf/index.js

// Load the state of the library
import { createStore } from 'nf-state'

import userController from '.. /views/state/controller/userController.js'

export default createStore({
  // Use reactive to read and write
  state: {
    // Whether the user is logged in and the login status
    user: {
      isLogin: false.name: 'jyk'.//
      age: 19}},// Global constants, using readonly
  readonly: {// Access indexedDB and webSQL identity to distinguish between different libraries
    dbFlag: {
      project_db_meta: 'plat-meta-db' // Meta required by the platform runtime.
    },
    // Whether the user is logged in and the login status
    user1: {
      isLogin: false.info: {name: 'Test Layer 2 properties'
      },
      name: 'jyk'.//
      age: 19}},// Keep track of status and proxy to Reactive
  track: {
    trackTest: {
      name: 'Trace test'.age: 18.children1: {
        name1: 'Child property Tests'.children2: {
          name2: 'Nested again'}}},test2: {
      name: ' '}},// You can set the initial state for the global state. For synchronous data, you can set the initial state directly above. For asynchronous data, you can set the initial state here.
  init (state, read) {
    userController().setWriteUse(read.user1)
    setTimeout(() = > {
      read.dbFlag.project_db_meta = 'Modify after loading'
    }, 2000)}})Copy the code

Two user states are set, one accessible and one read-only, for demonstration purposes.

State names cannot be repeated because they are all in the same container.

  • Initialize the

State is a container of state, and read is a read-only object that can be modified. Read can be used to change the read-only state.

Here we introduce the user’s controller and pass read so that the controller can change its read-only state.

main.js

import { createApp } from 'vue'
import App from './App.vue'

import store from './store' // vuex
import router from './router' / / routing

import nfStore from './store-nf' // Lightweight state

createApp(App)
  .use(nfStore)
  .use(store)
  .use(router)
  .mount('#app')
Copy the code

Main.js is basically used in the same way as Vuex. In addition, it does not conflict with Vuex and can be used simultaneously in a project.

controller

Okay, now that we get to the core, let’s look at how controller is written, and here we simulate the current logged-in user.

// User management class
import { state } from 'nf-state'

let _user = null

const userController = () = > {
  // Get the state that can be changed
  const setWriteUse = (u) = > {
    _user = u
  }

  const login = (code, psw) = > {
    // Pretend to access the back end
    setTimeout(() = > {
      // Get user information
      const newUser = {
        name: 'Username passed from back end:' + code
      }
      Object.assign(_user, newUser)
      _user.isLogin = true
    }, 100)}const logout = () = > {
    _user.isLogin = false
    _user.name = 'Already quit'
  }

  const getUser = () = > {
    // Returns read-only user information
    return state.user1
  }

  return {
    setWriteUse,
    getUser,
    login,
    logout
  }
}

export default userController
Copy the code

Isn’t that clear.

component

With all the work done, how do you use it in components?

  • Use it directly in templates
<template>Global status -user: {{$state.user1}}<br>
</template>
Copy the code
  • Direct use state
import { state, watchState } from 'nf-state'

// State can be manipulated directly
console.log(state)

const testTract2 = () = > {
  state.trackTest.children1.name1 = new Date().valueOf()
}
 
const testTract3 = () = > {
  state.trackTest.children1.children2.name2 = new Date().valueOf()
  state.test2.name = new Date().valueOf()
}
Copy the code

So it becomes reactive, and you’re all familiar with that.

  • Use state through controller
import userController from './controller/userController.js'

const { login, logout, getUser } = userController()

// Get the user status, read-only
const user = getUser()

// Simulate login
const ulogin = () = > {
  login('jyk'.'123')}// Simulate logging out
const ulogout = () = > {
  logout()
}
Copy the code

Set up listeners and hooks

import { state, watchState } from 'nf-state'

// Set the listener and hook
watchState.trackTest(({keyPath, key, value, oldValue}) = > {
  if (keyPath === ' ') {
    console.log(`\nstateKey.${key}= `)}else {
    console.log(`\nstateKey.${keyPath.replace(', '.'. ')}.${key}= `)}console.log('oldValue:', oldValue)
  console.log('value:', value )
  // return null
})
Copy the code

WatchState is a container that can be followed by a hook function with the same name as the state, meaning that the state name does not have to be written as a string.

We can directly specify the state to listen to, without affecting other states. The hook can fetch the log generated by the current set, thus obtaining various information.

You can also influence state changes by returning values:

  • No return value: State change is allowed.
  • Return original value: state changes are not allowed and the original value is maintained.
  • Return other value: Indicates that the return value is set to the value after the state change.

Local state

Local states do not need to be defined uniformly, but can be directly written to controller. A controller can be an object, a function, or a class.

import { reactive, provide, inject } from 'vue'
import { trackReactive } from 'nf-state'

const flag = 'test2'

/** * inject local state */
const reg = () = > {
  // It needs to be defined inside the function, otherwise it becomes "global".
  const _test = reactive({
    name: 'Controller in object form of local state'
  })
  / / injection
  provide(flag, _test)
  // Other actions, such as setting watch
  return _test
}

/** * get the state of the injection */
const get = () = > {
  / / to get
  const re = inject(flag)
  return re
}

const regTrack = () = > {
  const ret = reactive({
    name: 'Traceable state of local state'
  })
  // Define a container for logging trace logs
  const logTrack = reactive([])
  // Set the listener and hook
  const watchSet = (res) = > {
    console.log(res)
    console.log(res.stack)
    console.log(logTrack)
  }
  const loaclTrack = trackReactive(ret, 'loaclTrack', logTrack, watchSet)

  return {
    loaclTrack,
    logTrack,
    watchSet
  }
}

// Other operations

export {
  regTrack,
  reg,
  get,
}

Copy the code

Provide /inject + reactive if no trace is needed, it is nothing special. To implement tracing, you need to introduce trackReactive and set up log arrays and hook functions.

The source code

Gitee.com/naturefw/vu…

The online demo

Naturefw. Gitee. IO/vite2 – vue3 -…