Video tutorial is attached at the end of the article

This paper mainly studies the core principle of Vue bidirectional binding. The code is a simplified version, relatively simple. Other processing, such as arrays, is also not considered. Welcome to study and exchange.

First, preparation

1. What is the MVVM framework?

MVVM is short for Model-view-ViewModel, two-way data binding, that is, the View affects the Model, and the Model affects the data. It’s essentially an improved version of MVC.

  • A Model is a data access layer, such as the data passed by a background interface
  • A View is the structure, layout, appearance (UI) of the page that the user sees on the screen
  • The ViewModel is responsible for synchronizing changes from the View to the Model or converting changes from the Model to the View.

2. How does Vue implement bidirectional data binding

Ve2. X is a two-way data binding implemented via Object.defineProperty, which does not support IE8 or below. See MDN documentation directly for related syntax. There are many attribute descriptors. Here are a few commonly used descriptors. For details, please refer to the document. The following is to define a property named Vue for the definition obj object.

Object.defineProperty(obj, 'vue', {
  configurable: true.writable: true.enmerbale: true.value: 'Hello, Vue',
  get() {
    return 'Hello, Vue'
  }
  set(val) {
    console.log(val)
  }
})
Copy the code

Vue =’react’ is disabled, and works without any additional information. The new property can be configured without any additional information

Writable: Specifies whether attributes can be changed by assignment operators. If not set to true, assignments to vue attributes, e.g. Obj. vue=’react’, are invalid

Enmerbale: specifies whether a property can be enumerated. If not set to true, use for… in… Or object. keys cannot read this property

Value: Specifies the value corresponding to the attribute. If used together with the get attribute, an error will be reported

Get: When accessing this property, the get method, if set, executes and returns

Set: When modifying the value of this property, if there is a set method, this method is executed and the new value is passed as an argument to Object. vue = ‘hello, Vuex’.

Flow chart of 3.

4. Process analysis

Here we’ll take a look at the code implementation, get a sense of the process, and finally analyze the process.

Combine code, comments, and process analysis to better understand the process.

Two, start implementation

referenceVue2.xSource code implementation, with the actualVueThe implementation is different, but the principle is similar. It is suggested that you can continue to study the actual Vue source code

1. The Vue entrance

// Simulate the Vue entry
function MVVM(options) {
    var vm = this;
    vm.$options = options || {};
    vm._data = vm.$options.data;
    /** * initState implements observe for data *, that is, respond to data/computed and delegate data to vm instances */
    initState(vm)
		// Build the template
    this.$compile = new Compile(options.el || document.body, this)}Copy the code

2. Template compilation

Compile here is just a simple template compilation, which is quite different from the actual Compile of Vue. The actual Compile implementation is more complex and needs to go through three stages: parse, optimize and generate.

  • Parse: Use the re to parse the instructions (V-XXX) variables of vue in template to form an abstract syntax tree AST
  • Optimize: Flags static nodes for later performance optimizations, skipped in diff
  • Generate: Convert the AST generated in the first part into a render function
function Compile(el, vm) {
  this.$vm = vm;
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);

  if (this.$el) {
    // Convert native nodes to document fragments to improve operation efficiency
    this.$fragment = this.node2Fragment(this.$el);
    // Compile template content while collecting dependencies
    this.compile(this.$fragment);
    // Mount the processed DOM tree to the real DOM node
    this.$el.appendChild(this.$fragment); }}// compile the relevant method implementation
Compile.prototype = {
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    /** * copies the native node to the fragment, * each loop takes the first node out of the EL and appends it to the fragment until the EL has no byte points */
    let child;
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
  
    return fragment;
  },
  compile: function (el) {
    const childNodes = el.childNodes
    // childNodes is not a standard Array. Use array. from to turn childNodes into an Array and iterate over each node.
    Array.from(childNodes).forEach(node= > {
      // Use the closure mechanism to save the original text of the text node, and then update it according to the original text.
      const text = node.textContent;
      // Element node, for element attribute binding to handle instructions
      if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      // Text node and contains {{xx}} string for text, template content replacement
      else if (this.isTextNode(node) && / \ {\ {(. *) \} \} /.test(text)) {
        this.compileText(node, RegExp. $1.trim(), text);
      }
      // Recursively compile the content of the child node
      if (node.childNodes && node.childNodes.length) {
        this.compile(node); }}); },compileElement: function (node) {
    const nodeAttrs = node.attributes

    Array.from(nodeAttrs).forEach(attr= > {
      const attrName = attr.name;
      // Determine if the attribute is an instruction, such as V-text, etc
      if (this.isDirective(attrName)) {
        const exp = attr.value;
        const dir = attrName.substring(2);
        // Event command
        if (this.isEventDirective(dir)) {
          compileUtil.eventHandler(node, this.$vm, exp, dir);
        }
        // Common instruction
        else {
          compileUtil[dir] && compileUtil[dir](node, this.$vm, exp); } node.removeAttribute(attrName); }}); },compileText: function (node, exp) {
    // compileUtil.text(node, this.$vm, exp);
    // Use the closure mechanism to save the original text of the text node, and then update it according to the original text.
    const vm = this.$vm
    let text = node.textContent
    const updaterFn = updater.textUpdater

    let value = text.replace(/ \ {\ {(. *) \} \} /, compileUtil._getVMVal(vm, exp))
    updaterFn && updaterFn(node, value);

    new Watcher(vm, exp, function (value) {
      updaterFn && updaterFn(node, text.replace(/ \ {\ {(. *) \} \} /, value));
    });
  },
  / /... omit
};
Copy the code

