In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-02-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article mainly introduces "A brief Analysis of the Observer pattern examples in the Vue responsive data". In the daily operation, I believe that many people have doubts about the observer pattern examples in the Vue response data. The editor consulted all kinds of materials and sorted out the simple and useful operation methods. I hope it will be helpful to answer the doubts of "the Observer pattern examples in the Vue response data". Next, please follow the editor to study!
Initialize Vue instance
When reading the source code, because of the large number of files, complex references often make it difficult for us to grasp the key points, here we need to find an entry file, starting from the Vue constructor, put aside other irrelevant factors, step by step to understand the implementation principle of responsive data. First we find the Vue constructor:
/ / src/core/instance/index.jsfunction Vue (options) {if (process.env.NODE_ENV! = = 'production' & &! (this instanceof Vue)) {warn (' Vue is a constructor and should be called with the `new`keyword')} this._init (options)} / / src/core/instance/init.jsVue.prototype._init = function (options) {. / / a flag to avoid this being observed vm._isVue = true / / merge options / / initialize $options if (options & & options._isComponent) {initInternalComponent (vm) of the vm instance Options)} else {vm.$options = mergeOptions (resolveConstructorOptions (vm.constructor), options | | {}, vm)}... InitLifecycle (vm) / / comb the instance's parent, root, children and refs, and initialize some lifecycle-related instance properties initEvents (vm) / / initialize the instance's listeners initRender (vm) / / initialize slot Vm instance callHook (vm, 'beforeCreate') initInjections (vm) / / resolve injections before data/props initState (vm) initProvide (vm) / / resolve provide after data/props callHook (vm,' created') if (vm.$options.el) {vm.$mount (vm.$options.el) / / Mount components to nodes}}
To make it easier to read, we removed flow type checking and some extraneous code. As you can see, Vue.prototype._init is called when the Vue component is instantiated, while inside the method, the initialization of the data is mainly in initState (initInjections and initProvide here are similar to initProps, which are naturally understood after understanding the principle of initState), so we focus on initState.
/ src/core/instance/state.jsexport function initState (vm) {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)}}
First, a _ watchers array is initialized to store the watcher, and then the initProps, initMethods, initData, initComputed, and initWatch methods are called according to the vm.$options of the instance.
InitProps
Function initProps (vm, propsOptions) {const propsData = vm.$options.propsData | | {} const props = vm._props = {} / / cache prop keys so that future props updates can iterate using Array / / instead of dynamic object key enumeration. Const keys = vm.$options._propKeys = [] const isRoot =! vm.$parent / / root instance props should be converted if (! isRoot) {toggleObserving (false)} for (const key in propsOptions) {keys.push (key) const value = validateProp (key, propsOptions, propsData, vm). DefineReactive (props, key, value) if (! (key in vm)) {proxy (vm,'_ props', key)}} toggleObserving (true)}
Here, vm.$options.propsData is the data object passed to the child component instance through the parent component, such as {item: false} in, and then initializes vm._props and vm.$options._propKeys to store the instance's props data and keys, respectively. Because the child component uses the data in _ props referenced by proxy instead of the propsData passed by the parent component, _ propKeys is cached here to update vm._props when updateChildComponent. Then determine whether toggleObserving (false) needs to be called based on whether isRoot is the root component, which is a global switch to control whether the _ _ ob__ attribute needs to be added to the object. I believe we are not unfamiliar with this. The data and other data of general components all contain this attribute. Let's not delve into it here and explain it with defineReactive later. Because props is data passed from parent to child, _ _ ob__ has been added to the parent element initState, so this global switch is turned off when the root component is not instantiated, and is turned on through toggleObserving (true) before the call ends.
Then there is a for loop that sets the vm._props according to the propsOptions object defined in the component, where the propsOptions is what we often write
Export default {... Props: {item: {type: Object, default: () = > ({})}}
Circulatory body, first of all
Const value = validateProp (key, propsOptions, propsData, vm)
The validateProp method mainly verifies whether the data conforms to our defined type, and if key is not found in propsData, get the default value and define _ _ ob__ on the object, and finally return the corresponding value. Do not expand here.
Here, let's skip defineReactive first and look at the end.
If (! (key in vm)) {proxy (vm,'_ props', key)}
Where the proxy method:
Function proxy (target, sourceKey, key) {sharedPropertyDefinition.get = function proxyGetter () {return this [sourceKey] [key]} sharedPropertyDefinition.set = function proxySetter (val) {this [sourceKey] [key] = val} Object.defineProperty (target, key, sharedPropertyDefinition)}
When the key attribute does not exist in vm, Object.defineProperty enables us to access vm._ props [key] through vm [key].
DefineReactive
In initProps, we know that first, according to the user-defined vm.$options.props object, it validates the value object vm.$options.propsData set by the parent component, returns the valid value and saves it to vm._props, and saves the corresponding key to vm.$options._propKeys to update the props data of the child component. Finally, it uses the getter/setter accessor property to point the vm [key] to the operation of vm._ propsKeys. But the most important defineReactive has been skipped, and now we'll read the defineReactive source code to understand the implementation behind responsive data.
/ / src/core/observer/index.jsexport function defineReactive (obj, key, val, customSetter, shallow) {const dep = new Dep () 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 if ((! getter | | setter) & & arguments.length = 2) {val = obj [key]} let childOb =! shallow & & observe (val).}
First, const dep = new Dep () instantiates a dep, where closures are used to define a dependency that corresponds to a specific key. Because it rewrites the getter/setter of target [key] by Object.defineProperty to achieve the responsiveness of data, you need to determine the configurable property of the object key first. Then
If ((! getter | | setter) & & arguments.length = 2) {val = obj [key]}
Arguments.length = 2 means that the val value is not passed when calling defineReactive, when val is undefined, and! getter | | setter judgment condition means that if getter exists in property and setter does not exist, the data object of key will not be obtained, and val is undefined, and then it will not be viewed in depth when observe is called. As in the following setter accessor:
If (getter & &! setter) return
At this point, the data will be read-only, and since it is read-only, there is no data modification problem, and there is no need to observe the data in depth in order to invoke the observer registration method when the data changes.
Observe
In defineReactive, we first get the descriptor of target [key], cache the corresponding getter and setter, and then choose whether to get the val corresponding to target [key] according to the judgment.
Let childOb =! shallow & & observe (val)
To determine whether to call observe based on the shallow flag, let's take a look at the observe function:
/ / src/core/observer/index.jsexport function observe (value, asRootData) {if (! isObject (value) | | value instanceof VNode) {return} let ob 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}
First, determine whether the data you need to observe is an object in order to define the _ _ ob__ attribute through Object.defineProperty, and you need instances whose value is not part of VNode (VNode instances are compared and updated through Diff patch algorithm). Then determine whether the value already has _ _ ob__, and if not, make a subsequent judgment:
ShouldObserve: the global switch flag, which is modified by toggleObserving.
! isServerRendering (): determines whether to render on the server side.
(Array.isArray (value) | | isPlainObject (value)): array and pure objects are only allowed to add _ _ ob__ for observation.
Object.isExtensible (value): determines whether value is extensible.
! value._isVue: prevent Vue instances from being observed
Only when the above five conditions are met will ob = new Observer (value) be called. Next we will take a look at what has been done in the Observer class.
/ / src/core/observer/index.jsexport class Observer {constructor (value) {this.value = value this.dep = new Dep () this.vmCount = 0 def (value,'_ ob__', this) if (Array.isArray (value)) {if (hasProto) {protoAugment (value, arrayMethods)} else {copyAugment (value, arrayMethods) ArrayKeys)} this.observeArray (value)} else {this.walk (value)}} / * * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. * / walk (obj) {const keys = Object.keys (obj) for (let I = 0; I < keys.length; iTunes +) {defineReactive (obj, keys [I])}} / * * Observe a list of Array items. * / observeArray (items) {for (let I = 0, l = items.length; I < l; items +) {observe (items [I])}
Initialize the value, dep, and vmCount properties in the constructor, add a _ _ ob__ object to the this.value and point to yourself, that is, value.__ob__.value = = value, so that you can get dep and value through the value or _ _ ob__ object. The function of vmCount is mainly used to distinguish whether it is the root data of a Vue instance. The role of dep is not described here, but will be explained together with the dep in getter/setter.
Then, according to whether the value is an array or a pure object, the corresponding methods are called respectively to recursively operate on the value. When value is a pure object, the walk method is called and defineReactive is called recursively. When value is an array type, first determine whether there is a _ _ proto__, use _ _ proto__ to implement prototype chain inheritance, otherwise use Object.defineProperty to implement copy inheritance. The inherited base class arrayMethods comes from src/core/observer/array.js:
/ src/core/observer/array.jsconst arrayProto = Array.prototypeexport const arrayMethods = Object.create (arrayProto) 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 () return result})})
Why override the instance method of the array here? The methodsToPatch methods in the code do not return a new array, so the setter cannot be triggered, so the observer's method is not called. Therefore, these mutation methods are rewritten, so that when called, observeArray is used to add _ _ ob__ to the newly inserted array elements, and the corresponding observer can be manually informed by ob.dep.notify to perform the registration method, so as to achieve the response of array elements.
If (asRootData & & ob) {ob.vmCount++}
Finally, the if judgment is added to execute ob.vmCount++ on the root data object of the Vue instance. Here, the main purpose is to distinguish whether it is the root data according to ob.vmCount, and thus execute Vue.set and Vue.delete on it.
Getter/setter
After a recursive operation on the val (if necessary), the data object of the obj [key] is encapsulated as an observer so that it can be observed by the observer and call the observer's method when needed. Here, the accessor attribute of obj [key] is rewritten through Object.defineProperty, and the getter/setter operation is intercepted. The remaining code of defineReactive is as follows:
... Object.defineProperty (obj, key, {enumerable: true, configurable: true, get: function reactiveGetter () {const value = getter? Getter.call (obj): val if (Dep.target) {dep.depend () if (childOb) {childOb.dep.depend () if (Array.isArray (value)) {dependArray (value)}} return value}, set: function reactiveSetter (newVal) {... ChildOb =! shallow & & observe (newVal) dep.notify ()})
First of all, when getter is called, determine whether Dep.target exists or not, and if so, call dep.depend. Let's not delve into Dep.target, just as it is an observer, such as one of our commonly used computational properties, calling dep.depend will store dep in its dependency list as a dependency of the calculated property and register the calculated property with the dep. Why do we need to quote each other here? This is because a target [key] can act as a dependency for multiple observers, while an observer can have multiple dependencies that belong to a many-to-many relationship. In this way, when a dependency changes, we can call their registration methods based on the observers maintained in dep. Now let's go back to Dep:
/ / src/core/observer/dep.jsexport default class Dep {static target:? Watcher; id: number; subs: Array 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 (). For (let I = 0, l = subs.length; I < l; iSuppli +) {subs [I] .update ()}}
In the constructor, first add a self-increasing uid to mark the uniqueness of the dep instance, then initialize an observer list subs, and define the add observer method addSub and remove observer method removeSub. You can see that the depend called in getter adds the current dep instance to the observer's dependency, and the notify called in setter executes the update method registered by each observer, and the Dep.target.addDep method will be explained later in Watcher. To put it simply, when key's getter is triggered, dep dependencies are collected to watcher and Dep.target is added to the current dep's observer list, so that when key's setter is triggered, the observer's update method can be executed through the observer list.
Of course, there are a few lines of code in getter:
If (childOb) {childOb.dep.depend () if (Array.isArray (value)) {dependArray (value)}}
You might wonder, why call childOb.dep.depend when you've already called dep.depend? What is the relationship between the two dep?
In fact, the division of labor between the two dep is different. For the addition and deletion of data, the observer method is called by childOb.dep.notify, while for the modification of data, the dep.notify is used, because the setter accessor cannot listen to the addition and deletion of object data. For example:
Const data = {arr: [{value: 1}],} data.a = 1; / cannot trigger setterdata.arr [1] = {value: 2}; / / cannot trigger setterdata.arr.push ({value: 3}); / / cannot trigger setterdata.arr = [{value: 4}]; / / can trigger setter
Remember the responsive conversion for the array type value in the Observer constructor? By rewriting the value prototype chain, for newly inserted data:
If (inserted) ob.observeArray (inserted) / / notify changeob.dep.notify ()
Convert it to responsive data and call the observer method through ob.dep.notify, and the observer list here is collected through the above childOb.dep.depend. Similarly, in order to achieve the responsiveness of the new data added to the object, we need to provide the corresponding hack method, and this is our commonly used Vue.set/Vue.delete.
/ / src/core/observer/index.jsexport function set (target: Array | Object, key: any, val: any): any {. If (Array.isArray (target) & & isValidArrayIndex (key)) {target.length = Math.max (target.length, key) target.splice (key, 1 Val) return val} if (key in target & &! (key in Object.prototype)) {target [key] = val return val} const ob = (target: any). _ _ ob__ if (target._isVue | | (ob & & ob.vmCount)) {process.env.NODE_ENV! = 'production' & & warn (' Avoid adding reactive properties to a Vue instance or its root $data'+'at runtime-declare it upfront in the data option.') return Val} if (! ob) {target [key] = val return val} defineReactive (ob.value Key, val) ob.dep.notify () return val}
Determine whether the value is an array, and if so, call the splice that has already been hack.
Whether key already exists, if so, it is already responsive, you can modify it directly.
Then determine whether the target.__ob__ exists, and if it does not indicate that the object does not require in-depth observation, set the current value to be returned.
Finally, the new key is set through defineReactive, and ob.dep.notify is called to notify the observer.
Now we know that childOb.dep.depend () is to collect the current watcher into childOb.dep so that it can notify watcher when data is added or deleted. After childOb.dep.depend (), there is:
If (Array.isArray (value)) {dependArray (value)} / * * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. * / function dependArray (value: Array) {for (let e, I = 0, l = value.length; I < l; iSum +) {e = value [I] e & & e.__ob__.dep.depend (e) if (Array.isArray (e)) {dependArray (e)}}
When triggering the getter of target [key], if the type of value is an array, each element of the value will be called _ _ ob__.dep.depend recursively. This is because the getter of the array element cannot be intercepted, so the current watcher is collected into all the _ _ ob__.dep under the array, so that the observer can be notified when one of the elements triggers the add or delete operation. For example:
Const data = {list: [[{value: 0}]],}; data.list [0] .push ({value: 1})
In this way, watcher can only be notified when data.list [0]. _ _ ob__.notify.
The main functions of getter of target [key] are:
Collect Dep.target into the observer list of dep in the closure to notify the observer when the setter of target [key] modifies the data
Add _ _ ob__ to the data traversal according to the situation, and collect the Dep.target into the childOb.dep observer list so that the observer can be notified when the data is added / deleted
The array value is collected recursively through dependArray, and the observer can be notified when the array elements are added, deleted or changed.
The main function of the setter of target [key] is to observe the new data, save it to the childOb variable through the closure for getter to use, and call dep.notify to notify the observer, which is no longer expanded here.
Watcher
In the previous space, we mainly introduced defineReactive to define responsive data: dep and childOb are saved through closures, and observers are collected during getter, so that when the data is modified, dep.notify or childOb.dep.notify can be triggered to call the observer's method to update. But there is not much explanation on how to do the watcher collection, and now we will read Watcher to understand the logic behind the observer.
Function initComputed (vm: Component, computed: Object) {const watchers = vm._computedWatchers = Object.create (null) const isSSR = isServerRendering () for (const key in computed) {const userDef = computed [key] const getter = typeof userDef = = 'function'? UserDef: userDef.get if (! isSSR) {/ / create internal watcher for the computed property. Watchers [key] = new Watcher (vm, getter | | noop, noop, computedWatcherOptions)}...}}
This is the initialization of the Vue calculation property, removing some of the code that does not affect it. First, the object vm._computedWatchers is initialized to store all the calculation attributes, and the isSSR is used to determine whether it is rendered by the server. Then loop through the computed key-value pair we wrote, and instantiate a Watcher for each calculation property if it is not rendered on the server, and save it to the vm._computedWatchers object in the form of a key-value pair. Next we will mainly look at the Watcher class.
Constructor of Watcher
The constructor accepts five parameters, including the current Vue instance vm, the evaluation expression expOrFn (Function or String is supported, and usually Function in the evaluation attribute), and the callback function cb is required. Set this.vm = vm to bind the execution environment of this.getter later, and push this to vm._watchers (vm._watchers is used to maintain all observers in the instance vm). In addition, the value vm._watcher = this is assigned according to whether it is a render observer (the commonly used render is render observer). Then a series of initialization operations are performed according to options. There are several properties:
This.lazy: sets whether to evaluate lazily, which ensures that evaluation can be called only once when multiple people under observation change.
This.dirty: with this.lazy, it is used to mark whether the current observer needs to be re-evaluated.
This.deps, this.newDeps, this.depIds, this.newDepIds: used to maintain a list of observed objects.
This.getter: evaluation function.
This.value: the value returned by the evaluation function is the value in the calculated property.
Evaluation of Watcher
Because the evaluation property is lazy, let's move on to the body of the initComputed loop:
If (! (key in vm)) {defineComputed (vm, key, userDef)}
DefineComputed mainly converts userDef into getter/setter accessor, and sets key to vm through Object.defineProperty, so that we can access computational properties directly through this [key]. Next, let's take a look at the conversion of userDef to the createComputedGetter function in getter:
Function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers & & this._ computed Watchers [key] if (watcher) {if (watcher.dirty) {watcher.evaluate ()} if (Dep.target) {watcher.depend ()} return watcher.value}
Using the key of the closure to save the calculation attributes, when the getter is triggered, we first get the previously saved watcher through this._ computedWatchers [key]. If the watcher.dirty is true, call watcher.evaluate (perform this.get () evaluation operation, and mark the dirty of the current watcher as false), we mainly look at the get operation:
Get () {pushTarget (this) let value const vm = this.vm try {value = this.getter.call (vm, vm)} catch (e) {.} finally {/ / "touch" every property so they are all tracked as / / dependencies for deep watching if (this.deep) {traverse (value)} popTarget () this.cleanupDeps ()} return value}
As you can see, pushTarget (this) is executed first when evaluating. By looking up the src/core/observer/dep.js, we can see:
Dep.target = nullconst targetStack = [] export function pushTarget (target:? Watcher) {targetStack.push (target) Dep.target = target} export function popTarget () {targetStack.pop () Dep.target = targetStack [targetStack.length-1]}
PushTarget mainly stacks watcher instances and assigns values to Dep.target, while popTarget, on the contrary, unstacks watcher instances and assigns the top of the stack to Dep.target. Dep.target, which we have seen before in getter, is actually the observer who is currently being evaluated. Here, the Dep.target is set to watcher before the evaluation, so that the getter accessor is triggered when the data is obtained during the evaluation, thus calling dep.depend, and then performing the addDep operation of watcher:
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)}
First determine whether newDepIds contains dep.id. If not, it means that the dep has not been added. Add dep and dep.id to newDepIds and newDeps respectively. If the depIds does not contain dep.id, this dep has not been added before, because it is added in both directions (adding dep to watcher also needs to collect watcher to dep), so you need to call dep.addSub to add the current watcher to the observer queue of the new dep.
If (this.deep) {traverse (value)}
Then call traverse according to this.deep. The main function of traverse is to recursively traverse the getter that triggers the value, call dep.depend () of all elements, and filter the repeatedly collected dep. Finally, call popTarget () to move the current watcher off the stack and execute cleanupDeps:
CleanupDeps () {let I = this.deps.length while (iMuk -) {const dep = this.deps [I] if (! this.newDepIds.has (dep.id)) {dep.removeSub (this)}}.}
Traversing this.deps, if there is no dep.id in newDepIds, it means that the current dep is not included in the new dependency, and you need to remove the current watcher from the observer list of dep, followed by the exchange of values between depIds and newDepIds, deps and newDeps, and clear newDepIds and newDeps. At this point, the watcher is evaluated, the new dependency is updated, and finally the value is returned.
Go back to createComputedGetter and take a look:
If (Dep.target) {watcher.depend ()}
When executing the getter of the evaluation attribute, it is possible that there are other evaluation attributes in the expression that depend on, so we need to execute watcher.depend to add the deps of the current watcher to the Dep.target. Finally, you can return the obtained watcher.value.
Generally speaking, we trigger the get function of watcher from this [key], put the current watcher into the stack, collect the required dependent dep to newDepIds and newDeps through the evaluation expression, add watcher to the observer list of the corresponding dep, and finally clear the invalid dep and return the evaluation result, thus completing the collection of dependencies.
Updates to Watcher
Now that we understand the basic principles of dependency collection of watcher and observer collection of dep, let's take a look at how to notify watcher for update operation when the data of dep is updated.
Notify () {/ / stabilize the subscriber list first const subs = this.subs.slice () for (let I = 0, l = subs.length; I < l; update +) {subs [I] .update ()}}
First of all, in dep.notify, we copy the this.subs to prevent the subs from being updated during the get of watcher, and then call the update method:
Update () {/ * istanbul ignore else * / if (this.lazy) {this.dirty = true} else if (this.sync) {this.run ()} else {queueWatcher (this)}}
If it is lazy, mark it as this.dirty = true so that watcher.evaluate calls are calculated when the getter of this [key] is triggered.
If it is a sync synchronization operation, it executes this.run, calls this.get evaluation, and executes the callback function cb.
Otherwise, execute queueWatcher, select the appropriate location, and add watcher to the queue for execution, because it has nothing to do with responsive data, so it is no longer expanded.
Summary
Because of the limited space, we only give a basic introduction to the basic principles of data binding. Here we draw a simple flow chart to help understand the responsive data of Vue, omitting some logic and boundary conditions such as VNode, which do not affect understanding, and make the process more intuitive as simple as possible:
At this point, the study on "A brief Analysis of the Observer pattern examples in Vue responsive data" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.