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
- Dependencies are not collected automatically, and the corresponding updates are not automatically updated, but must be performed manually
- 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