Handwritten Vue3 responsive data principle


Preface

We want to process an object data to change the dom. But how to change the data of an object?

The two-way data binding of vue2 is implemented by using an API of ES5, Object.defineProperty() to hijack data and combine it with the publish-subscribe mode.

vue3 uses ES6's ProxyAPI to proxy data. Each object is wrapped with a layer of Proxy through the reactive() function, and changes in attributes are monitored through the Proxy to monitor data.

Compared with the vue2 version, here are the advantages of using proxy:

    1. defineProperty can only monitor a certain property, and cannot monitor the entire object. You can omit for...in..., closures, etc. to improve efficiency (just bind the entire object directly)
    1. To monitor an array, you no longer need to perform specific operations on the array separately. Proxy can directly intercept operations on all object type data, perfectly supporting monitoring of arrays.

If we want to know how to implement Vue3 responsive data, we need to know the concept of proxy.


1. What is proxy?

Proxy is a computer network technology that acts as an intermediary between the client and the server, forwarding network requests and responses. When a client sends a request, the proxy server receives and forwards the request to the target server, and then forwards the response returned by the server to the client.

It is equivalent to a star and an agent. If you want to find a star to do something, you need to find his agent. The star's affairs are all handled by the agent. The star is the source object, and the agent is equivalent to the proxy.

Proxy is used to create a proxy for an object to implement interception and customization of basic operations (such as attribute lookup, assignment, enumeration, function calling, etc.).

1.1 Basic use of proxy

// 定义一个源对象
let obj = {
    
    
	name: 'qx',
	age: 24
}
// 实现一个Proxy,传入要代理的对象和get和set方法
const proxy =  new Proxy(obj, {
    
    
	// get中返回代理对象的,target代表源对象(也是上面的obj),key代表obj中每个属性
	get(target, key) {
    
    
		return target[key];
	},
	// set中返回代理对象的,target代表源对象(也是上面的obj),key代表obj中每个属性,value是修改的新值
	set(target, key, value) {
    
    
		target[key] = value
		return true
	}
})
console.log(proxy)

obj.name = 'xqx'
// 现在打印的是修改后的proxy,看看会变成什么样?  已经修改好了
console.log(proxy)

Insert image description here

2. Implement the most basic reactive function

reactive is used to create a reactive object that can contain multiple properties and nested properties. When using reactive to create a reactive object, the returned object is a proxy object that has the same properties as the original object, and any changes to the properties of the proxy object will trigger a re-rendering of the component.

Now that we already know that reactive is a function and returns a proxy object, let’s set up the most basic framework first.

function reactive(data) {
    
    
	return new Proxy(data, {
    
    
		get(target, key) {
    
    
			return target[key];
		},
		set(target, key, value) {
    
    
			target[key] = value
			return true
		}
	})
}

It seems to have been completed, but when a non-object is passed in, an error is reported.

Prompt that this object is of object type, such as an array, and is just {}.

const arr = true;
console.log(reactive(arr))

Insert image description here
It prompts that proxy needs to pass in an object, so it needs to first determine whether it is an object.

function reactive(data) {
    
    
	//判断是不是对象,null也是object要排除
	if(typeof data === Object && data !== null) return 
	
	return new Proxy(data, {
    
    
		get(target, key) {
    
    
			return target[key];
		},
		set(target, key, value) {
    
    
			target[key] = value
			return true
		}
	})
}

3. Implement a basic responsive system

We know that processing data is to update the view, but a system cannot do without side-effect functions.

Side-effect functions, as the name suggests, functions that produce side effects are called side-effect functions. In layman's terms, this function can affect other variables.

Let’s look at the most basic side effect function

<div id="app"></div>
<script>
	let obj = {
    
    
		name: 'qx'
	}
	function effect(){
    
    
		app.innerText = obj.name
	}
	
	effect()
</script>

Now we need to complete a basic responsive system through the previous reactive function

