What is reactive

Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

When there is a variable X in the logic, there is some logic code that needs to use the variable X when it runs

So we think of the code that needs to use variable X as a dependency of variable X

When variable X changes, all dependent processes of variable X can be automatically executed, which is called reactive

Analog implementation

Response function

const user = {
  name: 'Klaus'.age: 23
}

const reactiveNameFns = []
const reactiveAgeFns = []

// Define a reactive function for name
function watchNameEffect(fn) {
  reactiveNameFns.push(fn)
}

// Define a reactive function for age
function watchAgeEffect(fn) {
  reactiveAgeFns.push(fn)
}

watchNameEffect(function() {
  console.log('name code to respond to - 1')
})

watchNameEffect(function() {
  console.log('name code to respond to - 2')})function foo() {
  console.log('Other business logic, no need to be responded to')
}

watchAgeEffect(() = > console.log('age code to respond to '))

user.name = 'Alex'

// The name attribute of the user has changed, requiring the code to respond to the name
reactiveNameFns.forEach(fn= > fn())

console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -')

// The age attribute of the user has changed
reactiveAgeFns.forEach(fn= > fn())
Copy the code

You can see when a property changes with reactive functions

Any code that needs to be executed is executed correctly

But using reactive functions alone is problematic

  1. Dependencies are not collected automatically, and the corresponding updates are not automatically updated, but must be performed manually
  2. A property that needs a response corresponds to a reactive function and an array of collection dependencies, and there is a lot of repetitive code that can be extracted

Dependent response class

const user = {
  name: 'Klaus'.age: 23
}

// Define the response class -- the extraction of duplicate code
class Dep {
  constructor() {
    this.depFns = []
  }

  depend(fn) {
    this.depFns.push(fn)
  }

  notify() {
    this.depFns.forEach(fn= > fn())
  }
}

// One attribute corresponds to one Dep instance
const nameDep = new Dep()
const ageDep = new Dep()

nameDep.depend(() = > console.log('name dependency - 1'))
nameDep.depend(() = > console.log('name dependency - 2'))
nameDep.notify()

ageDep.depend(() = > console.log('the age dependence'))
ageDep.notify()
Copy the code

Collect dependent data structures

The biggest problem with the previous code was that there would be one instance of each attribute, and as the project got bigger, there would be more and more deP instances

So we need to use a data structure to manage all the DEP instances, which is WeakMap

const user = {
  name: 'Klaus'.age: 23
}

// activeReactiveFn is used to pass the response function in the set method of watchEffect and Dep instances
let activeReactiveFn = null

// Collect response classes
class Dep {
  constructor() {
    // The reason for using set is
    // If the same attribute is used more than once in a response function, the same response function will be added more than once if an array is used
    If notify is executed, the same response functions will be executed sequentially
    // This does not make sense, so the purpose of using set to store the response function is to deduplicate the corresponding response function
    this.depFns = new Set()}depend() {
    if (activeReactiveFn) {
      this.depFns.add(activeReactiveFn)
    }
  }

  notify() {
    this.depFns.forEach(fn= > fn())
  }
}

// All deP instances are managed uniformly
const deps = new WeakMap(a)// Get the corresponding DEP instance based on the object and its attribute name
function getDep(target, key) {
  let map = deps.get(target)

  if(! map) { map =new Map()
    deps.set(target, map)
  }

  let depends = map.get(key)

  if(! depends) { depends =new Dep()
    map.set(key, depends)
  }

  return depends
}

// Listen for the set and get properties of the object
const userProxy = new Proxy(user, {
  get(target, key, receiver) {
     // Collect dependencies
     const dependency = getDep(target, key)
     dependency.depend()

    return Reflect.get(target, key, receiver)
  },

  set(target, key, newValue, receiver) {
    // The dependency needs to be modified after the value is updated
    // Otherwise, reactive functions are executed using the same values as before the update
    Reflect.set(target, key, newValue, receiver)

    const dependency = getDep(target, key)
    dependency.notify()
  }
})

