preface

The purpose of this paper is to master the responsive principle of Vue2. The learning process is to write a simple version of Vue. From data hijacking, to template compilation, and then to dependency collection, complete their own implementation of the entire data responsive process.

A very basic response

Implement a reactive representation of a property

Check out this article for defineProperty and I won’t cover it here.

πŸ‘‰πŸ‘‰ object. defineProperty can all “define” what?

We first encapsulate a reactive processing method defineReactive, which redefines the get and set descriptors of object attributes through defineProperty to achieve data hijacking. Get will be triggered every time data is read. The set is triggered every time data is updated, so we can trigger the update view method update in the set to achieve a basic reactive processing.

/ * * *@param {*} Obj Target object *@param {*} Key An attribute * of the target object@param {*} Val The initial value of an attribute of the target object */
function defineReactive(obj, key, val) {
  // Use this method to intercept data
  Object.defineProperty(obj, key, {
    // When reading data, it goes here
    get() {
      console.log('πŸš€ πŸš€ ~ get:', key);
      return val
    },
    // Update data will go here
    set(newVal) {
      // Reassignment is triggered only if the new value differs from the old value
      if(newVal ! == val) {console.log('πŸš€ πŸš€ ~ set:', key);
        val = newVal
        // This is where the view update is triggered
        update()
      }
    }
  })
}
Copy the code

Let’s test this by writing some code that changes the value of obj.foo every 1s and defines an update method to modify the contents of the app node.

// html
<div id='app'>123</div>

// js
// Hijack the obj.foo property
const obj = {}
defineReactive(obj, 'foo'.' ')

// Give obj.foo an initial value
obj.foo = new Date().toLocaleTimeString()

// Change the timer to obj.foo
setInterval(() = > {
  obj.foo = new Date().toLocaleTimeString()
}, 1000)

// Update the view
function update() {
  app.innerHTML = obj.foo
}
Copy the code

As you can see, every time obj.foo is modified, the get and set we defined are triggered and the update method is called to update the view. At this point, the simplest and most responsive processing is done.

Handle deep nesting

An object usually has more than one attribute, so when we want to add responsiveness to each attribute, we need to traverse all the attributes of the object and call defineReactive for each key.

/ * * *@param {*} Obj Target object */
function observe(obj) {
  The target of reactive processing must be an object type
  if (typeofobj ! = ='object' || obj === null) {
    return
  }
  // Iterate over obj and respond to each property of obj
  Object.keys(obj).forEach(key= > {
    defineReactive(obj, key, obj[key])
  })
}
// Define object obj
const obj = {
  foo: 'foo'.bar: 'bar'.friend: {
    name: 'aa'}}// Access obj properties, foo and bar are hijacked, and are not displayed in the browser.
obj.bar = 'barrrrrrrr' // => πŸš€πŸš€~ set: bar
obj.foo = 'fooooooooo' // => πŸš€πŸš€~ set: foo

// Access the obj attribute obj.friend.name
obj.friend.name = 'bb' // => πŸš€πŸš€~ get: friend
Copy the code

When we access obj.friend.name, we just print get: friend, not friend.name, so we need to recurse and make the deeper attributes responsive as well.

function defineReactive(obj, key, val) {
  / / recursion
  observe(val)
  
  // Continue with object.defineProperty...
  Object.defineProperty(obj, key, { ... . })}// Access obj.friend.name again
obj.friend.name = 'bb' // => πŸš€πŸš€~ set: name
Copy the code

In defineReactive, we recurse if value is an object, but if it doesn’t return directly, we proceed to make sure all nested properties in obj are handled responsively, so when we call obj.friend.name again, It prints out set: name.

Handles direct assignment of an object

What if I assign an object directly to the property?

const obj = {
  friend: {
    name: 'aa'
  }
}
obj.friend = {           // => πŸš€πŸš€~ set: friend
  name: 'bb'
}
obj.friend.name = 'cc'   // => πŸš€πŸš€~ get: friend
Copy the code

This assignment only prints get: friend, but does not hijack obj.friend.name. We just need to check the type of value when firing set. If it is an object type, we observe it.

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, { ... .set(newVal) {
      // Reassignment is triggered only if the new value differs from the old value
      if(newVal ! == val) {console.log('πŸš€ πŸš€ ~ set:', key);
        // If newVal is an object type, do reactive processing again.
        if (typeof obj === 'object'&& obj ! = =null) {
          observe(newVal)
        }
        val = newVal
      }
    }
  })
}
// Assign an object to obj.friend again
obj.friend = {
  name: 'bb'
}
// Access obj.friend.name again, and this time successfully hijacked the name attribute
obj.friend.name = 'cc'  / / = > πŸš€ ~ set: name
Copy the code