<body>
	<div id="app"></div>
	<script>
		let obj = {
    
    
			name: 'qx',
			age: 24
		}
		function reactive(data) {
    
    
			if(typeof data === Object && data !== null) return 
			return new Proxy(data, {
    
    
				get(target, key) {
    
    
					return target[key];
				},
				set(target, key, value) {
    
    
					target[key] = value
					return true
				}
			})
		}
		const state = reactive({
    
    name:'xqx'});
		
		function effect(){
    
    
			app.innerText = state.name
		}
		
		effect()
	</script>
</body>

Insert image description here
Until now, a most basic responsive system has appeared

4. Improve the basic responsive system

If multiple side-effect functions refer to a variable at the same time, we need each side-effect function to be executed when the variable changes.

4.1 Execute each side effect function

You can put multiple side-effect functions in a list, and traverse each side-effect function every time you operate on the object and execute the set method in the proxy.

<body>
	<div id="app"></div>
	<script>
		let obj = {
    
    name: 'qx'}
		let effectBucket = [];
		
		function reactive(data) {
    
    
			if(typeof data === Object && data !== null) return 
			return new Proxy(data, {
    
    
				get(target, key) {
    
    
					return target[key];
				},
				set(target, key, value) {
    
    
					target[key] = value
					effectBucket.forEach(fn=>fn())
					return true
				}
			})
		}
		
		const state = reactive({
    
    name:'xqx'});
		
		function effect(){
    
    
			app.innerText = state.name
			console.log('副作用函数1被执行')
		}
		effectBucket.push(effect)
		
		function effect1(){
    
    
			app.innerText = state.name
			console.log('副作用函数2被执行')
		}
		effectBucket.push(effect1)
		
		state.name = 'zs'
	</script>
</body>

Insert image description here

But what if we pass two identical side-effect functions.

function effect(){
    
    
	app.innerText = state.name
	console.log('副作用函数1被执行')
}
effectBucket.push(effect)
effectBucket.push(effect)

Insert image description here
It was found that there are two duplicate effect functions in the list. If the list is long, foreach will also waste time, which will greatly waste performance. es6 has a Set data structure that can help us solve this problem.

let effectBucket = new Set();

const state = reactive({
    
    name:'xqx'});

function effect(){
    
    
	app.innerText = state.name
	console.log('副作用函数1被执行')
}
effectBucket.add(effect)  //添加两次
effectBucket.add(effect)

function effect1(){
    
    
	app.innerText = state.name
	console.log('副作用函数2被执行')
}
effectBucket.add(effect1)

console.log(effectBucket)

Let’s add the effect twice to see what the result looks like
Insert image description here

4.2 Implement dependency collection

Previously we only processed the attributes in an object. What if multiple attributes need to be changed? Our above operations will cause each side effect function to be executed.

Suppose we have a structure like this

let obj = {
    
    name: 'qx',age:24}

When I want to change the name attribute, I only update the side-effect function with name, not all the side-effect functions in the list. This requires dependency collection.

4.2.1 Basic implementation

Save each side-effect function. When the side-effect function is called, the get method in the proxy will be executed. In the get method, the current side-effect function is added to the list, thus realizing the association between the current dependency attribute and the side-effect function.

The specific implementation steps are as follows:

let obj = {
    
    name: 'qx',age:24}
let effectBucket = new Set();

let activeEffect = null;   //1.保存当前的副作用函数状态

function reactive(data) {
    
    
	if(typeof data === Object && data !== null) return 
	return new Proxy(data, {
    
    
		get(target, key) {
    
    
			if(activeEffect != null){
    
                  //4. 将当前保存的副作用函数添加到副作用函数列表中
				effectBucket.add(activeEffect)  
			}
			return target[key];
		},
		set(target, key, value) {
    
    
			target[key] = value
			effectBucket.forEach(fn=>fn())
			return true
		}
	})
}
const state = reactive(obj);
function effectName(){
    
    
	console.log('副作用函数1被执行',state.name)
}
activeEffect = effectName()  // 2.将当前副作用函数赋值给activeEffect 
effectName()                 // 3.调用副作用函数,相当于访问proxy的get方法
activeEffect = null;         // 5.将副作用函数状态置空,给下一个副作用函数用

