Vue 2.x responsiveness
Principle of responsiveness
Object. DefineProperty is used to implement the observer pattern
When you pass an ordinary JavaScript object into a Vue instance as the data option, Vue iterates through all of the object’s properties, And use Object.defineProperty to turn all of these properties into getters/setters. Object.defineproperty is a non-shim feature in ES5, which is why Vue does not support IE8 and earlier browsers.
These getters/setters are invisible to the user, but internally they allow Vue to track dependencies and notify changes when the property is accessed and modified. It is important to note that different browsers format getters/setters differently when printing data objects on the console, so it is recommended to install vue-devTools for a more user-friendly interface for examining data.
Each component instance corresponds to a Watcher instance, which records “touched” data properties as dependencies during component rendering. Watcher is then notified when the setter for the dependency fires, causing its associated component to be re-rendered.
Dependencies collect and trigger dependencies, rendering views with the template compiler. The dependency Collection (DEP) collects the subscriber Watcher and triggers trigger(Notify) that relies on the subscriber –Watcher to update the view. Dependency collection is in the getter method of Object.defineProperty, and in the setter method the dependency update view is triggered.
Precautions for detecting changes
For the object
Vue cannot detect the addition or removal of property. Since Vue performs getter/setter conversions on property when it initializes the instance, the property must exist on the data object for Vue to convert it to reactive. Such as:
var vm = new Vue({
data: {a:1}})// 'vm.a' is reactive
vm.b = 2
// 'vm.b' is non-responsive
Copy the code
Vue does not allow dynamic root-level responsive properties to be added to already created instances. However, you can add responsive properties to nested objects using the vue.set (Object, propertyName, value) method. For example, for:
Vue.set(vm.someObject, 'b'.2)
Copy the code
You can also use the vm.$set instance method, which is also an alias for the global vue.set method:
this.$set(this.someObject,'b'.2)
Copy the code
Sometimes you may need to assign multiple new properties to existing objects, such as object.assign () or _.extend(). However, new properties that are thus added to the object do not trigger updates. In this case, you should create a new object with the property of the original object and the property of the object to be mixed in.
// Replace 'object. assign(this.someObject, {a: 1, b: 2})'
this.someObject = Object.assign({}, this.someObject, { a: 1.b: 2 })
Copy the code
$set adds attributes to add responsiveness (Object.defineProperty), and then notify updates the view. Vm.$delete delete property and notify update view.
For an array of
Vue cannot detect changes to the following arrays:
Items [indexOfItem] = newValue 2. When you change the length of the array, for example, vm.items. Length = newLength
var vm = new Vue({
data: {
items: ['a'.'b'.'c']
}
})
vm.items[1] = 'x' // Not responsive
vm.items.length = 2 // Not responsive
Copy the code
To solve the first type of problem, the following two methods can achieve the same effect as vm.items[indexOfItem] = newValue, and also trigger status updates within a responsive system:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
Copy the code
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
Copy the code
You can also use the vm.$set instance method, which is an alias for the global method vue. set:
vm.$set(vm.items, indexOfItem, newValue)
Copy the code
To solve the second type of problem, you can use Splice:
vm.items.splice(newLength)
Copy the code
For arrays, vm.$set/vm.$delete is implemented using splice, so use splice directly.
Asynchronous update queue
In case you haven’t noticed, Vue executes asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work. Vue internally attempts to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and setTimeout(fn, 0) instead if the execution environment does not support it.
For example, when you set vm.someData = ‘new value’, the component does not immediately re-render. When the queue is refreshed, the component is updated in the next event loop “TICK”. In most cases we don’t need to worry about this process, but if you want to do something based on the updated DOM state, it can be tricky. While vue.js generally encourages developers to think in a “data-driven” way and avoid direct contact with the DOM, sometimes we have to. To wait for Vue to finish updating the DOM after the data changes, use vue.nexttick (callback) immediately after the data changes. This callback will be called after the DOM update is complete. Such as:
<div id="example">{{message}}</div>
Copy the code
var vm = new Vue({
el: '#example'.data: {
message: '123'
}
})
vm.message = 'new message' // Change the data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
Copy the code
Using the vm.$nextTick() instance method within a component is particularly convenient because it does not require a global Vue and this in the callback function is automatically bound to the current Vue instance:
Vue.component('example', {
template: '<span>{{ message }}</span>'.data: function () {
return {
message: 'Not updated'}},methods: {
updateMessage: function () {
this.message = 'Updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function () {
console.log(this.$el.textContent) // => 'Updated'}}}})Copy the code
Because $nextTick() returns a Promise object, you can do the same thing using the new ES2017 async/await syntax:
methods: {
updateMessage: async function () {
this.message = 'Updated'
console.log(this.$el.textContent) // => 'not updated'
await this.$nextTick()
console.log(this.$el.textContent) // => 'Updated'}}Copy the code
Vue 3.x responsiveness
Principle of responsiveness
Proxy is used to implement the observer pattern
When a normal JavaScript object is passed to an application or component instance as a data option, Vue uses a handler with getters and setters to iterate over all of its properties and convert it to a Proxy. This is only a feature of ES6, but we also used Object.defineProperty to support IE in Vue 3. Both have the same Surface API, but the Proxy version is streamlined and improves performance.
A Proxy is an object that contains another object or function and allows you to intercept it.
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop, receiver) {
if(typeof target[prop] === 'object'&& target[prop] ! = =null) {// Recursive proxy, only when the corresponding value is obtained
return new Proxy(target[prop], handler)
}
track(target, prop) // Collect dependencies
return Reflect.get(... arguments) },set(target, key, value, receiver) {
trigger(target, key) // Trigger dependencies
return Reflect.set(... arguments) } }const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
Copy the code
The responsiveness difference between Vue 3.x and Vue 2.x is mainly in the area of data hijacking.
Declare reactive state
To create reactive state for JavaScript objects, you can use the reactive method:
import { reactive } from 'vue'
// Reactive state
const state = reactive({
count: 0
})
Copy the code
Reactive is equivalent to the Vue.Observable () API in Vue 2.x, renamed to avoid confusion with observables in RxJS. The API returns a reactive object state. This reactive transformation is a “deep transformation” — it affects all properties passed by nested objects.
The basic use case for reactive state in Vue is that we can use it during rendering. Because of its reliance on tracing, the view updates automatically when reactive state changes.
This is the essence of a Vue responsive system. When an object is returned from data() in a component, it is internally passed on to Reactive () to make it a reactive object. The template is compiled into rendering functions that can use these reactive properties.
Create separate reactive values as refs
Imagine that we have a single raw value (for example, a string) and we want it to be reactive. Of course, we can create an object with the same string property and pass it to Reactive. Vue gives us a way to do the same thing — ref:
import { ref } from 'vue'
const count = ref(0)
Copy the code
Ref returns a mutable reactive object as its internal value — a reactive reference, which is where the name comes from. This object contains only one property named value:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) / / 0
count.value++
console.log(count.value) / / 1
Copy the code
Ref on
When ref is returned as a property on the render context (the object returned from Setup ()) and can be accessed in the template, it will automatically shallow expand the internal value. Add.value to the template only when accessing nested refs:
<template>
<div>
<span>{{ count }}</span>
<button @click="count ++">Increment count</button>
<button @click="nested.count.value ++">Nested Increment count</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
</script>
Copy the code
Access reactive objects
When ref is accessed or changed as a property of a responsive object, it automatically expands the internal value to make it behave like a regular property:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) / / 0
state.count = 1
console.log(count.value) / / 1
Copy the code
If the new ref is assigned to the property of the existing ref, the old ref will be replaced:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) / / 2
console.log(count.value) / / 1
Copy the code
Ref expansion only occurs when nested by a reactive Object. Ref is not expanded when accessed from Array or native collection types such as Map:
const books = reactive([ref('Vue 3 Guide')])
// We need.value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// We need.value
console.log(map.get('count').value)
Copy the code
Reactive state deconstruction
When we want to use some property of a large responsive object, it might be tempting to use ES6 deconstruction to get the property we want:
import { reactive } from 'vue'
const book = reactive({
author: 'Vue Team'.year: '2020'.title: 'Vue 3 Guide'.description: 'You are reading this book right now ;) '.price: 'free'
})
let { author, title } = book
Copy the code
Unfortunately, the responsiveness of both properties using deconstruction is lost. In this case, we need to convert our reactive objects to a set of Refs. These Refs will retain reactive associations with source objects:
import { reactive, toRefs } from 'vue'
const book = reactive({
author: 'Vue Team'.year: '2020'.title: 'Vue 3 Guide'.description: 'You are reading this book right now ;) '.price: 'free'
})
let { author, title } = toRefs(book)
title.value = 'Vue 3 Detailed Guide' // We need to use.value as the title, now ref
console.log(book.title) // 'Vue 3 Detailed Guide'
Copy the code
usereadonly
Prevents changes to reactive objects
Sometimes we want to track changes in a reactive object (REF or Reactive), but we also want to prevent it from changing somewhere in the application. For example, when we have a responsive object that is provided, we don’t want it to be changed during injection. To do this, we can create a read-only proxy object based on the original object:
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// Changing count by Original triggers copy-dependent listeners
original.count++
// Changing count by copy will fail with a warning
copy.count++ // warning: "Set operation on key 'count' failed: target is readonly.
Copy the code
Reactive computing and listening
Calculated value
Sometimes we need to rely on the state of other states — in Vue, this is handled with component evaluation properties to create computed values directly, and we can use computed methods: It takes a getter function and returns an immutable responsive REF object for the value returned by the getter.
const count = ref(1)
const plusOne = computed(() = > count.value + 1)
console.log(plusOne.value) / / 2
plusOne.value++ // error
Copy the code
Alternatively, it can use an object with get and set functions to create a writable ref object.
const count = ref(1)
const plusOne = computed({
get: () = > count.value + 1.set: val= > {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) / / 0
Copy the code
watchEffect
To automatically apply and reapply side effects based on reactive state, we can use the watchEffect method. It executes a function passed in immediately, tracing its dependencies responsively, and rerunking the function when its dependencies change.
const count = ref(0)
watchEffect(() = > console.log(count.value))
// -> logs 0
setTimeout(() = > {
count.value++
// -> logs 1
}, 100)
Copy the code
Stop listening
When watchEffect is called on a component’s setup() function or lifecycle hook, the listener is linked to the component’s lifecycle and stops automatically when the component is uninstalled.
In some cases, it is also possible to explicitly call the return value to stop listening:
const stop = watchEffect(() = > {
/ *... * /
})
// later
stop()
Copy the code
Clearance side effect
Sometimes side effects functions perform asynchronous side effects, and these responses need to be cleared when they fail (that is, the state has changed before completion). So a function that listens for incoming side effects can take an onInvalidate function as an input parameter to register a callback in the event of a cleanup failure. This invalidation callback is triggered when:
- When the side effect is about to be re-executed
- The listener is stopped (if in
setup()
或Lifecycle hook
FunctionwatchEffect
When the component is uninstalled.)
watchEffect(onInvalidate= > {
const token = performAsyncOperation(id.value)
onInvalidate(() = > {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
Copy the code
The reason we register the invalidation callback by passing in a function rather than returning it from the callback is because the return value is important for asynchronous error handling.
When performing a data request, the side effect function is usually an asynchronous function:
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() = > { / *... * / }) // We registered the cleanup function before the Promise resolution
data.value = await fetchData(props.id)
})
Copy the code
We know that asynchronous functions implicitly return a Promise, but the cleanup function must be registered before the Promise can be resolved. In addition, Vue relies on this returned Promise to automatically handle potential errors in the Promise chain.
Side effect refresh time
Vue’s responsive system caches side effects functions and refreshes them asynchronously to avoid unnecessary repeated calls due to multiple state changes in the same “tick.” In the concrete implementation of the core, the component’s update function is also a monitored side effect. When a user-defined side effect function is queued, by default it is executed before all component updates:
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
const count = ref(0)
watchEffect(() = > {
console.log(count.value)
})
return {
count
}
}
}
</script>
Copy the code
In this example:
- Count is printed synchronously at initial run time
- When count is changed, side effects are performed before component updates.
If you need to re-run the listener side effect after a component update (e.g., when with a template reference), you can pass the additional options object with flush option (default ‘pre’) :
// Emitted after the component is updated so you can access the updated DOM.
// Note: This will also delay the initial running of side effects until the first rendering of the component is complete.
watchEffect(
() = > {
/ *... * /
},
{
flush: 'post'})Copy the code
The Flush option also accepts sync, which forces the effect to always fire synchronously. However, this is inefficient and should rarely be needed.
Listener debugging
The onTrack and onTrigger options can be used to debug the listener’s behavior.
onTrack
Will be in responsive formproperty
或ref
Called when traced as a dependency.onTrigger
Is called when a dependency change causes a side effect to be triggered.
Both callbacks will receive a debugger event that contains information about the dependencies. It is recommended to write debugger statements in the following callbacks to check dependencies:
watchEffect(
() = > {
/* Side effects */
},
{
onTrigger(e) {
debugger}})Copy the code
OnTrack and onTrigger only work in development mode.
watch
The Watch API is exactly equivalent to a component listener property. Watch needs to listen for specific data sources and perform side effects in callback functions. By default, it is also lazy, meaning that callbacks are executed only when the source being listened to changes.
In contrast to watchEffect, Watch allows us to:
- Lazy execution side effects;
- Be more specific about what state should trigger the listener to restart;
- Access values before and after listening state changes.
Listening to a single data source
The listener data source can be a getter function that returns a value, or it can be directly ref:
// Listen for a getter
const state = reactive({ count: 0 })
watch(
() = > state.count,
(count, prevCount) = > {
/ *... * /})// listen on ref directly
const count = ref(0)
watch(count, (count, prevCount) = > {
/ *... * /
})
Copy the code
Listening to multiple data sources
Listeners can also listen on multiple sources simultaneously using arrays:
const firstName = ref(' ');
const lastName = ref(' ');
watch([firstName, lastName], (newValues, prevValues) = > {
console.log(newValues, prevValues);
})
firstName.value = "John"; // logs: ["John",""] ["", ""]
lastName.value = "Smith"; // logs: ["John", "Smith"] ["John", ""]
Copy the code
Listen for reactive objects
Use listeners to compare the values of an array or object that are reactive and require it to have a copy of the values.
const numbers = reactive([1.2.3.4])
watch(
() = > [...numbers],
(numbers, prevNumbers) = > {
console.log(numbers, prevNumbers);
})
numbers.push(5) / / logs: [1, 2, 3, 4, 5] [1, 2, 3, 4]
Copy the code
When trying to check for property changes in deeply nested objects or arrays, the deep option still needs to be set to true.
const state = reactive({
id: 1.attributes: {
name: "",}}); watch(() = > state,
(state, prevState) = > {
console.log(
"not deep ", state.attributes.name, prevState.attributes.name ); }); watch(() = > state,
(state, prevState) = > {
console.log(
"deep ",
state.attributes.name,
prevState.attributes.name
);
},
{ deep: true}); state.attributes.name ="Alex"; // 日志: "deep " "Alex" "Alex"
Copy the code
However, listening for a reactive object or array always returns the current value of the object and a reference to the previous state value. To fully listen for deeply nested objects and arrays, you may need to make a deep copy of the values. This can be done with utilities such as Lodash.clonedeep.
import _ from 'lodash';
const state = reactive({
id: 1.attributes: {
name: "",}}); watch(() = > _.cloneDeep(state),
(state, prevState) = > {
console.log( state.attributes.name, prevState.attributes.name ); }); state.attributes.name ="Alex"; // log: "Alex" ""
Copy the code
Behavior shared with watchEffect
Watch shares with watchEffect stopping listening, clearing side effects (onInvalidate is passed in as the third argument to the callback accordingly), side refresh timing, and listener debugging behavior.
Vue 3.x bidirectional binding principle of an implementation
An example of implementation
Defining a constructor
function Vue(option){
this.$el = document.querySelector(option.el); // Get the mounted node
this.$data = option.data;
this.$methods = option.methods;
this.deps = {}; MSG: [subscriber 1, subscriber 2, subscriber 3], info: [subscriber 1, subscriber 2]}
this.observer(this.$data); // Call the observer
this.compile(this.$el); // Call the instruction parser
}
Copy the code
Define an instruction parser
Vue.prototype.compile = function (el) {
let nodes = el.children; // Get the child node of the mounted node
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.children.length) {
this.compile(node) // Get the children recursively
}
if (node.hasAttribute('l-model')) { // When the child node has an L-model instruction
let attrVal = node.getAttribute('l-model'); // Get the attribute value
node.addEventListener('input', (() = > {
this.deps[attrVal].push(new Watcher(node, "value".this, attrVal)); // Add a subscriber
let thisNode = node;
return () = > {
this.$data[attrVal] = thisNode.value // Update the data layer}}}) ())if (node.hasAttribute('l-html')) {
let attrVal = node.getAttribute('l-html'); // Get the attribute value
this.deps[attrVal].push(new Watcher(node, "innerHTML".this, attrVal)); // Add a subscriber
}
if (node.innerHTML.match(/ {{(+) [^ \ {| \}]}} /)) {
let attrVal = node.innerHTML.replace(/[{{|}}]/g.' '); // Get the content of the interpolation
this.deps[attrVal].push(new Watcher(node, "innerHTML".this, attrVal)); // Add a subscriber
}
if (node.hasAttribute('l-on:click')) {
let attrVal = node.getAttribute('l-on:click'); // Gets the name of the method that the event triggers
node.addEventListener('click'.this.$methods[attrVal].bind(this.$data)); // point this to this.$data}}}Copy the code
Defining the observer
Vue.prototype.observer = function (data) {
const that = this;
for(var key in data){
that.deps[key] = []; {MSG: [subscriber], info: []}
}
let handler = {
get(target, property) {
return target[property];
},
set(target, key, value) {
let res = Reflect.set(target, key, value);
var watchers = that.deps[key];
watchers.map(item= > {
item.update();
});
returnres; }}this.$data = new Proxy(data, handler);
}
Copy the code
Define subscribers
function Watcher(el, attr, vm, attrVal) {
this.el = el;
this.attr = attr;
this.vm = vm;
this.val = attrVal;
this.update(); // Update the view
}
Copy the code
Update the view
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.val]
}
Copy the code
The above code is defined in a vue.js file and can be introduced wherever bidirectional binding is required.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<! -- MVVM bidirectional binding is implemented in the way of data hijacking combined with publiser-subscriber mode. The setter and getter of each attribute is hijacked through Object.defineProperty(), and the message is published to the subscriber when the data changes, triggering the corresponding listener callback. The following points must be achieved: 1, implement a data listener Observer, can listen to all attributes of the data object, if there is any change can get the latest value and notify the subscriber 2, implement an instruction parser Compile, scan and parse the instructions of each element node, replace the data according to the instruction template. 3, implement a Watcher that serves as a bridge between the Observer and Compile, subscribe to and receive notification of each property change, execute the corresponding callback function bound by the instruction, and update the view 4, integrate the above three functions -->
<div id="app">
<input type="text" l-model="msg" >
<p l-html="msg"></p>
<input type="text" l-model="info" >
<p l-html="info"></p>
<button l-on:click="clickMe">Am I</button>
<p>{{msg}}</p>
</div>
<script>
var vm = new Vue({
el: "#app".data: {
msg: "Kung Hei Fat Choi".info: "Study hard and make progress every day."
},
methods: {
clickMe(){
this.msg = "I love coding."; }}})</script>
</body>
</html>
Copy the code
Vue is not officially implemented like this, it is just a simple implementation with functionality and performance issues.