Handle adding a new attribute

The examples above all operate on existing properties, so what if we add a new property?

const obj = {}
obj.age = 18
obj.age = 20
Copy the code

When we try to modify obj.age, nothing is printed, indicating that no responsive processing is being done to obj.age. This also makes sense because the newly added attribute is not handled by defineReactive, so we need a method to handle the newly added attribute manually.

/ * * *@param {*} Obj Target object *@param {*} Key An attribute * of the target object@param {*} Val The initial value of an attribute of the target object */
function $set(obj, key, val) {
  // Vue makes a lot of decisions here, whether val is an object or an array, etc. We'll keep it simple
  defineReactive(obj, key, val)
}

// Call the $set method to add new attributes to obj
$set(obj, 'age'.18)

// Access obj.age again
obj.age = 20 / / = > πŸš€ πŸš€ ~ set: the age
Copy the code

When we update obj. Age again, we print set: age, which is reactive processing.

Data responsiveness in VUE

Implement a simple Vue

This is the most basic way to use Vue. Create an instance of Vue and then use the reactive data defined in Data in the template. Today we will complete a simplified version of Vue.

<div id='app'>
  <p>{{counter}}</p>
  <p>{{counter}}</p>
  <p>{{counter}}</p>
  <p my-text='counter'></p>
  <p my-html='desc'></p>
  <button @click='add'>Click on the add</button>
  <p>{{name}}</p>
  <input type="text" my-model='name'>
</div>

<script>
  const app = new MyVue({
    el: "#app".data: {
      counter: 1.desc:  
    },
    methods: {
      add() {
        this.counter++
      }
    }
  })
</script>
Copy the code

The principle of

Introduction to Design Types

  • MyVue: frame constructor
  • Observer: Perform data reactivity (distinguish between objects and arrays)
  • Compile: Compile templates, initialize views, collect dependencies (update functions, createwatcher)
  • Watcher: Performs the update function (updatedom οΌ‰
  • Dep: Manage multipleWatcherBatch update

Process analytical

  • Pass during initializationObserverReactive processing of data inObserver ηš„ get“To create oneDepIs used to notify updates.
  • Pass during initializationCompileCompile, parse the template syntax, and find the dynamically bound data fromdataGets the data and initializes the view, replacing the template syntax with data.
  • Make a subscription at the same time and create oneWatcher, define an update function, when the data changes in the future,WatcherThe update function will be calledWatcherAdded to thedepIn the.
  • WatcherIt’s one to one for a specific element,dataA property in May appear multiple times in a view, that is, create multipleWatcher, so aDepChina will manage manyWatcher.
  • whenObserverWhen I hear the data change,DepInform allWatcherUpdate the view.

Code implementation – the first round of data responsive

observe

The Observe method makes a small change from the above. Instead of iterating through the defineReactive call directly, it creates an instance of the Observer class.

// Iterate over obj to respond to each of its attributes
function observe(obj) {
  The target of reactive processing must be an object type
  if (typeofobj ! = ='object' || obj === null) {
    return
  }
  new Observer(obj)
}
Copy the code

The Observer class

The Observer class, as explained earlier, is used to do data responses, internally differentiating between objects and arrays, and then performing different reactive schemes.

// Make a response based on the type of value passed in
class Observer {
  constructor(value) {
    this.value = value
    if (Array.isArray(value)) {
      // The todo branch is the responsive handling of an array and is not the focus of this article
    } else {
      // This branch is the responsive handling of the object
      this.walk(value)
    }
  }

  // The object's reactive processing is just another layer of functions wrapped around it
  walk(obj) {
    // Iterate over obj and respond to each property of obj
    Object.keys(obj).forEach(key= > {
      defineReactive(obj, key, obj[key])
    })
  }
}
Copy the code

MVVM Class (MyVue)

This.$data. Key = this.key = this.key;

class MyVue {
  constructor(options) {
    // Save the data
    this.$options = options
    this.$data = options.data

    // data reactive processing
    observe(this.$data)

    // The proxy mounts all attributes on this.$data to the vue instance
    proxy(this)}}Copy the code

The proxy proxy is also very easy to understand by changing a reference via Object.defineProperty.

/** * The proxy mounts all attributes on this.$data to the vue instance@param {*} Vm vue instance */
function proxy(vm) {
  Object.keys(vm.$data).forEach(key= > {
    // Proxy through object.defineProperty so that accessing this.key is equivalent to accessing this.$data.key
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key]
      },
      set(newValue) {
        vm.$data[key] = newValue
      }
    })
  })
}
Copy the code