function effectAge(){
    
    
 	console.log('副作用函数2被执行',state.age)
}
activeEffect = effectAge()
effectAge()
activeEffect = null;

state.name = 'zs'

Insert image description here
Simplify it again and encapsulate the repeated code above. When calling, directly adjust the encapsulated method

function registEffect(fn) {
    
    
	if (typeof fn !== 'function') return;
	activeEffect = fn();
	fn();
	activeEffect = null;
}

4.3 Improve bucket structure

The Set structure is like an array, except that it can remove duplicates and cannot implement different attributes corresponding to different sets. We need to improve it so that one attribute corresponds to multiple collections.

Another data structure, Map, appears in front of you. It is a collection of key-value pairs, but the scope of "keys" is not limited to strings. Various types of values ​​(including objects) can be used as keys.

Create a structure like this.

let a = {
    
    
	name: Set(fn,fn),
	age:Set(fn,fn)
}

Insert image description here

let effectBucket = new Map();  //{name:Set(fn,fn),age:Set(fn,fn)}
let activeEffect = null;

function reactive(data) {
    
    
	if (typeof data === Object && data !== null) return
	return new Proxy(data, {
    
    
		get(target, key) {
    
    
			if (activeEffect !== null) {
    
    
				let deptSet;
				if(!effectBucket.get(key)){
    
                           //没有得到key,说明没有添加过
					deptSet = new Set();            //重新创建一个集合
					effectBucket.set(key,deptSet);  //每次添加一个属性{name:Set(fn,fn)}结构
				}
				deptSet.add(activeEffect)
			}
			return target[key];
		},
		set(target, key, value) {
    
    
			target[key] = value
			//从副作用桶中依次取出每一个副作用函数执行
			let deptSet = effectBucket.get(key);
			if(deptSet){
    
                       
				deptSet.forEach(fn => fn())
			}
			return true
		}
	})
}

Continue to encapsulate and collect dependencies
get

function track(target, key) {
    
    
	if (!activeEffect) return
	let deptSet;
	if (!effectBucket.get(key)) {
    
     //没有得到key,说明没有添加过
		deptSet = new Set(); //重新创建一个集合
		effectBucket.set(key, deptSet);
	}
	deptSet.add(activeEffect)
}

set

function trigger(target, key) {
    
    
	let deptSet = effectBucket.get(key);
	if (deptSet) {
    
    
		deptSet.forEach((fn) => fn())
	}
}

Insert image description here

function track(target, key) {
    
    
	if (!activeEffect) return
	let deptMap =effectBucket.get(key);
	if (!deptMap) {
    
     //没有得到key,说明没有添加过
		deptMap = new Map(); //重新创建一个集合
		effectBucket.set(target, deptMap);
	}
	let depSet = deptMap.get(key)
	if(!depSet){
    
    
		depSet = new Set()
		deptMap.set(key,depSet)
	}
	deptSet.add(activeEffect)
}

function trigger(target, key) {
    
    
	let depMap = effectBucket.get(target)
	if(!depMap) return
	let deptSet = effectBucket.get(key);
	if (deptSet) {
    
    
		deptSet.forEach((fn) => fn())
	}
}

5. Related interview questions

1.What is the difference between Object.defineProperty and Proxy?

  1. Proxy can directly monitor objects instead of properties;
  2. Proxy can directly monitor changes in the array;
  3. Proxy has as many as 13 interception methods, not limited to apply, ownKeys, deleteProperty, has, etc., which Object.defineProperty does not have;
  4. Proxy returns a new object. We can only operate the new object to achieve the goal, while Object.defineProperty can only traverse the object properties and modify them directly.
  5. As a new standard, Proxy will be subject to continuous performance optimization by browser manufacturers, which is the legendary performance bonus of the new standard.
  6. Object.defineProperty has good compatibility and supports IE9. However, Proxy has browser compatibility issues and cannot be smoothed with polyfill. Therefore, the author of Vue stated that it needs to wait until the next major version (3.0) before rewriting with Proxy.