function watchEffect(fn) {
  activeReactiveFn = fn

  // When collecting dependencies, you need to execute the response function first
  // Only then will the corresponding property's get method be executed
  fn()

  activeReactiveFn = null
}

// Only when all objects are collected and used, the corresponding response functions of their properties will be collected and executed normally
// If you use the original object, the set and get methods of the corresponding property have not been modified, so the corresponding response cannot be triggered

// Response dependent function
watchEffect(() = > console.log('User's name has been changed - 1 --${userProxy.name}`))
watchEffect(() = > console.log('User's name has been changed - 2 --${userProxy.name}`))
watchEffect(() = > console.log('The age of user has changed ----${userProxy.age} --- ${userProxy.age}`))

console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --')

// userProxy.name = 'Alex'
userProxy.age = 18
Copy the code

However, the implementation of this code is still flawed because we need to manually convert a function to the corresponding proxy object

To do this, we can simply encapsulate the process of converting an object to a proxy object into a function called Reactive

Reactive function

let activeReactiveFn = null

class Dep {
  constructor() {
    this.depFns = new Set()}depend() {
    if (activeReactiveFn) {
      this.depFns.add(activeReactiveFn)
    }
  }

  notify() {
    this.depFns.forEach(fn= > fn())
  }
}

const deps = new WeakMap(a)function getDep(target, key) {
  let map = deps.get(target)

  if(! map) { map =new Map()
    deps.set(target, map)
  }

  let depends = map.get(key)

  if(! depends) { depends =new Dep()
    map.set(key, depends)
  }

  return depends
}

// Object --> proxy object
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
       const dependency = getDep(target, key)
       dependency.depend()

      return Reflect.get(target, key, receiver)
    },

    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)

      const dependency = getDep(target, key)
      dependency.notify()
    }
  })
}

const user = reactive({
  name: 'Klaus'.age: 23
})

function watchEffect(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

watchEffect(() = > console.log('User's name has been changed - 1 --${user.name}`))
watchEffect(() = > console.log('User's name has been changed - 2 --${user.name}`))
watchEffect(() = > console.log('The age of user has changed ----${user.age} --- ${user.age}`))

console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --')

// user.name = 'Alex'
user.age = 18
Copy the code

This is a simple implementation of the watchEffect and Reactive functions in Vue3

Vue2 responsiveness vs VUe3 responsiveness

Vue was first released in 2014, before ES6 was released, so instead of using proxy for data proxying, Object. DefineProrperty was used in VUe2

function reactive(obj) {
  Object.keys(obj).forEach(key= > {
    let value = obj[key]

    Object.defineProperty(obj, key, {
      set(newValue) {
        value = newValue

        const dependency = getDep(obj, key)
        dependency.notify()
      },

      get() {
        const dependency = getDep(obj, key)
        dependency.depend()
        return value
      }
    })
  })

  return obj
}
Copy the code

Object.defineprorperty can intercept only get and set of attributes of objects, but cannot listen for other operations of objects, such as adding new attributes or deleting attributes

So if you want to assign a new attribute to data after vue2 has defined it, the attribute is not responsive because the GET and set methods are not being listened for

So VUe2 provides the $setAPI specifically to solve this problem

In addition, when using Object.defineProrperty to modify the attributes of objects, the original data is modified, which may lead to uncontrollable data and is not conducive to later maintenance and debugging

Therefore, after Proxy is proposed in ES6, VUe3 uses Proxy to Proxy data when reconstructing VUE code. In this case, all operations of the whole object are intercepted instead of only getting and SET methods

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
       const dependency = getDep(target, key)
       dependency.depend()

      return Reflect.get(target, key, receiver)
    },

    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)

      const dependency = getDep(target, key)
      dependency.notify()
    }
  })
}
Copy the code