Zero. Target prototype
The goal of this paper is to copy a Vue2. X source code implementation to complete a basic Observer, but our purpose is not to achieve and write, but to understand the Vue2. X Observer implementation principle on the basis of manual practice, so that we have a deeper and more specific cognition and understanding of it.
The target feature we want to do is something like this:
preface
Vue3. X has been officially released on 18 September 2020, the corresponding Chinese documentation has been translated, and the upgrade guide from 2.x to 3.x has also been provided.
Admittedly, compared with Vue2, Vue3 has been greatly improved in both performance and code implementation, so there must be a lot of things worth exploring and studying. But as a former JavaWeb and jQuery developer, when I first came into contact with Vue2. X, I was amazed: is this magic? (jQuery party ultimate Gospel).
The most typical feature in Vue2. X is two-way data binding. What does two-way data binding have to do with observer mode? Let’s talk it through.
I. Bidirectional binding
Before we talk about two-way binding, we need to know what one-way binding is: one-way binding is as simple as binding a Model to a View, and when we update the Model with JavaScript, the View updates automatically.
Model and View refer to M and V in classic development mode MVC, namely, data Model and user View, while C is Controller. The purpose of using MVC is to separate the M and V implementation code so that the same program can use different representations. The purpose of C is to ensure the synchronization of M and V. Once M changes, V should be updated synchronously.
One-way binding is two-way binding: Based on one-way binding, if the user updates the View, the data corresponding to the Model is automatically updated, which is two-way binding. Hence the concept of MVVM:
MVVM was first proposed by Microsoft. It borrows the MVC idea of desktop applications. In the front page, the Model is represented as a pure JavaScript object and the View is responsible for the display, thus achieving maximum separation between the two. The thing that connects a Model to a View is the ViewModel. The ViewModel is responsible for synchronizing data from the Model to the View and for synchronizing changes from the View back to the Model. Vue is a typical MVVM framework, and the underlying implementation of the ViewModel requires bidirectional binding.
Therefore, to achieve bidirectional binding, we need to implement a ViewModel in JavaScript, and this ViewModel corresponds to the Observer in the Vue2. X source code.
Observer model
Observer pattern: Often referred to as the publisher-subscriber pattern. It defines a one-to-many dependency, that is, when an object’s state changes, all dependent objects are notified and updated automatically, resolving the coupling of functions between the subject object and the observer.
Observer mode can be used in any of the following scenarios:
- When an abstract model has two aspects, one of which depends on the other. Encapsulating the two in separate objects allows them to be changed and reused independently;
- When one object is changed, other objects need to be changed at the same time, but it is not known how many objects need to be changed.
- When an object must notify other objects without knowing who the object is. In other words, you don’t want these objects to be tightly coupled.
So how does Vue implement the observer pattern? We have to say object.defineProperty ().
Third, Object. DefineProperty
The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.
On MDN, we can see a more detailed description of Object.defineProperty(). The third parameter descriptor of the Observer is:
Property descriptors, that is, property descriptors to define or modify. There are two main types of property descriptors that currently exist in objects: data descriptors and access descriptors.
A data descriptor is a property with a value that can be writable or unwritable. Access descriptors are properties described by getter and setter functions. A descriptor can only be one of these two; You can’t be both.
Both descriptors are objects. They share the following optional key values (the default is the default when defining attributes using Object.defineProperty()) :
-
configurable
- If and only if of the property
configurable
The key value fortrue
, the descriptor of the attribute can be changed, and the attribute can also be deleted from the corresponding object; - The default is
false
.
- If and only if of the property
-
enumerable
- If and only if of the property
enumerable
When the key value is true, the property will appear in the object’s enumerated property; - The default is false.
- If and only if of the property
The data descriptor also has the following optional key values:
value
- The value corresponding to this property. It can be any valid JavaScript value (numeric value, object, function, etc.).
- The default is
undefined
.
writable
- If and only if of the property
writable
The key value fortrue
, the value of the property, which is the one abovevalue
Can be changed by the assignment operator. - The default is
false
.
- If and only if of the property
The access descriptor also has the following optional key values:
get
- Properties of the
getter
Delta function, if there is nogetter
, it isundefined
; - This function is called when the property is accessed. No arguments are passed, but they are
this
Object (because of inheritance, herethis
Not necessarily the object that defines the property); - The return value of this function is used as the value of the property;
- The default is undefined.
- Properties of the
set
- Properties of the
setter
Delta function, if there is nosetter
, it isundefined
; - This function is called when the property value is modified. This method takes an argument (that is, the new value being assigned) that is passed in to the assignment
this
Object. - The default is
undefined
.
- Properties of the
Key values that descriptors can have:
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
Data descriptor | can | can | can | can | Can not be | Can not be |
Access descriptor | can | can | Can not be | Can not be | can | can |
A descriptor is considered a data descriptor if it does not have any of the keys of value, writable, GET, and set. An exception is raised if a descriptor has both value or writable and get or set keys.
Keep in mind that these options are not necessarily their own properties, but also consider inherited properties. To make sure these defaults are preserved, you may want to freeze Object.prototype, specify all options explicitly, or point the __proto__ attribute to NULL with Object.create(null) before setting.
As you can see, we customize the read and write behavior of the target object by defining the functions corresponding to the set and GET properties in the access descriptor object. Here’s the simplest example:
const target = Object.create(null) Object.defineProperty(target, 'name', { enumerable: true, configurable: True, get () {return 'Hello Vue'}, set (value) {console.log(' I received ${value}, Target. name = 'Vue' // I received Vue, but I did nothing target.name // Hello VueCopy the code
Data hijacking
The first prerequisite for data hijacking is that the data we’re dealing with must be an Object, because Object.defineProperty() can only handle objects. If we want to hijack the property of the object, we need to traverse the value of each property of the object to complete the hijacking action, we can achieve through the following code:
Object.keys(obj).forEach(key => defineReactive(obj, key))
Copy the code
DefineReactive is the key to attribute hijacking. We define the simplest hijacking template:
function defineReactive (data, key) {
let oldValue = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
return oldValue
},
set (newValue) {
oldValue = newValue
}
})
}
Copy the code
A closer look at this method seems to do nothing, but it does seem to do something, which is worth our attention:
oldValue
The agent ofdata
objectkey
Control over property values, i.ekey
Reading and writing property values are essentially operationsoldValue
;enumerable
Set totrue
To ensure thekey
Can be read by normal traversal;configurable
Set totrue
, to ensure that subsequent users can configure the property value twice, you knowconfigurable
Set tofalse
Is one-way, that is, cannot be changedtrue
And it cannot be configured twice.
Next, we need to implement the observation pattern based on this template:
function defineReactive (data, key) {
let oldValue = data[key]
let dep = new Dep()
let childOb = observe(oldValue)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get () {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
return oldValue
},
set (newValue) {
if (newValue === oldValue) {
return
}
oldValue = newValue
childOb = observe(newValue)
dep.notify()
}
})
}
Copy the code
Before we start the code analysis, let’s get to know our new friends: Dep and Observe.
class Dep { constructor () { this.deps = new Set() } depend () { if (Dep.target) { this.deps.add(Dep.target) } } notify () { this.deps.forEach(watcher => watcher.update()) } } Dep.target = null function observe (target) { if (! isObject(target)) { return } let ob if (hasOwnKey(target, '__ob__') && target.__ob__ instanceof Observer) { ob = target.__ob__ } else { ob = new Observer(target) } return ob }Copy the code
Where Dep is a constructor, it has an instance property deps, two instance methods Depend and notify, and a static property target with a default value of null. The observe function is also simpler, returning an Observer instance.
So what is an Observer? Let’s move on:
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this, false)
if (Array.isArray(value)) {
if (hasProto()) {
setPrototype(value, ArrayPrototypeCopy)
} else {
copyProperty(value, ArrayPrototypeCopy, arrayKeys)
}
this.observeArray(value)
} else {
this.observeObject(value)
}
}
observeArray (array = []) {
array.forEach(item => observe(item))
}
observeObject (obj = {}) {
Object.keys(obj).forEach(key => defineReactive(obj, key))
}
}
Copy the code
This should give you a pretty good idea that Observer is what we’re going to implement in the end, and all it does is call observe and defineReactive in different cases. Don’t make it too complicated, it’s pretty simple.
5. Analyze the Observer line by line
Let’s take a line-by-line look at the Observer to see what it does.
this.value = value
this.dep = new Dep()
def(value, '__ob__', this, false)
Copy the code
The most important thing here is that it creates a property of the instance object called DEP with a value of deP instance; Def (value, this); def (value, this);
function def (target, key, value, enumerable) { Object.defineProperty(target, key, { value, configurable: true, writable: true, enumerable: !! enumerable }) }Copy the code
It’s worth noting that Enumerable is specified as false, which means it doesn’t want __ob__ to be iterated over.
if (Array.isArray(value)) {
if (hasProto()) {
setPrototype(value, ArrayPrototypeCopy)
} else {
copyProperty(value, ArrayPrototypeCopy, arrayKeys)
}
this.observeArray(value)
}
Copy the code
Here’s hasProto(), setPrototype(), and copyProperty, but let’s look at what they do:
export function hasProto () {
return ({ __proto__: [] } instanceof Array)
}
export function setPrototype (target, prototype) {
if (Object.setPrototypeOf) {
Object.setPrototypeOf(target, prototype)
} else {
target.__proto__ = prototype
}
}
export function copyProperty (target, src, keys) {
keys.forEach(key => def(target, key, src[key], false))
}
Copy the code
The following points need to be noted:
({ __proto__: [] } instanceof Array)
Make full use ofinstanceOf
The principle of (refWrite it by hand when you understand it) to detect the current runtimeArray
Whether have__proto__
Attribute value, that is, with prototype chain;target.__proto__ = prototype
This is also incompatible with VueIE8
And one of the reasons for the following browsers.
Returning to our code in the Observer:
if (Array.isArray(value)) {
if (hasProto()) {
setPrototype(value, ArrayPrototypeCopy)
} else {
copyProperty(value, ArrayPrototypeCopy, arrayKeys)
}
this.observeArray(value)
} else {
this.observeObject(value)
}
observeArray (array = []) {
array.forEach(item => observe(item))
}
observeObject (obj = {}) {
Object.keys(obj).forEach(key => defineReactive(obj, key))
}
Copy the code
Given the above analysis, we can see that the Observer does several things:
- The statement
dep
Instance property to mount the source object__ob__
Non-enumerable properties; - If the source data is an array, modify its prototype object to the specified object
ArrayPrototypeCopy
And iterate over each element executionobserve(item)
- If the source object is an object, its property execution is traversed
defineReactive(value, key)
.
Leaving ArrayPrototypeCopy for a separate discussion later, we come full circle back to the defineReactive() method, which illustrates its importance. Let’s go back to its implementation for further analysis.
Analyze defineReactive line by line
function defineReactive (data, key) {
let oldValue = data[key]
let dep = new Dep()
let childOb = observe(oldValue)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get () {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
return oldValue
},
set (newValue) {
if (newValue === oldValue) {
return
}
oldValue = newValue
childOb = observe(newValue)
dep.notify()
}
})
}
Copy the code
We’ve already analyzed most of the code, but let’s focus on the ones we haven’t seen:
let dep = new Dep()
let childOb = observe(oldValue)
Copy the code
As those of you familiar with JavaScript might have guessed, we declare two variables, dep and childOb, using the principle of closures, ensuring that they are accessible whenever the getter() and setter() methods on the key of the target object data are called. You’ll see the subtleties of using this closure next:
get () {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
return oldValue
}
Copy the code
Dep.depend () and childob.dep.depend (), combined with the definition of dep, we can see that depend never does anything:
depend () {
if (Dep.target) {
this.deps.add(Dep.target)
}
}
Copy the code
Target = null by default, so why do we do this? The essence of this is to take advantage of the iron law that JavaScript is single-threaded, and use it in conjunction with Watcher, which we’ll talk about at the end of the day, to implement dependency collection perfectly.
set (newValue) { if (newValue === oldValue && newValue ! == newValue) { return } oldValue = newValue childOb = observe(newValue) dep.notify() }Copy the code
See newValue! == oldValue == oldValue == oldValue == oldValue If I think of this, I think of NaN. In fact, this is used to avoid the value of newValue being NaN and avoid unnecessary subsequent execution.
In addition, the method guarantees that nothing will be done if the shallower values are equal to the shallower values.
Finally, the set method resets the childOb variable to the observe-treated value of newValue, ensuring that any subsequent get of the key will obtain the latest childOb.
Finally, dep.notify() is called, which means that the dep.targets we add to the DEPS attribute values of the DEP object using the Depend () method are executed one by one.
7. Handle arraysArrayPrototypeCopy
Before we get into Watcher, let’s go back to the definition of ArrayPrototypeCopy.
We already know that the Observer will first use the appropriate method (setPrototype or copyProperty) to set ArrayPrototypeCopy to the _proto_ reference of data or traverse to the data object (note) Specify it as Enumerable: false so it cannot be enumerable.
So what is the purpose of this? [[Prototype]] [[Prototype]] [[Prototype]] [[Prototype]] [[Prototype]]
The [[Prototype]] mechanism is an internal link in an object that references other objects.
In general, this link is used: if the desired attribute or method reference is not found on the object, the engine will continue to search on the object associated with [[Prototype]]. Similarly, if no reference is found in the latter, the [[Prototype]] is searched, and so on. This series of links is called a “prototype chain.”
Therefore, when we later operate on some property of data that doesn’t exist but exists on ArrayPrototypeCopy, we will perform the behavior defined on ArrayPrototypeCopy.
ArrayPrototypeCopy defines ArrayPrototypeCopy:
const ArrayPrototypeCopy = Object.create(Array.prototype) const arrayMethods = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort' ] arrayMethods.forEach(method => { const original = Array.prototype[method] def(ArrayPrototypeCopy, 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) } ob.dep.notify() return result }) })Copy the code
ArrayPrototypeCopy’s original value is an empty object with array. prototype as [[prototype]], which guarantees maximum compatibility with Array objects.
After that, arrayMethods is iterated through the specified method name set using defineProperty to set its attribute, and the corresponding attribute value of each attribute is completed by mutator function. Therefore, let’s focus on the implementation of mutator:
function mutator (... Args) {// Perform source operation, Const result = original. Apply (this, Const ob = this.__ob__ let Inserted Switch (method) {case 'push': case 'unshift': inserted = args break case 'splice': Inserted = args. Slice (2) break} // Inserted not empty, Ob.observearray is called to process them so that they are also traceable if (INSERTED) {ob.observearray (inserted)} // Finally, the 'dep' instance of the array's 'Observer' instance is called 'notify()' to notify its' subscribers' that it has changed. ob.dep.notify() return result })Copy the code
The notify of the response can be triggered when the specified methods operate on the array.
8, Observer and Watcher dream linkage
At this point, we have probably figured out most of the full implementation of the Observer, which can be summarized as follows:
- Attribute hijacking is achieved by traversing the attributes of the target object.
- For each property of the target object
key
The operation will be on the currentkey
A variable in the lexical scope ofdep
Do two things:- Is called when the value is specified
dep.depend()
And/orchildOb.dep.depend()
, will be at this timeDep.target
Added to thedep.deps
In the - When assignment is called
dep.notify
That will bedep.deps
Iterate over each child element in the execution.
- Is called when the value is specified
As you can see, in the Observer and Dep, we only evaluated dep. target without assigning it, and dep. Depend would never do anything without assigning it. So, when do we assign to dep.target?
All the answers are in Watcher.
8.1 What is Watcher
“Watcher” means “Observer” or “Observer”. But why have a Watcher when we have an Observer?
Remember at the beginning of the article that we were going to implement VM in MVVM? What VM can do is make the data and view bidirectional binding, that is, changing the data can drive the update of the view, and changing the view can change the content of the data.
At this point, the Observer does not achieve this effect. It only does data processing, but does nothing to update the view, which Watcher is designed to do.
Watcher is the glue that binds the View and Model together, and the “drug starter” for two-way data binding:
import Dep from "./dep.js"
const watcherStack = []
function pushWatcher (watcher) {
if (Dep.target) {
watcherStack.push(Dep.target)
}
Dep.target = watcher
}
function popWatcher () {
Dep.target = watcherStack.pop()
}
const noop = () => {}
export default class Watcher {
constructor (vm, expOrFn, callback = noop, options = {}) {
const { compute } = options
this.vm = vm
this.callback = callback
this.compute = compute
this.value = undefined
this.getter = noop
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else if (typeof expOrFn === 'string') {
this.getter = this.parseExp2Fn(expOrFn)
}
if (this.compute) {
this.dep = new Dep()
} else {
this.value = this.get()
}
}
depend () {
this.dep.depend()
}
get () {
pushWatcher(this)
const value = this.getter.call(this.vm, this.vm)
popWatcher()
return value
}
update () {
const oldValue = this.value
const newValue = this.get()
if (oldValue === newValue) {
return
}
this.callback(newValue, oldValue)
if (this.compute) {
this.dep.notify()
}
}
parseGetter (expOrFn) {
const getter = () => {
let value = this.vm
const expArr = expOrFn.split('.')
expArr.forEach(exp => {
exp = exp.trim(exp)
value = value[exp]
})
return value
}
return getter
}
}
Copy the code
8.2 What did Watcher do
The Watcher implementation is tied to its relationship with dep. target, which is always null in our Observer.
So the first thing we need to do is how Watcher handles dep.target:
const watcherStack = []
function pushWatcher (watcher) {
if (Dep.target) {
watcherStack.push(Dep.target)
}
Dep.target = watcher
}
function popWatcher () {
Dep.target = watcherStack.pop()
}
Copy the code
As you can see, Watcher maintains a stack locally, following a first-in, last-out rule. PushWatcher and popWatcher are the only two methods to manipulate the stack. If you modify the stack directly, you will have unexpected effects.
In particular, the principle of fifO is intended to be consistent with the order in which functions (methods) are executed in JavaScript (or most programming languages), which is where the names of the stack, the stack, and the stack are derived.
PushWatcher (target) assigns a value to dep.target, and before doing so pushes the existing dep.target onto the stack. PopWatcher () resets dep.target to the value at the end of the local stack.
Further analysis of where pushWatcher and popWatcher are called:
get () {
pushWatcher(this)
const value = this.getter.call(this.vm, this.vm)
popWatcher()
return value
}
Copy the code
Get () pushes this onto the stack, then executes the getter(), then pushes this off the stack, and finally returns the result of the getter().
At this point, all the previous doubts were answered:
Dep.target
Can be carried inWatcher
The instanceget()
Method is assigned to this instancewatcher
;dep.depend()
At execution time, if present, the currentDep.target
That iswatcher
The collecteddep.deps
;dep.notify()
During execution, it willdep.deps
All collected in thewatcher
Instances execute them in orderupdate()
Methods.
So let’s focus on the update() method:
update () {
const oldValue = this.value
const newValue = this.get()
if (oldValue === newValue) {
return
}
this.callback(newValue, oldValue)
if (this.compute) {
this.dep.notify()
}
}
Copy the code
Without compute, update simply executes get() again to get the current value, newValue, shallow compare to oldValue, and call the corresponding callback(newValue, oldValue) if it is not equal.
So, when did Data hook up with Watcher? In fact, they have a sequential relationship: ReactiveData (reactiveData, getter, callback, callback, etc.); reactiveData (reactiveData, getter, callback, callback); .
So it’s worth taking a look at Watcher’s constructor:
constructor (vm, getter, callback = noop, options = {}) { const { compute } = options this.vm = vm this.callback = callback this.compute = compute this.value = undefined this.getter = noop if (typeof expOrFn === 'function') { this.getter = expOrFn } if (this.compute) { this.dep = new Dep() } else { this.value = this.get() } }Copy the code
Regardless of compute, we can see that constructor values do the following things:
- will
vm
,callback
,getter
Mount to thewatcher
On instance objects; - One call
get()
Method that assigns its return value tothis.value
.
As you can see here, the default first execution of the get() method is where all the magic begins.
- use
Observer
The source objectdata
Become a traceable objectreactiveData
; - use
reactiveData
And designatedgetter
andcallback
callnew Watcher(...)
;Watcher
The constructor of theget()
:- call
pushWatcher(this)
thewatcher
Mount to theDep.target
On; - perform
this.getter.call(reactiveData, reactiveData)
- if
getter
Contains thereactiveData
The value of an attributekey
Access ([Get]
) will triggerkey
The correspondingdep
For dependency collection:dep.depend()
, i.e.,dep.deps.add(Dep.target)
- if
- End of execution call
popWatcher()
That will beDep.target
Reset to the previous value (the end of the stack is the value of the last operation) - return
getter()
The result value of the execution.
- call
- Then, build on that
reactiveData
A property ofkey
The following happens to the value of:- The trigger
key
The correspondingdep
thenotify()
- traverse
dep.deps
Get all thewatcher
, in orderwatcher.update()
- call
get()
Gets the latest valuevalue
(The specific process is the same as above 2-1) - call
callback(value, oldValue)
- call
- traverse
- The trigger
The definition of the getter function is important. If it involves accessing the value of the reactiveData property, the binding relationship between the data and the view is naturally associated. Vue uses updateComponent as the getter to bind views to data:
updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && ! vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)Copy the code
At this point, we’ve basically figured out what Watcher does, and we’ve combined it with the Observer to have the data changes we want to drive view updates. We can now try this with the following code:
<! - HTML -- > < div id = "app" > < p > the count value: < span id = "count" > < / span > < / p > < p > the name of the value of the deep: < span id = "deep" > < / span > < / p > < p > the content of the list: < span id = "list" > < / span > < / p > < / div >Copy the code
// JavaScript import { Observer } from './observer/index.js' import Watcher from './observer/watcher.js' const data = { count: 0, deep: { name: 'ZhangSan' }, list: [1,2,3,4,5]} new Observer(data) new Watcher(data, () => { document.getElementById('count').innerHTML = data.count }) new Watcher(data, () => { document.getElementById('deep').innerHTML = data.deep.name }) new Watcher(data, () => {document.getelementById ('list').innerhtml = data.list.join(', '); ') }) const bindClickEvent = (id, event) => { document.getElementById(id).addEventListener('click', event, false) } bindClickEvent('btn1', () => (data.count = data.count + 1)) bindClickEvent('btn2', () => (data.list.push(1))) bindClickEvent('btn3', () => (data.list.pop())) bindClickEvent('btn4', () => (data.deep.name = 'LiSi'))Copy the code
It looks like this:
Compute and watch
There’s one compute in Watcher that we’ve been ignoring, just so that Watch can talk about it at the end. Implementationally, watch and compute just need to call new Watcher() with the appropriate parameters to do what they expect.
9.1 watch
The function of Watch is simply to observe the specified property key of the given object data. When the property value of the key changes, it will do something that only needs to be specified in the callback parameter.
Thus, the implementation of Watch has been determined:
function watch (reactiveData, expOrFn, callback) {
new Watcher(reactiveData, expOrFn, callback)
}
Copy the code
Our general usage will specify expOrFn as reactiveData point operator value method, for example, data.deep. Name corresponding expOrFn is deep. Name, we can add watch usage based on the above example:
<! -- html --> <p id="callback"></p> // JavaScript watch(data, 'deep.name', (newValue) => {document.getelementById ('callback'). InnerHTML = (' modified deep name value is ${newValue} ')})Copy the code
The effect is as follows:
9.2 compute
Compute is a bit more complex than Watch because it relies on data to specify property modification operations:
- in
data
Creates a new property on the object with an initial value ofcompute
The specifiedgetter
Return value of; - in
compute
Rely on thegetter
In anydata
Needs to be reflected when other properties of thecompute
The value of the; - Therefore, contains
compute
View dependency ofcompute
.compute
Rely ondata
Other property values of.
Compute compute compute compute compute compute compute compute compute compute
constructor (vm, expOrFn, callback = noop, options = {}) { ... This.pute = options.pute // compute may be used by the view, If (this.pute) {this.dep = new dep ()} else {this.value = this.get()}} update () {... If (this.dep.notify) {this.dep.notify()}} if (this.dep.notify()}}Copy the code
We can further implement our compute method based on this:
function compute (vm, name, getter, callback) { const computeWatcher = new Watcher(vm, getter, callback, { compute: true }) Object.defineProperty(vm, name, Computewatcher.depend () {get() {// Collect dependencies (that is, compute collects the watcher on which it depends) computeWatcher.depend() Const value = computeWatcher. Get () return value}})}Copy the code
Compute compute compute compute compute compute compute compute
const computeWatcher = new Watcher(vm, getter, callback, { compute: true })
A calculation is createdwatcher
, essentially creating an object that has no initial values but can manually collect view dependencieswatcher
;Object.defineProperty(vm, name, { ... }
To givevm
Defines a key positionname
Property, and proxy its value operation, i.egetter
;computeWatcher.depend()
That is, the compute variable collects the watcher that has a dependency on it.const value = computeWatcher.get()
The watcher of the compute variable itself is collected for other variables on which it depends.
Finally, we iterate over compute into the example above:
<! -- HTML --> <p> Calculate the value of the other attribute: <span id="other"></span></p> // JavaScript function showCompute () { document.getElementById('other').innerText = Other compute(data, 'other', () => {return data.count + data.list[0]}, showCompute) showCompute()Copy the code
The effect is as follows:
Write at the end
At this point, the relevant content of the Observer is almost complete. Although there are many differences with the Vue2. X source code Observer, but should be a minimal, you can quickly understand and master the Observer version. In my opinion, this is what Vue should look like in the beginning, and everything that follows is gradually added up and enriched.
Vue2. X still has a lot to learn, such as the implementation of nextTick, the clever implementation of recursive rendering of createElement components, and the diff algorithm in Patch. Each point is worth digging and thinking about.
To learn a complex knowledge, we should not only have the idea of “step by step”, but also have the spirit of “peeling the silk from the cocoon”. Finally, I hope you can study happily and have strong strength