2. What is the difference between vue2.0 and vue3.0? Two-way binding update?

The two-way data binding of vue2 is implemented by using an API of ES5, Object.defineProperty() to hijack data and combine it with the publish-subscribe mode.

vue3 uses ES6's ProxyAPI to proxy data. Each object is wrapped with a layer of Proxy through the reactive() function, and changes in attributes are monitored through the Proxy to monitor data.

Compared with the vue2 version, here are the advantages of using proxy:

  1. defineProperty can only monitor a certain property, and cannot monitor the entire object. You can omit for in, closure, etc. to improve efficiency (just bind the entire object directly)

  2. To monitor an array, you no longer need to perform specific operations on the array separately. Proxy can directly intercept operations on all object type data, perfectly supporting monitoring of arrays.

Get props

vue2 can directly obtain props in the script code block, and vue3 passes them through the setup instruction.

API is different

Vue2 uses the options API (Options API), and Vue3 uses the composition API (Composition API).

Create data data

vue2 puts data into data, vue3 needs to use a new setup() method, which is triggered when the component is initialized and constructed.

different life cycles

view2 view 3
beforeCreate Before setup() starts creating components, it creates data and methods.
created setup()
beforeMount onBeforeMount function executed before the component is mounted on the node
mounted onMounted Function executed after component mounting is completed
beforeUpdate onBeforeUpdate function executed before component update
updated onUpdated function executed after component update is completed
beforeDestroy onBeforeUnmount Function executed before the component is mounted on the node
destroyed onUnmounted Function executed before the component is unmounted
activated onActivated function executed after component uninstallation is completed
deactivated onDeactivated

Regarding the priority of v-if and v-for:

vue2 uses v-if and v-for on an element at the same time v-for will execute first

vue3 v-if will always take effect before v-for

diff algorithm of vue2 and vue3

view2

The vue2 diff algorithm compares virtual nodes and returns a patch object to store the differences between the two nodes. Finally, the message recorded by the patch is used to locally update the Dom.

The vue2 diff algorithm compares each vnode, and for some elements that do not participate in the update, comparison is a bit performance consuming.

view 3

The vue3 diff algorithm will add a patchFlags to each virtual node during initialization, and patchFlags is the optimization identifier.

Only the vnodes whose patchFlags have changed will be compared to update the view. Elements that have not changed will be statically marked and reused directly during rendering.

3.How does Vue implement two-way data binding?

Vue data two-way binding mainly refers to: data changes update view

  • When the content of the input box changes, the data in Data changes simultaneously. That is the change of View => Data.
  • When the data in Data changes, the content of the text node changes synchronously. That is, changes in Data => View

Vue mainly implements two-way data binding through the following four steps:

  • The first step: recursively traverse the data object of the observation, including the attributes of the sub-attribute object, and add setters and getters. In this case, assigning a value to this object will trigger the setter, and then the data changes can be monitored.

  • Step 2: compile parses the template instructions, replaces the variables in the template with data, then initializes the rendering page view, binds the update function to the node corresponding to each instruction, and adds a subscriber to monitor the data. Once the data changes, receive to the notification and update the view.

  • Step 3: Watcher subscribers are the bridge of communication between Observer and Compile. The main things they do are:

    • 1. Add yourself to the attribute subscriber (dep) when instantiating yourself.
    • 2. It must have an update() method
    • 3. When notified by dep.notice() of attribute changes, you can call your own update() method and trigger the callback bound in Compile, then you will be done.
  • Step 4: As the entrance to data binding, MVVM integrates Observer, Compile and Watcher, uses Observer to monitor changes in its own model data, uses Compile to parse and compile template instructions, and finally uses Watcher to build the connection between Observer and Compile. Communication bridge, achieving the two-way binding effect of data changes -> view updates; view interactive changes (input) -> data model changes.

4. Introduce the differences between Set, Map, WeakSet and WeakMap?

