preface
Vue official interpretation of the responsive principle: in-depth responsive principle
To summarize the official description, it can be divided into the following points:
- Component instances have their own Watcher objects for logging data dependencies
- Each property of data in a component has its own getter and setter methods for collecting dependencies and firing dependencies
- During component rendering, the getter method of the property in Data is called to collect the dependencies into the Watcher object
- A property change in data calls a method in the setter to tell Watcher that a dependency has changed
- Watcher receives the message of the dependency change, rerenders the virtual DOM, and implements the page response
However, the official introduction is only a general process, we still do not know how vue set getter and setter methods for each property of data. What is the difference between the implementation of object properties and array properties? How to implement dependency collection and dependency triggering? To find out, you’ll have to look at a wave of source code. Next, please follow me from vUE source code analysis of vUE responsive principle
— Now I’m going to start my show
Instance initialization phase
Vue source code instance/init.js is the entry to initialization, which is divided into the following steps:
// Initialize the life cycle
initLifecycle(vm)
// Initialize the event
initEvents(vm)
// Initialize render
initRender(vm)
// Trigger the beforeCreate event
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// Initialize state,!! Highlight here!!
initState(vm)
initProvide(vm) // resolve provide after data/props
// Trigger an created event
callHook(vm, 'created')
Copy the code
In the initState() method, props, methods, data, computed, and watcher are initialized. You can see the following code in instance/state.js.
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// Initialize props
if (opts.props) initProps(vm, opts.props)
// Initialize methods
if (opts.methods) initMethods(vm, opts.methods)
// Initialize data!! Highlight again!!
if (opts.data) {
initData(vm)
} else {
// Call the observe _data object even if there is no data
observe(vm._data = {}, true /* asRootData */)}// Initialize computed
if (opts.computed) initComputed(vm, opts.computed)
// Initialize watcher
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
Data is initialized in the highlighted initData() method. The code is still visible in instance/state.js. The initData() method code is as follows (abridged).
/* Initializes data */
function initData (vm: Component) {
// Determine if data is an object
if(! isPlainObject(data)) { ... }// Check whether attributes in data have the same name as methods
if (methods && hasOwn(methods, key)) {
...
}
// Check whether the attribute in data has the same name as props
if (props && hasOwn(props, key)) {
...
}
// Move the attributes in the VM to vm._data
proxy(vm, `_data`, key)
// Invoke the observe data object
observe(data, true /* asRootData */)}Copy the code
The initData() function, in addition to the previous series of data judgments, is the data proxy and the observe method call. Data proxy(VM, ‘_data’, key) is used to proxy vm attributes to vm._data, for example:
// The code is as follows
const per = new VUE({
data: {name: 'summer'.age: 18,}})Copy the code
When we access per.name, we are actually accessing per._data.name and the following observe(data, true /* asRootData */) is the beginning of the response.
summary
The initialization process is summarized as follows
Responsive stage
In observe/index.js, observe is a factory function that generates an observe instance for the object. It is the Observe instance returned by the Observe factory function that turns the object into a responsive object.
Observe constructor
The code for the Observe constructor is as follows (an abridged version).
export class Observer {
constructor (value: any) {
// The object itself
this.value = value
// Rely on the collector
this.dep = new Dep()
this.vmCount = 0
// Add an __ob__ attribute to the object
def(value, '__ob__'.this)
// If the object is array
if (Array.isArray(value)) {
...
} else {
// If the object is of type Object. }}Copy the code
From code analysis, the Observe constructor does three things:
- Add to the object
__ob__
Properties,__ob__
Contains the value data object itself, the DEP dependent collector, and vmCount. After this step, the data changes as follows:
/ / the original data
const data = {
name: 'summer'
}
// Data after change
const data = {
name: 'summer'.__ob__: {
value: data, //data the data itself
dep: new Dep(), // DEP depends on the collector
vmCount: 0}}Copy the code
- If the object type is array, perform the array operation
- If the object type is Object, the object type operation is performed
The data is of type Object
When the data is of type Object, a walk method is called in which all attributes of the data are iterated over and the defineReactive method is called. The defineReactive method code is still in observe/index.js, with the following abridged version:
export function defineReactive (.) {
// DeP stores dependent variables. Each property field has its own DEP, which is used to collect dependencies belonging to that field
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// Cache the original get and set methods
const getter = property && property.get
const setter = property && property.set
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// Create childOb for each attribute and observe recursion for each attribute
letchildOb = ! shallow && observe(val)// Add getter/setter methods to attributes
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {... },set: function reactiveSetter (newVal) {... })}Copy the code
The defineReactive method does a few things:
- Instantiate a DEP dependency collector for each attribute to collect the associated dependencies for that attribute. [Reference through getter, setter]
- Caches the original get and set methods of the property to ensure the normal behavior when rewriting get and set methods later.
- Create childOb for each attribute. It’s a process of observing recursion of attributes and storing the results in childOb. The childOb of an object or array property is
__ob__
, the childOb of other attributes is undefined). [Reference through getter, setter] - Add getter and setter methods to each property in the object.
The data processed by defineReactive changes as follows, each property has its own DEP, childOb, getter, setter, and __ob__ for each property of type object
/ / the original data
const data = {
user: {
name: 'summer'
},
other: '123'
}
// Processed data
const data = {
user: {
name: 'summer',
[name dep,]
[name childOb: undefined]
name getter,// Reference name dep and name childOb
name setter,// Reference name dep and name childOb
__ob__:{user, dep, vmCount}
},
[user dep,]
[user childOb: user.__ob__,]
user getter,// Reference user dep and user childOb
user setter,// Reference user dep and user childOb
other: '123',
[other dep,]
[other childOb: undefined,]
other getter,// Reference other dep and other childOb
other setter,// Reference other dep and other childOb
__ob__:{data, dep, vmCount}
}
Copy the code
The last step in the defineReactive function is to add getter and setter methods to each attribute. So what do getter and setter functions do?
In the getter method:
The internal code of the getter function is as follows:
get: function reactiveGetter () {
// Call the get method of the original property to return the value
const value = getter ? getter.call(obj) : val
// If there are dependencies that need to be collected
if (Dep.target) {
/* Collect the dependencies into the deP of the property */
dep.depend()
if (childOb) {
This dependency is also collected in obj.__ob__.dep for each object
childOb.dep.depend()
DependArray if the attribute is array type
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
Copy the code
The getter method does two main things:
- Call the get method of the original property to return the value
- Collect rely on
- Dep.target represents a dependency, the observer, and in most cases, a dependency function.
- If a dependency exists, it is collected into the DEP dependency collector that relies on that attribute
- If there is a childOb (that is, the property is an object or array), the dependency is collected into childOb, that is
__ob__
Dependency collector__ob__.dep
In, the dependency collector is touched when adding a new attribute to an attribute object using $set or vue. set, that is, vue. set or vue. delete__ob__.dep
Dependency in. - If the property value is an array, the dependArray function is called to collect the dependency for each object element in the array
__ob__.dep
In the. Ensure that nested objects in the array respond properly when $set or vue. set is used. The code is as follows:
/ / data
const data = {
user: [{name: 'summer'}}]// Page display
{{user}}
<Button @click="addAge()">addAge</Button>
// the addAge method adds an age attribute to a nested object in an array
addAge: function(){
this.$set(this.user[0].'age'.18)}Copy the code
/ / dependArray function
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
// Collect dependencies into each child object/array
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
Copy the code
// Converted data
const data = {
user: [{name: 'summer'.__ob__: {user[0], dep, vmCount}
}
__ob__: {user, dep, vmCount}
]
}
Copy the code
DependArray collects user’s dependencies into its internal user[0] object __ob__.dep so that the page responds to addAge changes.
In setter methods:
The internal code of the setter function is as follows:
set: function reactiveSetter (newVal) {
// Set the correct value for the property
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
/* eslint-enable no-self-compare */
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// Create a new childOb for the property because its value has changed, and re-observechildOb = ! shallow && observe(newVal)// Execute all dependencies in the dependency in the set method
dep.notify()
}
})
Copy the code
Setter methods do three main things:
- Set the correct value for the property
- Since the value of the property has changed, create a new childOb for the property and re-observe
- Performs all dependencies in a dependent
Now that we’re done with the data being pure object type, let’s look at the operations where the data is array type.
The data is of array type
Observer /index.js for array:
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
// Intercepts the modify array method
augment(value, arrayMethods, arrayKeys)
// Recursively observe each value in the array
this.observeArray(value)
}
Copy the code
When the data type is array
- Specify constructors for data using the protoAugment method
__proto
ArrayMethods for compatibility if the browser does not support it__proto__
, arrayMethods is used to override all related methods in array data. - Recursively observe each value in the array
ArrayMethods Intercepts methods that modify arrays
ArrayMethods is defined in observe/array.js as follows:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// Modify the array method
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// Intercepts methods that modify an array, triggering all dependencies in __ob__.dep in the array when the modified array method is called
def(arrayMethods, method, function mutator (. args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// Observe the new element using observeArray
if (inserted) ob.observeArray(inserted)
// Triggers all dependencies in __ob__.dep
ob.dep.notify()
return result
})
})
Copy the code
ArrayMethods does the following:
- The methods that need to be intercepted to modify the array are push, POP, Shift, unshift, splice, sort, reverse
- When a new element is added to the array, observe the new element using observeArray
- Intercepts methods that modify an array, firing when the modify array method is called
__ob__.dep
All dependencies of
ObserveArray recursively observes each item in the array
ObserveArray code:
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
Copy the code
In the observeArray method, observe the observeArray recursion for all properties in the array. One problem, however, is that you cannot observe all the non-object primitive types in an array. The first sentence of the observe method is
if(! isObject(value) || valueinstanceof VNode) {
return
}
Copy the code
That is, values in an array that are not of type Object will not be observed if there is data:
const data = {
arr: [{
test: 0
}, 1.2],}Copy the code
Change ARr [0]. Test =3 can trigger a response, change ARR [1]=4 can not trigger a response, because observeArray observation of each item, observe(ARR [0]) one observation one object can be observed. Observe (arr[1]) observe a basic type of data and cannot be observed.
summary
Responsive phase flow chart
Reference article: Lifting the veil on data response systems