Instruction set processing

// Instruction processing set
const compileUtil = {
  text: function (node, vm, exp) {
    this.update(node, vm, exp, 'text');
  },
	/ /... omit
  update: function (node, vm, exp, dir) {
    // Use different functions to render and update data for different instructions.
    const updaterFn = updater[dir + 'Updater'];
    // Take the value here and render the content for the first time
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    new Watcher(vm, exp, function (value, oldValue) {
      updaterFn && updaterFn(node, value, oldValue);
    });
  },
 	/ /... omit
};
  
const updater = {
  textUpdater: function (node, value) {
    node.textContent = typeof value == 'undefined' ? ' ' : value;
  },
  / /... omit
};
Copy the code

3. Responsive objects

initState

The initState method initializes properties such as props, methods, data, computed, and wathcer. Here we mainly implement data and computed operations.

function initState(vm) {
  const opts = vm.$options
  // Initialize data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true)}// Initialize computed
  if (opts.computed) initComputed(vm, opts.computed)
}
Copy the code

initData

The following two operations are mainly implemented:

  1. Call the observe method to observe the change of the entire data and make data responsive. You can access the corresponding properties of the data return function through vm._data. XXX.

  2. To define the data function to return the object traversal, proxy each value vm._data. XXX to the vm.

function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function' ?
    data.call(vm, vm) :
    data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (methods && hasOwn(methods, key)) {
      console.log(`Method "${key}" has already been defined as a data property.`, vm)
    }
    if (props && hasOwn(props, key)) {
      console.log(`The data property "${key}" is already declared as a prop. Use prop default value instead.`, vm)
    } else if(! isReserved(key)) {XXX -> vm._data. XXX -> vm._data. XXX -> vm._data
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true)}Copy the code

proxy

Delegate each value vm._data. XXX to vm. XXX.

This is a common method. Here we’re just proxying the data definition for the property. Vue also uses this method to proxy(VM, ‘_props’, key).

// Proxy (vm, '_data', key).
function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true.configurable: true.get: function proxyGetter() {
      // initData treats vm._data as a response object.
      / / here to return to this [' _data while forming] [key], implement vm [key] - > vm. _data while forming [key]
      return this[sourceKey][key]
    },
    set: function proxySetter(val) {
      This ['_data'][key] is the same as this['_data'].
      this[sourceKey][key] = val
    }
  })
}
Copy the code

observe

Observe’s function is to monitor data changes.

function observe(value) {
  if(! isObject(value)) {return
  }
  return new Observer(value);
}
Copy the code

Observer

An Observer is a class that adds getters and setters to an object’s properties for dependency collection and distribution of updates

class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    this.walk(value)
  }
  walk (obj) {
    // The defineReactive method is called to create a reactive object by iterating over the key of the data object
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
Copy the code

defineReactive

The function of defineReactive is to define a reactive object and dynamically add getters and setters to it. The getters do what depends on collection and the setters do what sends out updates.

function defineReactive (obj, key) {
  // Initialize the Dep for dependency collection
  const dep = new Dep()

  let val = obj[key]

  // Call the observe method recursively on the child, which ensures that no matter how complex obj's structure is,
  // All of its child attributes can also become responsive objects,
  // This allows us to access or modify a deeply nested property in obj and also trigger getters and setters.
  // Make it possible to implement responsiveness in multi-layer objects such as foo.bar.
  let childOb = observe(val)
  // object.defineProperty to add getters and setters to obj's property key
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      // dep. target points to Watcher
      if (Dep.target) {
        // Depending on the collection, every time a value in data is used, get is called once and it is collected into an array.
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return val
    },
    set: function reactiveSetter (newVal) {
      // If the value does not change, return directly
      if (newVal === val) {
        return
      }
      // Set a new value for val
      val = newVal
      // If the new value is an object, observe again.
      childOb = observe(newVal)
      dep.notify()
    }
  })
}
Copy the code