Code implementation – Second round template compilation

VNode is not the focus of this article, so leave out the VNode part and it’s all in the comments

// Parse the template syntax
{{}}}
// 2. Handle instructions and events
// 3. Initialize and update the above two
class Compile {
  / * * *@param {*} El Host element *@param {*} Vm vue instance */
  constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    // If the element exists, compile
    if (this.$el) {
      this.compile(this.$el)
    }
  }

  / / compile
  compile(el) {
    // Get the child nodes of el, determine their types and do the corresponding processing
    const childNodes = el.childNodes
    childNodes.forEach(node= > {
      This article focuses on elements and text and does not consider other types
      if (node.nodeType === 1) { // This branch represents a node whose type is element
        // Get the attribute on the element
        const attrs = node.attributes
        // Convert attrs to a real array
        Array.from(attrs).forEach(attr= > {
          // Commander my-xxx = 'ABC
          // Get the node property name
          const attrName = attr.name
          // Get the node attribute value
          const exp = attr.value
          // Determine if the node attribute is an instruction
          if (attrName.startsWith('my-')) {
            // Get the specific instruction type, the XXX part after my-xxx
            const dir = attrName.substring(3)
            // Execute this instruction if this[XXX] instruction exists
            this[dir] && this[dir](node, exp)
          }
        })
      } else if (this.isInter(node)) { // This branch represents a node whose type is text and is an interpolation syntax {{}}
        // Text initialization
        this.compileText(node)
      }
      // Recursively traverses the DOM tree
      if (node.childNodes) {
        this.compile(node)
      }
    })
  }

  // Compile text
  compileText(node) {
    $1 {{key}} {{key}}
    // this.$vm[RegExp.$1] = this.$vm[key]
    $vm[key] = this.$vm[key]
    node.textContent = this.$vm[RegExp.$1]
  }

  // the method corresponding to the my-text instruction
  text(node, exp) {
    My-text = 'key' // This command is used to modify the text of the node.
    $vm[key] = this.$vm[key]
    node.textContent = this.$vm[exp]
  }

  // the method corresponding to the my-html directive
  html(node, exp) {
    My-html = 'key'; my-html = 'key'; my-html = 'key'
    // Assign this.$vm[key] to innerHTML
    node.innerHTML = this.$vm[exp]
  }

  {{}}}
  isInter(node) {
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
  }
}
Copy the code

Code implementation – Third round collecting dependencies

Every attribute key in data that will be used in a view can be called a dependency. The same key may appear several times, and every time it appears, a Watcher will be created for maintenance. These Watcher need to be collected for unified management, and this process is called collecting dependencies.

Multiple Watchers created with the same key require a Dep to manage and notify them uniformly when updates are needed.

In the above code, name1 is used twice to create two Watcher, Dep1 collects both Watcher, name2 uses it once to create one Watcher, Dep2 collects the one Watcher.

Collect ideas for dependencies

  • defineReactiveTime is for each of themkeyTo create aDepThe instance
  • When the view is initialized, akey, e.g.name1, create aWatcher1
  • Because the triggername1 ηš„ getterMethod, then willWatcher1Added to thename1The correspondingDep δΈ­
  • whenname1Is triggered when an update occurssetter, you can pass the correspondingDepNotify all that it managesWatcherUpdate the view

Watcher class

When a Watcher instance is created, the instance is assigned to dep. target, data.key is read manually, get in defineReactive is triggered, and the current Watcher instance is added to the Dep for management. Target is then set to null.

// Listener: responsible for updating dependencies
class Watcher {
  / * * *@param {*} Vm Vue instance *@param {*} Key Watcher instance data.key *@param {*} Cb updates the function */
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn

