Before, the interviewer asked me: Please tell me how the vUE response is realized. I answer: Object. DefineProperty. That’s the end of the answer, and I know the interviewer will continue to ask questions, and I will be confused.

In vUE source code learning 10: Creating virtual DOM into a real DOM article, the realization of how to generate real DOM virtual DOM. Now the core question is, how does a view change when you change data in a script?

Let’s take a look at the vue code and implementation results:

<div id="app">Hello ({{name}}) word</div>
<script src="dist/vue.js"></script>

<script>
    let vm = new Vue({
        el: '#app'.data() {
            return { name: 'days'.age: 11}}});setTimeout(() = > {
        vm.name = 'Become programmer after 1s'
    }, 1000)
</script>
Copy the code

In this case, a name is defined in data, which is referenced in the view and rendered to the page.

Today’s lesson is that when I change the name to vm.name after 1s, I can only change the name to 1s. The page changes synchronously.

Review: How does the page render the real Dom

In the previous vue source learning (8-10), we implemented the AST to the Render string, then generated the virtual Dom from the Render string, and then generated the real Dom from the virtual Dom.

To generate the real Dom, there is code in Lifecycle

export function mountComponent(vm, el) {
    // After the data changes, the update function is called again
    let updateComponent = () = > {
        vm._update(vm._render())
    }
    updateComponent()
}
Copy the code

Vm._update (vm._render()) is the method that renders the virtual Dom to the page, that is, we call this method after 1s.

setTimeout(() = > {
    // Note: recall the render method without diff algorithm
    vm.name = 'Become programmer after 1s'
    vm._update(vm._render())
}, 1000)
Copy the code

However, we should not have the user call vm._update(vm._render()) every time an update occurs. This needs to be done automatically when the data changes.

Two concepts before the source code

1. Observer mode

The Observer Pattern is used when there is a one-to-many relationship between objects. For example, when an object is modified, dependent objects are automatically notified. The observer model belongs to the behavioral model.

2. Rely on collection

Vue enables the view to refresh when a data change and synchronize the change with other places that use the data;

In addition, the data must be dependent before views and other uses of the data change.

So, Vue needs to be able to know if a piece of data is being used, and the technique that implements this mechanism is called dependency collection.

To explain this picture:

  1. Component generates a virtual Dom. Each Component has a Watcher and the properties are logged as dependencies when the page is rendered.
  2. When the action property in script is triggering the setter, watcher is told to trigger the re-rendering of the component.

So here’s the question:

  1. Who is the observer in Vue’s dependency collection?
  2. Who is being observed?
  3. How does each component observe changes in data?

With these questions, I continue to study the source code in depth.

Responsive source code implementation

In VUE, three classes are implemented.

  • Observer class: This class is responsible for converting arrays and objects into observable classes. How is data hijacking implemented in the previous # explore ve2. X? Learn more in this article.)

  • Dep (abbreviation of dependent) : It acts as the observed. Each data has a Dep, in which there is a subs queue storing watcher. When the data changes, the corresponding Watcher is notified to re-render through dep.notify()

  • Watcher: It acts as an observer. Each component has a Watcher, and each Watcher has a DEPS inside that holds all the data observed by the Watcher.

It can be seen that dePS and Watcher have a many-to-many relationship. There are multiple Watchers in the DEPS (a property can be used by multiple components), and there are multiple DePs in the Watcher (a component can use multiple data, all need to listen).

Watcher class

// A component corresponds to a watcher
let id = 0;
import { popTarget, pushTarget } from './dep';
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.options = options
        this.id = id++ // Add an identifier to watcher
        // exprOrFn should be executed by default
        // exprOrFn does render and update
        // When the method is called, the value is taken
        this.getter = exprOrFn
        this.deps = []
        this.depsId = new Set(a)// Get is initialized by default
        this.get()
    }
    get() {
        pushTarget(this) // Dep target is a watcher
        /* Create associations * each attribute can collect its own Watcher * One attribute can correspond to multiple Watcher */
        // The get method can be called again later when the user updates
        this.getter()
        popTarget() // Dep. Target is removed to prevent dependency collection of user values in js
    }
    update() {
        this.get()
    }
    addDep(dep) {
        let id = dep.id
        if (!this.depsId.has(id)) {
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)}}}export default Watcher
Copy the code

