This post was originally posted on github Blog.
This article is analyzed according to Vue source code V2.x. Here only comb the most important part of the source code, skip some non-core parts. Reactive updates mainly involve Watcher, Dep, and Observer classes.
This article is mainly to clarify the following problems that are easy to confuse:
Watcher
.Dep
.Observer
What is the relationship between these classes?Dep
In thesubs
What is stored?Watcher
In thedeps
What is stored?Dep.target
What is it? Where is the value assigned?
This article directly starts from creating a Vue instance, step by step to uncover the responsive principle of Vue, assuming the following simple Vue code:
var vue = new Vue({
el: "#app".data: {
counter: 1
},
watch: {
counter: function(val, oldVal) {
console.log('counter changed... ')}}})Copy the code
1. Initialize the Vue instance
From the life cycle of the Vue, init initialization is performed first, which is in instance/init.js.
src/core/instance/init.js
initLifecycle(vm) // Initialize the vm lifecycle related variables
initEvents(vm) // VM event-related initialization
initRender(vm) // Template parsing related initialization
callHook(vm, 'beforeCreate') // Call the beforeCreate hook function
initInjections(vm) // resolve injections before data/props
initState(vm) // Initialize the vm state.
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // Call the created hook function
Copy the code
InitState (VM), which implements initialization operations for props, methods, data, computed, and watch, is the focus of the study. Here, based on the above examples, we focus on data and Watch. The source code is located in instance/state.js
src/core/instance/state.js
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) // Initialize the VM's data, mainly by setting the corresponding getter/setter methods through the Observer
} else {
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
// Initialize the added watch
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
2. initData
A Vue instance implements getter/setter methods for each of its data, which is the basis for a responsive implementation. See MDN Web docs for getters/setters. Counter = this.counter = this.counter = this.counter = this.counter When changing the value this.counter = 10, you can also customize some actions when setting the value. The implementation of initData(VM) is instance/state.js in the source code.
src/core/instance/state.js
while (i--) {
...
// here we want to delegate all the data on the data, props, and methods to the vue instance
// make vm.counter accessible directly
}
// Skip the previous code and go straight to the core observe method
// observe data
observe(data, true /* asRootData */)
Copy the code
Here the observe() method makes data observable. Why is it observable? The main thing is to implement getter/setter methods so that Watcher can observe changes to the data. Let’s look at the implementation of Observe.
src/core/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( observerState.shouldConvert && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value) // This is where the core of responsiveness lies
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
Here we focus only on the New Observer(Value), which is the heart of the method, using the Observer class to make vue’s data responsive. In our case, the value of the input parameter is {counter: 1}. Let’s look at the Observer class in detail.
3. Observer
First look at the constructor of this class, which is first implemented by the New Observer(Value). The author’s comments state that the Observer Class converts the key value of each target object (that is, the data in data) into getter/setter form for dependency collection and update via dependency notification.
src/core/observer/index.js
/** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__'.this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value) // Iterate over the data object {counter: 1,.. } for each key value (such as counter), set its setter/getter methods.}}... }Copy the code
This. Walk (value) ¶ This. ObserveArray (value) ¶
Moving on to the walk() method, it is stated in the comments that all walk() does is walk through the data for each set in the data object, converting it into a setter/getter.
/** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
Copy the code
The defineReactive() method is the final way to convert the corresponding data into a getter/setter. It is also easy to know from the method name that the method is defined to be responsive. Combining with the original example, the call here is defineReactive(…). As shown in the figure:
The source code is as follows:
export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
// dep is a dependent instance of the current data
// DeP maintains a list of subs that hold observers (or subscribers) that depend on the current data (in this case, the current data is counter). The observer is the Watcher instance.
const dep = new Dep() ---------------(1)
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
letchildOb = ! shallow && observe(val)// Define getter and setter
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Dependency collection is done before fetching the value, if dep.target has a value.
if (Dep.target) { -----------------(2)
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// Depends on the value returned after collection
return value
},
...
}
Copy the code
Let’s start with the getter method, which has two important things.
- Declare one for each data
dep
Instance object, followed bydep
Is referenced to the closure by the corresponding data. For example, every timecounter
Its DEP instance is accessible when it is set or modified and does not disappear. - According to the
Dep.target
To determine whether to collect dependencies, or a common value. hereDep.target
And we’ll do that later, but we know that this is the case.
Then look at the setter method, source code is as follows:
set: function reactiveSetter (newVal) {
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()
}
// Change the value of the data
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal)// The most important step is to notify the observer through the DEP instance that my data is updated
dep.notify()
}
Copy the code
Initialization of Vue instance data is now complete. The following is a review of initData:
The next step is to initialize the watch:
src/core/instance/state.js
export function initState (vm: Component) {... if (opts.data) { initData(vm)// Initialize the VM's data, mainly by setting the corresponding getter/setter methods through the Observer
}
// initData(VM) initWatch(..).// Initialize the added watch
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
4. initWatch
Here initWatch(VM, opts.watch) corresponds to our example as follows:
InitWatch:
src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
// Handler is a callback function to the observed object
// As in the example counter callback function
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
Copy the code
CreateWatcher (vm, key, handler) createWatcher(VM, key, handler)
function createWatcher (vm: Component, keyOrFn: string | Function, handler: any, options? : Object) {
// Check if it is an object. If it is, fetch the handler method inside the object
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// Check whether handler is a string, if it is a method on the VM instance
// Get this method from vm[handler]
// If handler='sayHello', then handler= vm.sayHello
if (typeof handler === 'string') {
handler = vm[handler]
}
// Finally call $watch(...) on the VM prototype chain. The Watcher () method creates a Watcher instance
return vm.$watch(keyOrFn, handler, options)
}
Copy the code
$watch is a method defined on the Vue prototype chain.
core/instance/state.js
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options? : Object) :Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// Create a Watcher instance object
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
// This method returns a reference to a function that calls the Teardown () method of the Watcher object to remove itself from its registered list (subs).
return function unwatchFn () {
watcher.teardown()
}
}
Copy the code
After a lot of wrapping, you finally see the create Watcher instance object. The Watcher class is explained in detail below.
5. Watcher
According to our example, new Watcher(…) As shown below:
First, execute the Watcher class constructor, source code as follows, omits some code:
core/observer/watcher.js
constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) { ... this.cb = cb// Save the callback function passed in
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = [] // Save the observed data to the current DEP instance object
this.newDeps = [] // The latest DEP instance object to save observed data
this.depIds = new Set(a)this.newDepIds = new Set(a)// parse expression for getter
// Get the get method of the observed object
// For calculating attributes, expOrFn is a function
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// Obtain the expOrFn get method of observed objects using the parsePath method
this.getter = parsePath(expOrFn)
...
}
// Finally, by calling the watcher instance get() method,
// This method is the key to associating watcher instances with observed objects
this.value = this.lazy
? undefined
: this.get()
}
Copy the code
The specific implementation methods of parsePath(expOrFn) are as follows:
core/util/lang.js
/** * Parse simple path. */
const bailRE = /[^\w.$]/ // Matches any string that does not match any combination of words and numbers containing underscores
export function parsePath (path: string) :any {
// Invalid string returns directly
if (bailRE.test(path)) {
return
}
Split ('.') --> ['counter']
const segments = path.split('. ')
// Return a function to this.getter
// So this.getter.call(vm, vm), where vm is the input parameter to the return function obj
// It actually calls the vm instance data, such as vm.counter, which triggers the getter for counter.
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
This neatly returns a method to this.getter, that is:
this.getter = function(obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
Copy the code
Getter is called inside the this.get() method to get the value of the watch object and trigger its dependency collection, in this case counter.
The last step in the Watcher constructor method is to call this.get().
/** * Evaluate the getter, and re-collect dependencies. */
get () {
// This method actually sets dep. target = this
// Set dep. target to the Watcher instance
// Dep.target is a global variable that can be used once the getter method in the observation data is set
pushTarget(this)
let value
const vm = this.vm
try {
// Call the getter method to observe the data
// Make dependencies to collect and obtain values for observed data
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// At this point the observation data dependencies have been collected
/ / reset Dep. Target = null
popTarget()
// Remove the old deps
this.cleanupDeps()
}
return value
}
Copy the code
The key steps have been commented in the above code. Here is an example of the relationship between Observer and Watcher classes:
- Red arrow: Instantiate the Watcher class, calling the Watcher instance
get()
Method and setDep.target
Is the current watcher instance that triggers the watch objectgetter
Methods. - Blue arrow:
counter
The object’sgetter
Method is raised and calleddep.depend()
Do dependency collection and returncounter
The value of the. Depending on the results of the collection:1.counter
Dep instance of the closuresubs
Add watcher instance W1 to watch it;2. The w1deps
To add the observed objectcounter
The closure of the dep. - Orange arrow: When
counter
The value changes after the triggersubs
To observe its W1 executionupdate()
Method, which actually ends up calling w1’s callback function cb.
Other related methods in the Watcher class are more intuitive and will be skipped here. Please refer to the Watcher class source code for details.
6. Dep
The Dep is associated with the Observer and Watcher classes. What is Dep?
Dep is a publishing house, Watcher is a reader, and Observer is a higashino keigo book. Such as reader w1 white night line of tolo keigo interested in (counter) in our example, readers w1 once bought tolo keigo book, then it will automatically in the press of the book instances (Dep) registered fill w1 inside information, once the press had tolo keigo this book the latest news will inform w1 (such as a discount).
Now look at the Dep source code:
core/observer/dep.js
export default class Dep {
statictarget: ? Watcher; id: number; subs:Array<Watcher>;
constructor () {
this.id = uid++
// Saves an array of watcher instances
this.subs = []
}
// Add an observer
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Remove the observer
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// Do dependency collection
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}// Notify the observer that the data has changed
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
The Dep class is relatively simple, and the corresponding method is very intuitive. The most important thing here is to maintain an array subs that stores the observer instance Watcher.
7. To summarize
At this point, the three main classes have been studied, and you are now ready to answer the first few questions of this article.
Q1: What is the relationship between Watcher, Dep and Observer?
A1: Watcher is for the Observer to observe the data encapsulated by the Observer. Dep is the link between Watcher and observation data, and mainly plays a role of relying on collection and notification of update.
Q2: What are subs stored in the Dep?
A2: Subs stores the Watcher instance of the observer.
Q3: What does DEPS store in Watcher?
A3: DEPS stores the DEP instance in the observation data closure.
Q4: What is dep. target and where is the value assigned?
Target is a global variable that holds the current watcher instance and is assigned to the current watcher instance when new Watcher() is created.
8. Extension
Here’s an example of calculating a property:
var vue = new Vue({
el: "#app".data: {
counter: 1
},
computed: {
result: function() {
return 'The result is :' + this.counter + 1; }}})Copy the code
The value of result here is dependent on the value of counter, which can better reflect the responsive calculation of Vue. Computed properties are initialized using initComputed(VM, opts.computed), and if you follow the source code, you can see that there is also a Watcher instance created:
core/instance/state.js
watchers[key] = new Watcher(
vm, // The current vUE instance
getter || noop, Function (){return 'The result is :' + this.counter + 1; }
noop, // noop is defined as an empty method, where no callback function is replaced by noop
computedWatcherOptions // { lazy: true }
)
Copy the code
The schematic diagram is as follows:
The result property is evaluated here because it depends on this.counter, so set a watcher to look at the value of result. Then compute attributes are defined through definedComputed(VM, key, userDef). When result is computed, the getter for this.counter is fired, which makes the value of result depend on the value of this.counter.
Finally, the result calculation property is defined with its setter/getter property: Object.defineProperty(Target, Key, sharedPropertyDefinition). See the source code for more details.
9. The reference
- Vue official document
- Vue source
- Vue source code analysis