4. Rely on collecting and distributing updates

Dep

The Dep is the heart of the whole collection of getter dependencies. The special thing to note here is that it has a static property target, which is a globally unique Watcher. This is a very clever design because only one global Watcher can be evaluated at any one time. And its own property subs is an array of Watcher.

Dep is actually a management of Watcher, and it makes no sense for Dep to exist independently of Watcher.

class Dep {
  static target;
  constructor () {
    // Where to store the watcher
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// Send updates
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null
Copy the code

Watcher

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.cb = cb
    this.expOrFn = expOrFn;
    this.depIds = {};
    // expOrFn is a function, if not, it will be converted to a function by parsePath.
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // parsePath expOrFn into a function
      this.getter = parsePath(expOrFn) || function noop (a, b, c) {}}// Triggers dependency collection.
    this.value = this.get()
  }
  get() {
    // Dep.target refers to the watcher itself, and the value triggers the getter method for the property.
    // The dep. target used in the getter method has a value.
    Dep.depend () -> dep.target.adddep (dep) -> dep.addSub(watcher)
    // Add watcher to subs array.
    // Delete dep. target to ensure that there is only one dep. target at a time.
    Dep.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    Dep.target = null;
    return value
  }
  addDep(dep) {
    if (!this.depIds.hasOwnProperty(dep.id)) {
      dep.addSub(this);
      this.depIds[dep.id] = dep;
    }
  }
  update() {
    // this.value is a value that Watcher caches to compare with the changed value. If the value does not change, it is not updated.
    const value = this.get()
    const oldValue = this.value
    if(value ! == oldValue) {// Cache the new value for the next operation
      this.value = value
      // call cb with vm as this.
      // cb is the update function passed in new watcher. It's going to pass in the new value and update it to the view through the update function.
      this.cb.call(this.vm, value, oldValue)
    }
  }
}
Copy the code

3. Process analysis

  1. new MVVM()When, first of all, will be rightdata,propscomputedInitialize them to become responsive objects.
  2. Reactive is by usingObject.definePropertySets the properties of an objectget,setProvides getters and setters for properties. Once an object has a getter and setter, we can simply call it a reactive object.
  3. Getter methods are fired when we access the property, and setter methods are fired when we modify the property.
  4. Do dependency collection in getter methods. Because when a property is used, the getter is triggered, and the use is recorded, subsequent changes to the property will be updated based on the collected record.
  5. Do the dispatch update in the setter method. Because this setter is triggered when you make changes to the property, you can update it based on the records collected in the getter.
  6. In the implementation of a getter, it’s throughDepImplementation depends on collection. In the getter methodDep.depend()To collect,Dep.depend()Is called again inDep.target.addDep(this)
  7. hereDep.targetIt’s a very clever design, because at the same timeDep.targetIt only points to oneWatcher, such that there can be only one global at a timeWatcherBy calculation.
  8. Dep.target.addDep(this)Is equal to the callWatcher.addDep(dep)Inside, it’s called againdep.addSub(this)Add this globally unique watcher todep.subsArray, collected, and Watcher itself passeddepIdsCollected and heldDepInstance.
  9. The above is just a process defined, but you need to access the data object to trigger the getter to make the process work. So when does that happen?
  10. Vue will passcompileCompile the template intorenderDelta function and delta functionrenderFunction to access the data object to trigger the getter. Here we are directly incompileTrigger the getter when accessing the data object.
  11. compileResponsible for content rendering and data update.compileCompiling the contents of the template, replacing the {{xx}} string in the template with the corresponding property value will access the data object to fire the getter, but not yetwatcherWithout relying on collection.
  12. compileIt will be instantiated nextWatcherThe instantiation will fetch the value again, and then the getter will be triggered for dependency collection. Specific seeWatcherConstructor and get method implementation.
  13. At this point, the page rendering is complete and the dependency collection is complete.
  14. Changes to the data are then monitored, and if the data changes, the setter method of the property value is triggered, which in addition to setting the value to the new value, also dispatches updates. performdep.notify(), loop callsubsInside preservedwatcherupdateMethod to update.

Get video tutorial + source code