A brief summary of the functions of the above classes:

  • Each component corresponds to a Watcher, so you need an ID to record the uniqueness of the Watcher. Every time a new Watcher instance is created, a new ID is generated

  • PopTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget; popTarget; pushTarget;

get() {
    pushTarget(this)
    this.getter()
    popTarget()
}
Copy the code

Javascript is a single-threaded language, and the way it works here is that when the GET method is called, it pushes target, then renders the page, and then removes the Watcher object on that target.

The reason for removing this object is to prevent dependency collection of user values in JS.

Look at this picture again

Render only getters do dependency collection, not setters.

  • AddDep: This method is used by the Dep class and stored in the corresponding Watcher for each new Dep. DepsId is a set that is used to de-duplicate, because the same variable is used multiple times in the view and only one dependency collection is required.
addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
        this.depsId.add(id)
        this.deps.push(dep)
        dep.addSub(this)}}Copy the code

Dep class

// One attribute corresponds to one DEP
let id = 0
class Dep {
    // Each attribute is assigned a Dep, each Dep can store watcher, watch to store Dep
    constructor() {
        this.id = id++
        this.subs = [] // Use it to store watcher
    }
    depend() {
        // dep. target Dep = Dep
        if (Dep.target) {
            // Put the deP to Watcher to store the deP
            Dep.target.addDep(this)}}addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.subs.forEach(watcher= > {
            watcher.update()
        })
    }
}
Dep.target = null
export function pushTarget(watcher) {
    Dep.target = watcher
}

export function popTarget() {
    Dep.target = null
}

export default Dep
Copy the code
  • Id: Like the Watcher class, ID is used for deP uniqueness.

  • Depend: In an Observer class, the Depend method is called when the GET method is triggered. This method gives the deP instance to Watcher and tells watcher to store the current DEP instance.

  • AddSub: Stores watcher to subs queue

  • Notify: indicates that a change message is sent. When this variable changes, each watcher in the subs queue receives the change message and rerenders the page

  • Dep. Target is a static variable that is common to all Dep instances. PushTarget and popTarget are introduced in the Watcher class.

When is Watcher instantiated?

As mentioned earlier, each component has a Watcher

So, there is a new Watcher process when the component is mounted.

Make the following changes to the code in lifecyle.js:

export function mountComponent(vm, el) {
    let updateComponent = () = > {
        // 1. Generate the virtual DOM with render
        vm._update(vm._render()) // Subsequent updates can invoke the updateComponent method
        // 2. Virtual Dom generates real Dom
    }
    // Observer mode: property is the page refreshed by the observer: observer
    If the property changes, the updateComponent method is called
    // Each component has a watcher
    new Watcher(vm, updateComponent, () = > {
        // True tells him that it is a rendering process
        // There will be other watcher to follow
    }, true)}Copy the code

When to collect dependencies and when to rerender the page at the same time?

In the Observer class, we do a full hijacking of the data by iterating through the object and redefining each attribute with a defineProperty

Therefore, every time a property changes and is used, the GET and set methods are triggered. The core code is as follows

function defineReactive(data, key, value) {
    // Value can be an object (object nested object), recursive hijacking
    observe(value)
    let dep = new Dep()
    Object.defineProperty(data, key, {
        get() {
            console.log('key', key)
            // I want to associate watcher with Dep
            // But there is no Watcher
            if (Dep.target) {
                // Get is used in the template
                // Let deP remember that Watcher is a dependency collector
                dep.depend()
            }
            return value
        },
        set(newV) {
            if(newV ! == value) { observe(newV)// If the user assigns a value to a new object, the object needs to be hijacked
                value = newV
                dep.notify() // Notify the watcher execution of the current property}}})}Copy the code

Dependent collection:

// Dependencies are collected when data is called by page rendering
get() {
    if (Dep.target) {
        dep.depend()
    }
    return value
}
Copy the code

Notice changes:

set(newV) {
    // If the new value is inconsistent with the old value, the page is re-rendered
    if(newV ! == value) { ... dep.notify()// Tell the current property to store watcher execution to re-render the page}}Copy the code

Well, this is the end of today’s learning, looking forward to the next learning.

Historical articles

  • How does CodeGen convert ast syntax trees into Render strings?
  • Vue source code learning 9: virtual Dom implementation principle
  • Vue source code learning 10: create a virtual DOM into a real DOM