Play an advertisement 🤠on a Vue3.0 experience, imitation of a NetEase cloud music client
Why write such an article at this time?
It’s 2021, and several months have passed since Vue3.0 was officially released. However, it is not enough for the framework to just be able to use it. In order to understand its relevant principles, we need to know what the difference is between it and previous versions and what the advantages are. So today we re – product 2. X version of responsive source code!
Each of the following stages may have some methods or object instances temporarily unable to understand, it doesn’t matter, when you read the complete process of the article, hand knock again, you will suddenly understand ðŸ¤
The function and relationship of Observer, Dep and Watcher
Those of you who have used Vue more or less know that Utah uses a data hijacking and subscription-publish pattern to achieve responsiveness, which is dependent on the following three objects.
Observer
Observer listeners are used to hijack responsive objects by adding getters and setters, and are used as an intermediate station for Dep and Watcher to collect and update dependent data. Its related source code and analysis are as follows:
// it is called when the vUE is initialized
function initData(vm) {
let data = vm.$options.data
// This assumes that data is a function that returns an object
data = vm._data = data.call(vm, vm)
const keys = Object.keys(data);
// Delegate _data to this
for (let i = 0; i < keys.length; i++) {
proxy(vm, '_data', keys[i]);
}
// If this is the case, the props and methods in option are the same as each other
observe(data, true)}function observe(value, asRootData) {
// If it is not an object or a virtual DOM node, no observation is required
if(! isObject(value) || valueinstanceof VNode) {
return
}
let obj
// If it is already monitored, there is no need to instantiate it
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
obj = value.__ob__
} else {
obj = new Observer(value)
}
return obj
}
// Property proxy method
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable:!!!!! enumerable,writable: true.configurable: true})}class Observer {
constructor(value) {
this.value = value
// Here deP is for array dependency collection
this.dep = new Dep()
// The def proxy's __ob__ is important because the array uses the proxy's observer object
def(value, '__ob__'.this)
// Extra processing is required if the value is an array, because Object.defineProperty cannot listen for any operations that change the length of the array, as well as methods on the prototype.
// This is why Utah doesn't handle arrays, instead using $set and proxy variants
if (Array.isArray(value)) {
// Proxy array variation method, reassign the prototype
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
// Otherwise, the object is iterated over, hijacking its properties
this.walk(value)
}
}
// Listen to the object
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
Arr [I].xxxx = XXX; arr[I] = XXX; arr[I] = XXX
observeArray(array) {
for (let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
Copy the code
Observe that the observe method value is data or its child object, and then instantiate Obverver to treat the value as an object or an array.
Object to hijack
Object walks through the Object’s keys, passing each key along with the original OBJ to the defineReactive method, which calls Object.defineProperty. The method analysis is as follows:
/** * Each key corresponds to a dep. Obj is the object we need to hijack, such as the initial data, and the object val in the data is manually set. By default, it is not required **/
function defineReactive(obj, key, val) {
const dep = new Dep()
// Get the description object of the object attribute
const property = Object.getOwnPropertyDescriptor(obj, key)
// If the value of the object has been set previously and is set to an unchangeable value, no listening is required
if (property && property.configurable === false) {
return
}
// If the property of the original object defines the corresponding GET and set, it should be consistent with them
const getter = property && property.get
const setter = property && property.set
// In the case of non-set, get is not defined, i.e. val is undefined and needs to be given its original value
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// Recursively collect dependencies
let ob = observe(val)
Object.defineProperty(obj, key, {
// Can be traversed, can be modified
enumerable: true.configurable: true.get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// Dep is a dependency collector, as described later. For now, just know that Dep. Target is a Watcher instance
if (Dep.target) {
// Rely on collection
dep.depend()
/ / if val object or array, ob is observe the object, or undefined
Ob.dep. depend for array method dependency collection
if (ob) {
ob.dep.depend()
if (Array.isArray(value)) {
Ob.dep. depend depends on the methods of the outermost array
// But consider the case if the value is also an array, i.e., a collection of dependencies of multi-dimensional arrays
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value) return
// If object properties cannot be set
if(getter && ! setter)return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// New data is processed recursively and responsively
ob = observe(newVal)
// Notify data update
dep.notify()
}
})
}
// Dependency collection for multi-dimensional array variation methods
function dependArray(array) {
for (let i = 0; i < array.length; i++) {
const element = array[i];
// __obj__ is what def does
element && element.__ob__ && element.__ob__.dep.depend()
if (Array.isArray(element)) {
dependArray(element)
}
}
}
Copy the code
An array of hijacked
An array is a special case where each value is not handled with Object.defineProperty like an Object, which is why this.a[0] = XXX is invalid. This. A [0]. XXX = XXX is valid because the observe method listens for each value in the array. The operation of adding, deleting, modifying and checking arrays is realized by hijacking the variation methods on their prototypes and calling methods for dependency collection. Its implementation analysis is as follows:
const arrayMethods = Object.create(Array.prototype)
// Array variation method
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
// Here is the mutation method for hijacking
methodsToPatch.forEach((method) = > {
// The original method of the array
const original = arrayMethods[method]
def(arrayMethods, method, function (. args) {
const result = original.apply(this, args)
Def proxies the current obsever instance to value, which in the case of an array is the array itself
const ob = this.__ob__
The value of / / push/unshift/splice
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// The new value needs to be processed in a responsive manner
if (inserted) ob.observeArray(inserted)
// Change notification
ob.dep.notify()
return result
})
})
Copy the code
summary
At this point, aside from the DEP-related code, you should have some idea of how the Observer hijacks data. ObserveArray, defineReactive, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray The object instantiates an Observer instance. It is in this’ recursive ‘way that we listen for the data value we pass in and its children!
Hope you can continue to read, when you finish Watcher and Dep related source code, look back here, there will be a different harvest!
Dep
The dependency collector, which collects the dependencies of reactive objects, instantiates a Dep instance for each reactive object, including its children, in defineReactive for objects and in defineReactive for arrays, which, as mentioned earlier, hijks its mutated methods. The deP of each deP instance is instantiated in an Observer class, and subs of each DEP instance is an array of Watcher instances. When data changes, each Watcher will be notified through DEP. notify.
let depId = 0
class Dep {
constructor() {
this.id = depId++
this.subs = []
}
removeSub(sub) {
// Where sub is watcher
remove(this.subs, sub)
}
addSub(sub) {
// Where sub is watcher
this.subs.push(sub)
}
// The watcher instances depend on each other for collection
depend() {
if (Dep.target) {
Dep.target.addDep(this)}}notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// Update notifications
subs[i].update()
}
}
}
Dep.target = null
function remove(arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)}}}Copy the code
Watcher
The subscriber, or observer, is responsible for our relevant update operations. There are three types of Watcher: renderWatcher (responsible for component rendering), Watcher(calculated Watcher) and Watcher(userWatcher) for watch property. When a responsive object performs an update operation, The setter method is triggered to call dep.notify of its corresponding dependent collector instance and update the collected Watcher array.
class Watcher {
dirty
getter
cb
value
// VM is the vUE instance object
/ / expOrFn for updating method, renderWatcher for our updateComponent update view, userWatcher/computedWatcher for evaluation method
// cb is a callback, mainly used in userWatcher and our watch property
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
this.id = ++wId
this.cb = cb
// Record the last evaluated dependency
this.deps = []
// Record the current evaluated dependency
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)// Optionslazy is the parameter of the computedWatcher, and user is the parameter of the userWatcher
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync }else {
this.deep = this.user = this.lazy = this.sync = false
}
this.dirty = this.lazy
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// If it is watch
this.getter = parsePath(expOrFn)
}
// When renderWatcher and userWatchernew watcher are used, the get method is called
// If computeWatcherThis. lazy is true, the get method will not be called
this.value = this.lazy ?
undefined :
this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
// The getter here is the updateComponent method for renderWatcher, the key for watch, and computed is the method
value = this.getter.call(vm, vm)
} catch (error) {
} finally {
// This is for watch deep
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
// Add yourself to the deP
addDep(dep) {
const id = dep.id
// The two DEPs are used to prevent repeated collection of dependencies
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)}}}// Add all existing dependencies
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
update() {
// For computed data, initializing computedWatcher lazy is true and always is
// So computedWatcher does not execute run, but triggers its computedWatcher when the value of the data attribute in its calculation method changes, setting dirty to true.
// Evaluate gets the latest value because dirty is true. (This is a very clever design, and it doesn't compute the value until it's needed.)
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}// recalculate the value to calculate the method called by the property
evaluate() {
// For computed properties, the get method calls the computed object method it declared in the Vue instance
// For example, computed: {name(){return this.first + thi.second}}, get computes the values of the attributes of the two data objects returned by name.
// Before this, the watcher itself is added to the subs array of the deP of the frist/second responsive data, and changes in the two values trigger changes in the computed property.
// Also, as you will see later, the watcher id is sorted in the update operation, because the computedWatcher is instantiated before the renderWatcher(which is instantiated at mount time), so its value changes before rendering
// That's why the page sees updated values for computed attributes.
this.value = this.get()
this.dirty = false
}
run() {
const value = this.get()
if(value ! = =this.value || isObject(value) || this.deep) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
// Remove useless dependencies
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
// If the current new dependency list does not exist, the old dependency does exist, then the dependency collection needs to be removed
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}// The current dependency list acts as the last dependency list, and then resets the current dependency list
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0}}const targetStack = []
// Depends on the collection array
// dep. target is a Watcher
function pushTarget(target) {
targetStack.push(target)
Dep.target = target
}
function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
Several links to the above are as follows
-
Depend on the collection
One of the concepts that I’ve been talking about, you know, deP collects the Watcher, how do you collect the Watcher? In the Observer, the object will determine whether there is a dep. target during the value operation. In the Watcher, the new Watcher will be called when the Watcher is instantiated. When Wacther is instantiated, it calls the get method based on the watcher type. The GET method assigns the current watcher instance to dep. target, thus collecting the dependencies. Next, let’s analyze when Watcher instantiation takes place.
-
When new Watcher is called
- renderWatcher: be responsible for
data
The update of data to view needs to be updated when every data changes, which must be after the initialization of data, calculation properties and watch, and at the same time must be correctdata
Each attribute of thegetter
The dependency collection is triggered by the action (the value operation is done when the view is rendered), so it is instantiated when the component is mounted.
// Here is a rough implementation of mount and update methods, but it is much more than that const vm = new Vue({ data() { return { name: 'cn'.age: 24.wife: { name: 'csf'.age: 23}}},computed: { husband() { return this.name + this.wife.name } }, watch: { wife: { handler(val, oldVal) { console.log('watch--->',val.name, oldVal.name) }, deep: true.immediate: true}},render() { return ` <h3>normal key</h3> <p>The ${this.name}</p> <p>The ${JSON.stringify(this.wife)}</p> <h3>computed key</h3> <p>The ${this.husband}</p> `; } }).$mount(null.document.getElementById('root')) function mountComponent(vm, el, hydrating) { vm.$el = el; const _updateComponent = function (vm) { vm._update(vm._render(), hydrating) } // That's right, _updateComponent is our view update method // Because lazy is not set, watcher will be called directly when instantiated // _updateComponent -> _render -> render the page new Watcher(vm, _updateComponent, noop, {}, true) return vm } Vue.prototype.$mount = function (el, hydrating) { return mountComponent(this, el, hydrating) }; Vue.prototype._update = function (node, hydrating) { hydrating.innerHTML = node } Copy the code
- ComputedWatcher: Responsible for calculating property changes. The Vue instance is instantiated as soon as it is initialized, before renderWatcher, for reasons explained in the previous comment. Let’s see how this is instantiated.
// initState is called in vue.prototype. _init._init is called when Vue is instantiated function initState(vm) { // destory vm._watchers = []; // The parameters here are our data, computed, watch, render..... const opts = vm.$options if (opts.data) { // Initializing data, that is, responding to data, as we'll talk about in a moment initData(vm) } // This is initComputed if (opts.computed) initComputed(vm, opts.computed) if (opts.watch) { // init our watch here initWatch(vm, opts.watch) } } function initComputed(vm, computed) { // Calculate the wacher object of the property const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] // Because compouted has function form or set/get mode const getter = typeof userDef === 'function' ? userDef : userDef.get // Instantiated here watchers[key] = new Watcher( vm, getter || noop, noop, // Note that lazy initialization is true { lazy: true})if(! (keyin vm)) { defineComputed(vm, key, userDef) } } } function defineComputed(target, key, userDef) { // For now, the default is not server-side rendering // const shouldCache = ! isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = userDef.set || noop } This.com putedxxx will trigger the following computedGetter to perform the value operation Object.defineProperty(target, key, sharedPropertyDefinition) } function createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // Dirty is true at the beginning of new // For the first time, computed values are undefined because dirty is true // There is no need to calculate the value at first (waste), only the evaluate will be recalculated. if (watcher.dirty) { // Call evaluate and set it to false // Unless the dependent data is changed and dirty is set to true, no fetching is required watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } } Copy the code
- UserWatcher: Watch listens for changes. The Vue instance is instantiated as soon as it is initialized, before renderWatcher. This may be a bit convoluted, but you need to look at it a few more times and debug it a few more times. Let’s see how it is instantiated.
stateMixin(Vue) function stateMixin(Vue) { // expOrFn is the key of the value we want to listen for, cb is the callback we pass Vue.prototype.$watch = function(expOrFn, cb, options) { const vm = this options = options || {} // The user in watcher is for watch options.user = true // The watcher get method is called immediately const watcher = new Watcher(vm, expOrFn, cb, options) // If watch is configured to fetch values immediately if (options.immediate) { try { cb.call(vm, watcher.value, null)}catch (error) { } } } } function initWatch(vm, watch) { for (const key in watch) { // This is a callback defined by user const handler = watch[key] createWatcher(vm, key, handler) } } function createWatcher(vm, expOrFn, handler, options) { // For the case where the watch value is an object if(isPlainObject(handler)) { options = handler handler = handler.handler } return vm.$watch(expOrFn, handler, options) } // Let's review the above class Watcher { dirty getter cb value constructor(vm, expOrFn, cb, options, isRenderWatcher) { if (options) { this.user = !! options.user }else { this.deep = this.user = this.lazy = this.sync = false } if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // This is the way to go this.getter = parsePath(expOrFn) } // When new watcher is invoked, the get method is called this.value = this.lazy ? undefined : this.get() } get() { pushTarget(this) let value const vm = this.vm try { // value = this.getter.call(vm, vm) } catch (error) { } finally { // This is for watch deep // If deep is deep, it needs to recursively traverse all subproperties of the data property of watch // Add the current userWatcher to their DEP to make deep changes if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } run() { const value = this.get() if(value ! = =this.value || isObject(value) || this.deep) { const oldValue = this.value this.value = value // Cb is our callback this.cb.call(this.vm, value, oldValue) } } } function parsePath(path) { // select A.B.C from watch const segments = path.split('. '); // Obj will be assigned to the VM, so return key in the data of watch return function (obj) { for (let i = 0; i < segments.length; i++) { if(! obj) {return } obj = obj[segments[i]]; } return obj } } // To prevent repeated dependency on collections const seenObjects = new Set(a)Deep can be implemented by recursively traversing the values of watch's responsive objects that require deep for dependency collection function traverse(val) { _traverse(val,seenObjects) seenObjects.clear() } function _traverse (val, seen) { let i, keys const isA = Array.isArray(val) if((! isA && ! isObject(val))) {return } if (val.__ob__) { const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } } Copy the code
- renderWatcher: be responsible for
-
To summarize
With the complete Watcher object and several Watcher instances analyzed above, you can get a general idea of when and how Watcher binds to our reactive data to publish -> subscribe.
If you read all the previous sections, you should have a general idea of the process, which is as follows
- Data hijacking: traversal
data
If the value of the property is not an array, use theObject.defineProperty
Add a propertygetter
andsetter
, is the array is hijack its mutation method, recursively traverses its child object, traverses the number group, repeat the operation. - Watcher instantiation: different
watcher
Instantiate at different stages, rightdata
Data is selected for dependency collection. - Modify responsive dataTrigger:
setter
, the calldep.notify
, traverses the subs array on its corresponding DEP, and is calledwatcher.update
.
Update strategy
That’s basically it, but there’s still one more thing that’s going to happen to our update operation, watcher.update.
When we go to watcher.update, instead of calling watcher.run, we update the view asynchronously. If we don’t update the view asynchronously, every time we call this. XXX = XXX, the view will update. If we update a lot of data at one time, or we just want the last result, the transformation in the middle is invalid, is there a lot of disadvantages?
So what Utah did was, Watcher saw that there was a change in the data, and it started a queue to buffer all the changes that happened in the same event cycle. If a watcher is triggered more than once, it will only be pushed to the queue once (removing duplicate data, useless arrays, fetching the last time). Then the actual update is fired in the Tick of the next event loop. For details, see vUE asynchronous update mechanism.
let waiting = false
let has = {}
// Cache the watcher array
const queue = []
// Caches watcher in an update
function queueWatcher(watcher) {
const id = watcher.id
if(! has[id]) { has[id] =true
queue.push(watcher)
if(! waiting) { waiting =true
nextTick(flushSchedulerQueue)
}
}
}
let index = 0
The flushScheduleQueue function is used to flushScheduleQueue
// It will fetch all the watcher in the queue and perform the corresponding update
function flushSchedulerQueue() {
flushing = true
let watcher, id
// Watcher sort by order
queue.sort((a, b) = > a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
resetSchedulerState()
}
/ / reset
function resetSchedulerState() {
index = queue.length = 0
has = {}
waiting = flushing = false
}
// Callback is only flushSchedulerQueue
// When we define $nextTick ourselves, we will add it here
const callbacks = []
let pending = false
function nextTick(cb, ctx) {
callbacks.push(() = > {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
}
}
})
// Prevent repeated execution
if(! pending) { pending =true
timerFunc()
}
}
const p = Promise.resolve()
// Here we have default browser support for Promise, whose source code determines a variety of cases
let timerFunc = () = > {
p.then(flushCallbacks)
}
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Copy the code
The last
The content is also quite a lot of round, at the beginning may be quite confused, but the whole process related to all go through, in fact, will happen its design beauty. I feel the comments are relatively complete, empty code is more difficult to fully understand, so I suggest the following demo I hand knock, compared to step by step debugging. Dist /vue.js, dist/vue.js, dist/vue.js. Sorting is not easy, if there is something wrong can be pointed out in the comment area, if you feel helpful, I hope to give three even ðŸ¤!
The demo address