Before introducing the reactive principle, let’s look at how to detect Object changes. There are currently two ways to detect Object changes: Object.defineProperty and ES6 Proxy. In the Vue2.0 stage, the browser support for Proxy was not ideal, so 2.0 was implemented based on object.defineproperty. This article also introduces how to implement responsivity based on Object.defineProperty. In the next article, we will also introduce how to implement responsivity based on Proxy.
Basic knowledge of
In the process of parsing the source code, the Object. DefineProperty, observer mode and pointcut are used to parse how vUE implements bidirectional binding, and data changes drive view updates.
Object.defineProperty
Object.defineproperty is a new Object method added in ES5 that directly defines a new property on an Object or modifies an existing property of an Object and returns the Object.
ECMAScript has two types of properties: data properties and accessor properties
- Data attributes include [[Video]], [[Enumerable]], [[Writable]], [[Value]];
- Accessor properties contain a pair of set and GET functions. When a accessor property is read, a getter function is called, which returns a valid value. When a accessor property is written, a setter function is called and a new value is passed in. This function is responsible for deciding how to handle the accessor properties, which contain [[Video]], [[Enumerable]], [[Get]], [[Set]].
var obj = {};
var a;
Object.defineProperty(obj, 'a', {
get: function() {
console.log('get val');  return a;
},
set: function(newVal) {
console.log('set val:'+ newVal); a = newVal; }}); obj.a;// get val
obj.a = '111'; // set val: 111
Copy the code
Object. DefineProperty in the example code converts the A property of OBj into getter and setter, which can realize the data monitoring of OBJ. Vue formally implements responsiveness based on this feature. Vue iterates through all of the Object’s properties and converts them into getters/setters using Object.defineProperty.
Observer model
Vue is based on the observer mode to implement data updates and then trigger a series of dependencies to automatically update the view. The observer mode is an object that maintains a series of dependent objects and automatically notifies them of state changes. The basic elements of the observer model
- Subject
- An Observer
{% img /images/vue/observer. PNG “click to view the cheat sheet :vi/vim-cheat sheet” %}
Define a container to collect all dependencies
// Target class
class Subject {
constructor() {
this.observers = []
}
/ / add
add(observer) {
this.observers.push(observer)
}
/ / delete
remove(observer) {
let idx = this.observers.find(observer)
idx > - 1 && this.observers.splice(idx,1)}/ / notice
notify() {
for(let oberver of this.observers) {
observer.update()
}
}
}
// Observer class
class Observer{
constructor(name) {
this.name = name
}
update() {
console.log('Target notified me of the update. I amThe ${this.name}`)}}Copy the code
The source code parsing
Overall overview
Let’s enter the vUE source code to start parsing how vUE is responsive.
Vue does a series of init operations during initialization, and we’ll focus on converting data into responsive data. Parsing the source step by step, in the init.js file, Observe observe(data, true /* asRootData */). Observe observe(data, true /* asRootData */).
Object.defineproperty () cannot detect array changes due to javascript limitations. Vue implements arrays and objects in two different ways. For Object types, it hijacks getters and setters to detect changes. In the case of arrays, intercepting array-related apis (push, POP, Shift, unshift…) via interceptors To monitor change.
// instance/observer
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'? getData(data, vm) : data || {} ... Omit some code// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
constmethods = vm.$options.methods ... Omit some code// observe data
observe(data, true /* asRootData */)}Copy the code
// observe/index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) { // If it is a basic type or virtual node
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__'.this)
// score groups and objects separately
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods) // Add interceptor
} else {
copyAugment(value, arrayMethods, arrayKeys) // Add interceptor
}
this.observeArray(value) // Convert the array to responsivity
} else {
this.walk(value) // Convert the object to reactive form}}}Copy the code
Data is of type Object
{% img/images/vue/observer1 PNG “click to view a larger version: vi/vim – cheat – sheet” %}
// observe/index.js
// Loop over each key, hijacking add getter setter
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
const dep = new Dep() // Dep corresponds to Subject in observer mode, where the user collects user dependencies and sends notifications.// Omit some code
letchildOb = ! shallow && observe(val)// Recursively each can convert data to observe
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// Add accessor property get to the data
// When to trigger get? When the page component is in the "mount" stage, the render page will be called. During rendering, data will be obtained and reactiveGetter will be automatically triggered.
// Dep.target is what? By looking at lifecycle. Js in the mountComponent phase will new the Watcher and point the global dep. target to that Watcher
// dep.denpend() does what? The Watcher is added to the SUBs queue of the DEP
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend() // Collect dependencies
if (Array.isArray(value)) {
dependArray(value) // Collect dependencies}}}return value
},
// Add accessor attribute set to data
// When to trigger set? When the value corresponding to the key changes, the reactiveSetter call is automatically triggered and the notify notification is executed
// What does notify do? Iterate through subs (Watcher), perform update in Watcher, and add Watcher to the queue to be updated
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
... // Omit some code
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Call deP notification to store all dependencies after data update}})}Copy the code
Dep (Target: Subject) defineReactive uses an important object Dep, so what does Dep do? Dep is a target object that manages Watcher (adding Watcher, deleting Watcher, adding itself to Watcher’s DEPS queue, notifying every Watcher it manages to update)
export default class Dep {
statictarget: ? Watcher; id: number; subs:Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
... // Omit some code
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
Watcher is a mediator role that notifies it when data changes, and then notifies the rest of the world. He does all the dirty work
- 1. Collect dependencies
- 2. Responsible for executing CB to update all dependencies
// Watcher.js
export default class Watcher {...// Omit some code
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function
) {
this.vm = vm
... // Omit some code
this.cb = cb
... // Omit some code
this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
... // Omit some code
}
this.value = this.lazy
? undefined
: this.get()
}
/** * Evaluate the getter, and re-collect dependencies. */
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
... // Omit some code
return value
}
/** * Add a dependency to this directive. */
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if( value ! = =this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue) // Perform specific updates
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
this.cb.call(this.vm, value, oldValue) // Perform specific updates
}
}
}
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
Copy the code
Data is an Array type
{% img /images/vue/array. PNG 440 320″ click to view the big picture :vi/vim-cheat-sheet” %} Below will be a step by step to sort out the data structure in the data is array type, Vue source code is how to intercept and convert to response type
// Take the data structure as the column
data: {
array: [1.2.3]}Copy the code
// instance/state.js
/ / the entry. observe(data,true /* asRootData */)...Copy the code
// observer/index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
Convert data to observer, execute Walk (value)
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer {... constructor (value: any) {this.value = value
this.dep = new Dep() // important ! The DEP here is actually used for data collection dependencies that are of type array
this.vmCount = 0
def(value, '__ob__'.this)
if (Array.isArray(value)) {
if (hasProto) { // Check whether the browser supports __proto__
protoAugment(value, arrayMethods) // Use __proto__ to override methods in interceptors directly
} else {
copyAugment(value, arrayMethods, arrayKeys) // Mount methods from interceptors to value via copy
}
this.observeArray(value)
} else {
this.walk(value)
}
}
}
// iterate over each key
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
const dep = new Dep() // Dep corresponds to Subject in observer mode, where the user collects user dependencies and sends notifications.// Omit some code
letchildOb = ! shallow && observe(val)// This is an important step to recursively convert an array to an observer
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// reactiveGetter is triggered when the page retrieves data in the mount phase, adding dependencies to the array
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend() // Array adds collection dependency
if (Array.isArray(value)) {
dependArray(value) // Collect dependencies}}}return value
},
// Array changes do not trigger the set callback here, but actually execute __obj__.dep.notify() in the interceptor (see array.js).
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
... // Omit some code
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Call deP notification to store all dependencies after data update}})}Copy the code
When accessing a method in an array, because of the interceptor added, when accessing a method in an array, a forged method is accessed.
// Interceptor method
// observer/array.js
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
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
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify() // When the array changes, the DEP target is called to inform all dependencies to update
return result
})
})
Copy the code
// observer/index.js
// Override the prototype with __proto__
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// Mount methods from interceptors to value via copy
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
Copy the code
conclusion
How is VUE responsive? The implementation of objects and arrays is slightly different:
- 1. Objects: In the create stage, the data in data will be recursively added get and set accessor attributes. In the mount stage, the page will create a global Watcher, and render will be performed in the mount stage, and the corresponding GET function of the page data will be called. Each data key has a corresponding DEP dependency, which will be added to the subs queue of the current Watcher when dep.depend() is executed. When the page data is updated, the set function is called to perform the notification.
- 2. Array: In the create phase, if it is an array type, add an interceptor to the method that performs the array change, and add get and set accessors to the data. The set function is not triggered when the array changes, and the page executes render in the mount phase, calling the corresponding GET function of the data. Call childobj.dep.depend () to collect watcher,(childobj.dep is what? When initializing data, array is recursively converted to an observer, so childobj.dep refers to array dependencies. After an array data update, the __obj__.dep.notify() execution in the interceptor is notified, and the set is not triggered.
How does the page update render after notification? When the notification is sent, Watcher will be added to the queue and vUE will uniformly schedule the update. Later, VUE will perform patch, compare with virtual DOM, and make an overall update at the level of the current page component.