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:

  1. 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;
  2. 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.
  3. 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()) :

  1. configurable

    • If and only if of the propertyconfigurableThe key value fortrue, the descriptor of the attribute can be changed, and the attribute can also be deleted from the corresponding object;
    • The default isfalse.
  2. enumerable

    • If and only if of the propertyenumerableWhen the key value is true, the property will appear in the object’s enumerated property;
    • The default is false.

The data descriptor also has the following optional key values:

  1. value
    • The value corresponding to this property. It can be any valid JavaScript value (numeric value, object, function, etc.).
    • The default isundefined.
  2. writable
    • If and only if of the propertywritableThe key value fortrue, the value of the property, which is the one abovevalueCan be changed by the assignment operator.
    • The default isfalse.

The access descriptor also has the following optional key values:

  1. get
    • Properties of thegetterDelta function, if there is nogetter, it isundefined;
    • This function is called when the property is accessed. No arguments are passed, but they arethisObject (because of inheritance, herethisNot 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.
  2. set
    • Properties of thesetterDelta 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 assignmentthisObject.
    • The default isundefined.

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:

  1. oldValueThe agent ofdataobjectkeyControl over property values, i.ekeyReading and writing property values are essentially operationsoldValue;
  2. enumerableSet totrueTo ensure thekeyCan be read by normal traversal;
  3. configurableSet totrue, to ensure that subsequent users can configure the property value twice, you knowconfigurableSet tofalseIs one-way, that is, cannot be changedtrueAnd 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:

  1. ({ __proto__: [] } instanceof Array)Make full use ofinstanceOfThe principle of (refWrite it by hand when you understand it) to detect the current runtimeArrayWhether have__proto__Attribute value, that is, with prototype chain;
  2. target.__proto__ = prototypeThis is also incompatible with VueIE8And 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:

  1. The statementdepInstance property to mount the source object__ob__Non-enumerable properties;
  2. If the source data is an array, modify its prototype object to the specified objectArrayPrototypeCopyAnd iterate over each element executionobserve(item)
  3. If the source object is an object, its property execution is traverseddefineReactive(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:

  1. Attribute hijacking is achieved by traversing the attributes of the target object.
  2. For each property of the target objectkeyThe operation will be on the currentkeyA variable in the lexical scope ofdepDo two things:
    • Is called when the value is specifieddep.depend()And/orchildOb.dep.depend(), will be at this timeDep.targetAdded to thedep.depsIn the
    • When assignment is calleddep.notifyThat will bedep.depsIterate over each child element in the execution.

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:

  1. Dep.targetCan be carried inWatcherThe instanceget()Method is assigned to this instancewatcher;
  2. dep.depend()At execution time, if present, the currentDep.targetThat iswatcherThe collecteddep.deps;
  3. dep.notify()During execution, it willdep.depsAll collected in thewatcherInstances 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:

  1. willvm,callback,getterMount to thewatcherOn instance objects;
  2. One callget()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.

  1. useObserverThe source objectdataBecome a traceable objectreactiveData;
  2. usereactiveDataAnd designatedgetterandcallbackcallnew Watcher(...);
    • WatcherThe constructor of theget():
      • callpushWatcher(this)thewatcherMount to theDep.targetOn;
      • performthis.getter.call(reactiveData, reactiveData)
        • ifgetterContains thereactiveDataThe value of an attributekeyAccess ([Get]) will triggerkeyThe correspondingdepFor dependency collection:dep.depend(), i.e.,dep.deps.add(Dep.target)
      • End of execution callpopWatcher()That will beDep.targetReset to the previous value (the end of the stack is the value of the last operation)
      • returngetter()The result value of the execution.
  3. Then, build on thatreactiveDataA property ofkeyThe following happens to the value of:
    • The triggerkeyThe correspondingdepthenotify()
      • traversedep.depsGet all thewatcher, in orderwatcher.update()
        • callget()Gets the latest valuevalue(The specific process is the same as above 2-1)
        • callcallback(value, oldValue)

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:

  1. indataCreates a new property on the object with an initial value ofcomputeThe specifiedgetterReturn value of;
  2. incomputeRely on thegetterIn anydataNeeds to be reflected when other properties of thecomputeThe value of the;
  3. Therefore, containscomputeView dependency ofcompute.computeRely ondataOther 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

  1. 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;
  2. Object.defineProperty(vm, name, { ... }To givevmDefines a key positionnameProperty, and proxy its value operation, i.egetter;
  3. computeWatcher.depend()That is, the compute variable collects the watcher that has a dependency on it.
  4. 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