Set
Set is a data structure called a set, which is a combination of a bunch of unordered, related, and non-repeating memory structures. Sets store elements in the form [value, value]

  1. Members cannot be duplicated;
  2. Only key values, no key names, a bit like an array;
  3. Can be traversed, the methods are add, delete, has, clear

WeakSet

  1. The members of WeackSet can only be reference types, not values ​​of other types;
  2. Members are weak references and can disappear at any time (not counted in the garbage collection mechanism). It can be used to save DOM nodes and is less likely to cause memory leaks;
  3. It cannot be traversed, there is no size attribute, and the methods include add, delete, and has;

Map
Map is a data structure called a dictionary. Each element has a field called key, and the keys of different elements are different. Dictionaries are stored in the form [key, value].

  1. Essentially it is a collection of key-value pairs, similar to a collection. Map keys can be any type of data, even functions. ;
  2. It can be traversed, has many methods, and can be converted to various data formats;
  3. The keys of the Map are actually bound to the memory addresses. As long as the memory addresses are different, they are regarded as two keys;

WeakMap

  1. Only accepted 对象as key names (except null), other types of values ​​are not accepted as key names;
  2. The object pointed to by the key name is not included in the garbage collection mechanism;
  3. It cannot be traversed, and there is no clear method. The methods are the same as get, set, has, and delete;

5.Why can’t Vue2.0 check changes in arrays? How to solve it?

  • Unable to detect addition of array/object?

Vue detects data changes through Object.defineProperty, so it is understandable that it cannot monitor the addition operation of the array, because this detection and binding operation has been done for all properties in the constructor.

  • Unable to detect operations that mutate an array by index? That is vm.items[indexOfItem] = newValue?
function defineReactive(data, key, value) {
    
    
	Object.defineProperty(data, key, {
    
    
		enumerable: true,
		configurable: true,
		get: function defineGet() {
    
    
			console.log(`get key: ${
      
      key} value: ${
      
      value}`)
			return value
		},
		set: function defineSet(newVal) {
    
    
			console.log(`set key: ${
      
      key} value: ${
      
      newVal}`)
			value = newVal
		}
	})
}

function observe(data) {
    
    
	Object.keys(data).forEach(function(key) {
    
    
		console.log(data, key, data[key])
		defineReactive(data, key, data[key])
	})
}

let arr = [1, 2, 3]
observe(arr)

The original Object.defineProperty found that it could be assigned by index, and also triggered the set method, but why couldn't Vue do it?
Insert image description here

For objects, every data change will enumerate the properties of the object. Generally, the number of properties of the object itself is limited, so the performance loss caused by traversing enumeration and other methods is negligible, but what about arrays? The number of elements contained in the array may reach tens of thousands. Assuming that each update of the array elements triggers enumeration/traversal, the performance loss will not be proportional to the user experience obtained, so Vue cannot detect the array. change.

solution

  • array
  1. this.$set(array, index, data)
//这是个深度的修改,某些情况下可能导致你不希望的结果,因此最好还是慎用
this.dataArr = this.originArr
this.$set(this.dataArr, 0, {
    
    data: '修改第一个元素'})
console.log(this.dataArr)        
console.log(this.originArr)  //同样的 源数组也会被修改 在某些情况下会导致你不希望的结果 
  1. splice
//因为splice会被监听有响应式,而splice又可以做到增删改。
  1. Use temporary variables for transfer
let tempArr = [...this.targetArr]
tempArr[0] = {
    
    data: 'test'}
this.targetArr = tempArr
  • object
  1. this.$set(obj, key,value) - can add and change
  2. Add deep: true when watching for deep monitoring. You can only monitor changes in attribute values. New and deleted attributes cannot be monitored.
this.$watch('blog', this.getCatalog, {
    
    
    deep: true
    // immediate: true // 是否第一次触发
  });
  1. Directly monitor a key during watch
watch: {
    
    
  'obj.name'(curVal, oldVal) {
    
    
    // TODO
  }
}

Guess you like

Origin blog.csdn.net/2201_75499330/article/details/132423460