In Vue
, responsiveness refers to the establishment of an automatic association between data and views. When the data changes, the view will automatically update to reflect the changes in the data, without the need for developers to manually operate the DOM. The element updates the view.
Responsiveness is the basis for implementing data-driven views. In Vue
, data can be understood as statestate
, and the view is what the user intuitively sees< strong>PageUI
. The page will dynamically change as the status changes, so the following formula can be obtained:
UI = render(state)
In the above formula: state state
is the input, and the page UI
is the output. Once the state input changes, the page output will also change accordingly. This feature is called Data-driven view.
The formula can be further split into three parts: state
, render()
, and UI
. Among them, state
and UI
are defined by users or implemented by developers, and what remains unchanged is the render()
function. So Vue
plays the role of render()
. When Vue
detects the change of state
, after After a series of processing, changes will eventually be reflected on the UI
interface in a responsive manner.
Implementing data responsiveness requires solving the following three problems:
- How to monitor changes in state
state
? - How to determine which view to update after the state
state
changes? - When is the time to update the view?
1. Monitoring state changes
In Vue2
, data monitoring is implemented with the help of the Object.defineProperty()
function provided by javaScript
.
1. Object.defineProperty()
Object.defineProperty()
Static method is used to define a new property on an object or modify an existing property of the object.
Object.defineProperty(obj, prop, descriptor)
Among them, descriptor
represents the attribute descriptor object
to be defined. There are two types: data descriptor and accessor descriptor< /strong>.
Note: A descriptor can be of only one of these two types, and the two cannot be mixed.
-
A data descriptor is a property with a writable or non-writable value. The object supports four key values:
value
,enumerable
,writable
,configurable
const person = {<!-- -->} Object.defineProperty(person, 'age', {<!-- --> value: 18, //Define attribute value, default is undefined enumerable: true, // Attributes can be enumerated, default false writable: true, // Attribute value can be modified, default false configurable: true, // Attributes can be deleted, default false }) console.log(person.age) // > 18 person.age = 19 console.log(person.age) // > 19
-
Accessor descriptors are properties described by
getter/setter
function pairs and support four key values:enumerable
,configurable
,get
,set
const person = {<!-- -->} let age = 18 Object.defineProperty(person, 'age', {<!-- --> enumerable: true, configurable: true, get() {<!-- --> console.log(`Someone has read person's age, current age ${<!-- -->age}`) return age }, set(value) {<!-- --> console.log(`Someone modified the person's age, the new age is ${<!-- -->value}`) age = value }, }) person.age person.age = 19 age
Output result:
2. Object change monitoring in Vue2.0
Vue2.0
uses the accessor descriptor in the Object.defineProperty
method to hijack data read and write operations. Capture data reading events in getter
, capture data modification events in setter
, and then monitor data changes.
For data of type Object
, Vue
sets each property in the data to the form of getter/setter
through recursive traversal, so that the object Every attribute of becomes observable
The following is a simple implementation of Object
type data monitoring:
-
Simulate a function that updates the view
/** * @description: update view function */ function updateView() {<!-- --> console.log('Received notification, I went to update the view') }
-
Define the
Observer
classIn
vue
, all responsive data are instance objects of theObserver
class./** * @description: Define the Observer class to convert all properties of an object into observable objects * @return {*} */ class Observer {<!-- --> constructor(value) {<!-- --> this.value = value //Add a new __ob__ attribute to value, whose value is the Observer instance of value // This is equivalent to marking the value to indicate that it has been converted into a responsive format to avoid repeated operations. Object.defineProperty(value, '__ob__', {<!-- --> value: this, enumerable: false, writable: true, configurable: true, }) this.walk(value) } walk(value) {<!-- --> const keys = Object.keys(value) for (let i = 0; i < keys.length; i + + ) {<!-- --> defineReactive(value, keys[i]) // Traverse all properties and convert all property values into getter/setter form } } }
-
Define reactive functions
//Source code location:/src/core/observer/index.ts /** * @description: Recursively define responsiveness for the object's properties and set getters/setters so that the read events of the object's properties can be monitored. */ function defineReactive(obj, key, value) {<!-- --> if (arguments.length === 2) {<!-- --> value = obj[key] } if ( // If the value is object or array, call the observer function recursively to implement the responsiveness of value Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value) ) {<!-- --> let childObj = observer(value) // Recursively call the listening function to monitor the child properties } Object.defineProperty(obj, key, {<!-- --> configurable: true, enumerable: true, get() {<!-- --> console.log(`Someone read the ${<!-- -->key} attribute, and the attribute value is ${<!-- -->value}`) // Capture the data reading event return value }, set(newValue) {<!-- --> if (newValue !== value) {<!-- --> console.log( `Someone modified the ${<!-- -->key} attribute, and the modified value is ${<!-- -->newValue}. I want to send a notification to update the view! ` ) // Capture data modification events childObj = observer(newValue) //The updated value must also be monitored value = newValue updateView() // Notify update view } }, }) }
-
Define listening function
/** * @description: listening function */ function observer(value) {<!-- --> if ( // Only data of object or array type needs to be monitored Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value) ) {<!-- --> if (typeof value.__ob__ !== 'undefined') {<!-- --> // value is already responsive data and directly returns the observer instance return value.__ob__ } return new Observer(value) // Construct an observer instance with value as a parameter and return } }
Verify implementation:
const data = {<!-- --> name: 'zhangsan', age: 19, address: {<!-- --> city: 'beijing', country: 'China', }, } const ob = observer(data) console.log(ob)
Output result:
Then perform some operations on the data in data
:
const data = {<!-- --> name: 'zhangsan', age: 19, address: {<!-- --> city: 'beijing', country: 'China', }, } data.name data.name = 'lisi' data.address.city data.address.city = 'shanghai'
Output result:
As you can see, we captured events for reading object properties and modifying object property values. For nested objects, it is also possible to monitor the reading and modification of the property values of the nested objects.
It is worth noting that the modification of the internal property value of the nested object cannot be sensed by the outer object. This is why in the monitoring function watch
, if you want to implement deep monitoring, you must use
Process Summary:
For a piece of data, first call the observer
function to implement monitoring. If the incoming data is an object that has not been monitored, then new
a Observe
observer
instance object corresponding to the data.
In the constructor
of the Observer
class, add a __ob__
attribute to the passed in object, the value of which is the observer
of the object. The code> instance object is used to mark the object to indicate that the object has been converted into a responsive object to avoid repeated operations.
Then call the walk
function to convert each attribute into the form of getter/setter
to monitor data changes.
Finally, in the defineReactive
function, when the incoming attribute value is still an object
, the observer
function is used recursively to monitor the nesting of the object. Object, so that all attributes (including deep attributes) in data
can be converted into the form of getter/setter
to deeply monitor changes in object data.
3. Array change monitoring in Vue2.0
Array
itself is also an object, and also supports using defineProperty
to set getter/setter
for responsive monitoring of elements, but using defineProperty
code>What problems will occur?
Give me an example:
Define the attribute hobbies
of data
as the array ['swimming', 'football']
. When an element is inserted at the front of the array running
, see what happened?
const data = {<!-- --> name: 'zhangsan', age: 19, address: {<!-- --> city: 'beijing', country: 'China', }, hobbies: ['swimming', 'football'], } observer(data) data.hobbies.unshift('running') console.log(data.hobbies)
Output result:
First of all, when an element is inserted into the first position of the array, two view updates are triggered. This is because when monitoring the hobbies
array, the 0
of the array is changed. ,1
The two key
s are monitored separately, so when a value is inserted at the front of the array, all the values corresponding to the original index have changed, so two triggers are triggered. View updates. Although Vue
adopts an asynchronous update strategy, these two updates will be merged and have little impact. But if the amount of data in the array is very large or the elements of the array are deep objects, the performance loss will be huge.
Secondly, we can see that the third element football
of the updated hobbies
is not responsive. This is because the array is calling observer
When the function implements responsive monitoring, there are only two indexes: 0
and 1
, so only these two key
are converted into Listening is performed in the form of getter/setter
, so no matter how many elements are added to the subsequent array, it is no longer responsive.
Therefore, if you use defineProperty
to monitor array elements, you will not be able to monitor changes in new elements. Many API
s that operate on arrays cannot be used, and will trigger multiple This is not a good strategy.
? Just to interrupt, if the element value is modified through the index at this time, it can actually be monitored.
const data = {<!-- --> name: 'zhangsan', age: 19, address: {<!-- --> city: 'beijing', country: 'China', }, hobbies: ['swimming', 'football'], } observer(data) data.hobbies[0] = 'running' console.log(data.hobbies)
Output result:
In Vue2.0
, the author uses seven methods to rewrite the array in place to monitor the array data.
1. Array method interceptor
In Vue2.0
, an array method interceptor is defined, intercepted between the array instance and Array.property
, and the method of operating the array is rewritten inside the interceptor For some methods, when an array instance uses an array operation method, the methods rewritten in the interceptor are used instead of the native methods on Array.prototype
.
//Source code location:/src/core/observer/array.ts const arrayProto = Array.prototype //Create an object as an interceptor export const arrayMethods = Object.create(arrayProto) // 7 ways to change the contents of the array itself const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', ] /** * @description: Define and override 7 methods on the arrayMethods interceptor object * @return {*} */ methodsToPatch.forEach((method) => {<!-- --> const original = arrayProto[method] // cache native method Object.defineProperty(arrayMethods, method, {<!-- --> enumerable: false, writable: true, configurable: true, value: function mutator(...args) {<!-- --> const result = original.apply(this, args) const ob = this.__ob__ // If an element is inserted or updated, monitor the new element responsively let inserted switch (method) {<!-- --> case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // Call the observe function to convert the new elements into responsive console.log( `Someone has called the overridden ${<!-- -->method} method to change the array data, and the view can be notified to be updated` ) updateView() return result }, }) }) const array = [1, 2] Object.setPrototypeOf(array, arrayMethods) array.push(3) console.log(array)
Output result:
2. Use interceptors
Determine the type of value
in the Observer
class. If it is an array, point the __proto__
pointer of value
to the interceptor. At the same time, call the observeArray()
function to monitor value
/** * @description: Define the Observer class to convert all properties of an object into observable objects * @return {*} */ class Observer {<!-- --> constructor(value) {<!-- --> // ... if (Array.isArray(value)) {<!-- --> // setPrototypeOf is a new syntax in es6 // You can also determine whether the browser supports __proto__ syntax. If it supports value.__proto__ = arrayMethods // Otherwise, traverse the methods in arrayMethods and add them to the value object itself one by one. Object.setPrototypeOf(value, arrayMethods) this.observeArray(value) // Convert all elements in the array into detectable responses } else {<!-- --> this.walk(value) } } observeArray(value) {<!-- --> for (let i = 0, l = value.length; i < l; i + + ) {<!-- --> observer(value[i]) } } } const data = {<!-- --> name: 'zhangsan', age: 19, address: {<!-- --> city: 'beijing', country: 'China', }, hobbies: ['swimming', 'football'], } observer(data) data.hobbies.unshift('running') console.log('--------------------------------------------- --------') console.log(data.hobbies) console.log('--------------------------------------------- --------') data.hobbies[0] = 'drawing' console.log('--------------------------------------------- --------') console.log(data.hobbies)
Output result:
The observer
function implements monitoring of seven operation methods of arrays. At this time, modification of array elements through index cannot be monitored, and splice
can be used instead.
At this point, we have implemented data monitoring of objects and arrays, solving the first problem of responsiveness:
- How to change
state
Listen for changes?
So after monitoring the status changes, how does vue
know which views to update?
2. Collection and triggering of dependencies
During the rendering process, the vue
component will call the getter
function of the data to read the data and render it on the page. Then when the data is read, the The caller collects it, and when the data changes, that is, when the setter
function is triggered, all callers are notified to update the view, thus realizing responsive updating of the data.
We call the caller of data a dependency of data.
The core is: Collect dependencies in getter
and trigger dependencies in setter
During the template compilation process, a watcher
instance will be instantiated for each element that calls state
;
During the construction process, this watcher
instance first sets itself to the globally unique specified location window.target
, and then reads the dependent data. Reading data triggers the getter
of the data. In the getter
function, the value of window.target
will be read to obtain the dependencies of the current data. watcher
, then call dep.depend()
to collect this watcher
into the dependency collector dep.subs
Completed dependency collection;
When the data changes, the setter
function of the data will be called. In the setter
, dep.notify()
will be called to notify the collector dep. Each
sends an update notification. After receiving the notification, each watcher
instance in subswatcher
instance will call its own update
The function updates the view, thus completing the triggering of dependencies and achieving data responsiveness.
1. Define Dep class
// Source code location: src/core/observer/dep.js /** * @description: Define Dep class * @return {*} */ class Dep {<!-- --> constructor() {<!-- --> this.subs = [] // Internally stores all watcher instances that observe the dependent data } /** * @description: Notification update function * @return {*} */ notify() {<!-- --> this.subs.forEach((sub) => {<!-- --> sub.update() //update method on watcher instance }) } /** * @description: Dependency collection function * @return {*} */ depend() {<!-- --> if (window.target) {<!-- --> //When the watcher calls the dependency getter, it will place itself on window.target for the dependency collector to obtain, and delete it after reading it. this.addSub(window.target) } } /** * @description: Add a dependency to the subs array * @param {*} sub a watcher instance * @return {*} */ addSub(sub) {<!-- --> this.subs.push(sub) } /** * @description: Remove a dependency from the subs array * @param {*} sub a watcher instance * @return {*} */ removeSub(sub) {<!-- --> if (this.subs.length) {<!-- --> const index = this.subs.indexOf(sub) if (index > -1) {<!-- --> return this.subs.splice(index, 1) } } } }
2. Define Watcher class
// Source code location: src/core/observer/watcher.js /** * @description: Define Watcher class * @return {*} */ class Watcher {<!-- --> constructor(vm, expOrFn, cb) {<!-- --> This.vm = vm this.cb = cb this.getter = parsePath(expOrFn) // Function to get the object attribute value based on expression this.value = this.get() // Directly call and execute during construction } /** * @description: Data acquisition of watcher instance * @return {*} */ get() {<!-- --> window.target = this // Mount itself to window.target first let value = this.getter.call(this.vm) // Read dependency data and trigger dependency collection window.target = null // After the dependencies are collected, window.target will be empty. return value //The value of the obtained data is used for page rendering } /** * @description: View update of watcher instance * @return {*} */ update() {<!-- --> const oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) // Comparison of new and old virtual DOM or user-defined callback function, etc. } } /** * Parse simple path. * Take the value represented by a string path in the form of 'data.a.b.c' from the real data object * For example: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */ function parsePath(path) {<!-- --> const segments = path.split('.') return function (obj) {<!-- --> for (let i = 0, len = segments.length; i < len; i + + ) {<!-- --> if (!obj) return obj = obj[segments[i]] } return obj } }
3. Collect & trigger dependencies
/** * @description: Define responsiveness for the object's properties, which can be monitored for read events of the object's properties. */ function defineReactive(obj, key, value) {<!-- --> // ... let childOb = observer(value) const dep = new Dep() // Instantiate dependency collector Object.defineProperty(obj, key, {<!-- --> get() {<!-- --> dep.depend() // Collect dependencies if(childOb){<!-- --> // If the child element of the current value is responsive, it also needs to collect dependencies. childOb.dep.depend() } return value }, set(newValue) {<!-- --> if (newValue !== value) {<!-- --> chileOb = observer(newValue) //The updated value must also be monitored value = newValue dep.notify() // Trigger dependencies and notify updated views if (childOb) {<!-- --> // If the child element of the current value is responsive, it also needs to be notified and updated. childOb.dep.notify() } } }, }) return dep }
At this point, we have solved the second problem of responsive implementation:
- How to determine
state
Views to be updated after changes?
3. Asynchronous update queue
Vue
executes asynchronously when updating DOM
. As long as it listens to data changes, Vue
will open a queue and buffer all data changes that occur in the same event loop.
If the same watcher
is triggered multiple times in the same eventLoop
, it will only be pushed into the queue once.
Then, in the next event loop tick
, Vue
flushes the queue and performs the actual (deduplicated) work.
Vue
internally attempts to use native Promise.then()
, MutationObserver()
and setImmediate()
for asynchronous queues >, if the execution environment does not support it, setTimeout()
will be used instead.
If you want to operate based on the updated DOM
, you should use the Vue.nextTick()
function. The callback function of this function will be called after the page is refreshed.
The third problem is also solved:
- When is the time to update the view?
The above is the whole process of data-driven view!
4. Method of modifying data responsively
Since Vue2.0
uses Object.defineProperty
provided by javaScript
to implement data monitoring
Disadvantage:
- Object:
- Add a new
key/value
key-value pair - Delete existing
key/value
key-value pairs
- Add a new
- Array:
- Modify array element value by index
- Modify array length
Solution:
-
object
Vue.set(object, key, value)
orvm.$set(object, key, value)
to add and modify propertiesVue.delete(object, key)
orvm.$delete(object, key)
delete attributesobject = Object.assign({}, newObject)
Updates the entire object
-
array
array.splice(index, 1, value)
Vue.set(array, index, value)
orvm.$set(array, index, value)
array = newArray
updates the entire array
Vue3.0
uses the new Proxy
of ES6
for data monitoring, and does not have the above problems.
5. Summary
Vue2.0
adopts data hijacking and message publishing and subscription models to achieve data responsiveness.
When a Vue
instance is created, Vue
will traverse the data in data
and convert them using Object.defineProperty
It is getter/setter
, and internally tracks related dependencies (message subscriptions), captures data access events and collects dependencies, captures data modification events and triggers dependencies.
Each component instance has a corresponding watcher
instance, which records the properties as dependencies during component rendering. When the dependent data's setter
is called, it will Notify watcher
to update the page (message publishing), thereby achieving "data-driven view".