    // Triggers a dependency collection that assigns the current Watcher to the Dep static property target
    Dep.target = this
    // Deliberately read the value of data.key to trigger get in defineReactive
    this.vm[this.key]
    // Collect dependencies and set them to null later
    Dep.target = null
  }

  // The update method will be called by Dep in the future
  update() {
    // Perform the actual update operation
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
Copy the code

Dep class

AddDep collects Watchers and manages it in DEPS. Notify notifies all Watchers in DEPS to update their views.

class Dep {
  constructor() {
    this.deps = [] / / store Watchers
  }
  / / collect Watchers
  addDep(dep) {
    this.deps.push(dep)
  }

  // Notify all Watchers to update the collected Watcher
  notify() {
    this.deps.forEach(dep= > dep.update())
  }
}
Copy the code

To upgrade the Compile

In round 2, our Compile class only implements view initialization, so in round 3 it will be updated to support view updates.

The Watcher instance is created after initialization to listen for updates.

class Compile {... .// There are no changes to the ellipses
    // Here is the code that has changed
  /** * Operates on the DOM node * based on the type of instruction@param {*} Node DOM node *@param {*} Exp this.$vm[key] *@param {*} Dir * / instruction
  update(node, exp, dir) {
    // 1. Initialize the corresponding operation function of the instruction
    const fn = this[dir + 'Updater']
    // Execute if the function exists
    fn && fn(node, this.$vm[exp])
    // 2. Update the value of the actual function corresponding to the call instruction is passed in from outside
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })

  }

  // Compile text {{XXX}}
  compileText(node) {
    $1 {{key}} {{key}}
    // this.$vm[RegExp.$1] = this.$vm[key]
    $vm[key] = this.$vm[key]
    this.update(node, RegExp. $1,'text')}/ / my - text commands
  text(node, exp) {
    this.update(node, exp, 'text')}// the my-text instruction corresponds to the actual operation
  textUpdater(node, value) {
    My-text = 'key' // This command is used to modify the text of the node.
    $vm[key] = this.$vm[key]
    node.textContent = value
  }

  / / my - HTML commands
  html(node, exp) {
    this.update(node, exp, 'html')}// The my-html directive corresponds to the actual operation
  htmlUpdater(node, value) {
    My-html = 'key'; my-html = 'key'; my-html = 'key'
    // Assign this.$vm[key] to innerHTML
    node.innerHTML = value
  }

  {{}}}
  isInter(node) {
    return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent)
  }

}
Copy the code

Watcher makes a connection with Dep

First create a Dep instance in defineReactive with a one-to-one relationship to data.key, and then call dep.addDep in GET for the collection of dependencies. Dep.target is a Watcher. Call dep.notify() in set to notify all Watchers to update the view.

function defineReactive(obj, key, val) {... .// Create a Dep instance that corresponds to the key
  const dep = new Dep()

  // Use this method to intercept data
  Object.defineProperty(obj, key, {
    // When reading data, it goes here
    get() {
      console.log('πŸš€ πŸš€ ~ get:', key);
      // Dep.target is a Watcher
      Dep.target && dep.addDep(Dep.target)

      return val
    },
    // Update data will go here
    set(newVal) {
      // Reassignment is triggered only if the new value differs from the old value
      if(newVal ! == val) {console.log('πŸš€ πŸš€ ~ set:', key);
        // If newVal is an object type, do reactive processing again.
        if (typeof obj === 'object'&& obj ! = =null) {
          observe(newVal)
        }
        val = newVal
        
        // Notification update
        dep.notify()
      }
    }
  })
}
Copy the code

Code implementation – Round 4 events and bidirectional binding

event

Event binding is also easy to understand. First check whether the node’s attribute starts with @, then get the event type (click in the example), then find the function body defined in Methods according to the function name, and finally add event listener.

class Compile {... .// There are no changes to the ellipses
  compile(el) {
      // Determine if the node attribute is an event
      if (this.isEvent(attrName)) {
        // @click="onClick"
        const dir = attrName.substring(1) // click
        // Event listener
        this.eventHandler(node, exp, dir) } } ... .// Determine whether a node is an event that begins with @
  isEvent(dir) {
    return dir.indexOf("@") = = =0
  }
  eventHandler(node, exp, dir) {
    // Get the function body in the configuration item based on the function name
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
    // Add event listener
    node.addEventListener(dir, fn.bind(this.$vm)) } ... . }Copy the code

Two-way binding

In fact, my-Model is also an instruction and follows the processing logic related to the instruction, so we just need to add a Model instruction and the corresponding modelUpdater handler function.

My-model bidirectional binding is actually a syntactic sugar for event binding and value modification. In this article, taking input as an example, other form element binding events will be different, but the principle is the same.

class Compile {

  // my-model=' XXX '
  model(node, exp) {
    // The update method only completes assignment and update
    this.update(node, exp, 'model')
    // Event listener
    node.addEventListener('input'.e= > {
      // Assign the new value to data.key
      this.$vm[exp] = e.target.value
    })
  }

  modelUpdater(node, value) {
    // Assign a value to the form element
    node.value = value
  }

}
Copy the code

It is now possible to update the template compilation flowchart

After the language

Here is a simple version of Vue data response type is completed, the whole process is handwritten from beginning to end, also afraid not to understand the principle?

The full sample code address for this article is πŸ‘‰πŸ‘‰ full code

The resources

Open the full stack architect course