Data driven

  • Data response

The data in the data response is the data model, which is just an ordinary javascript object. When we modify the data, the view will be updated, which avoids tedious DOM operation and improves development efficiency.

  • Two-way binding

Data changes, view changes; Try to change, and the data will change

V-model can be used to create two-way data binding on form elements

  • Data-driven is one of vUE’s most unique features

The development process is focused on the data itself, not how the data is rendered to the view. Okay

The core principle of data response

vue.2x–Object.defineProperty

When you pass a normal JavaScript object to a Vue instance as the data option, Vue will iterate through all the properties of that object, Use object.defineProperty to turn all of these properties into getters/setters. Object.defineproperty is a non-shim feature in ES5, which is why Vue does not support IE8 and earlier browsers.

<! DOCTYPEhtml>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>DefineProperty Basic use - adapt a property to a getter and setter</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // Simulate the data option in Vue
    let data = {
      msg: 'hello'
    }

    // Simulate an instance of Vue
    let vm = {}

    // Data hijacking: Do some intervention when accessing or setting vm members
    Object.defineProperty(vm, 'msg', {
      // Enumerable (traversable)
      enumerable: true.// Configurable (can be deleted with delete, can be redefined with defineProperty)
      configurable: true.// Execute when a value is obtained
      get () {
        console.log('get: ', data.msg)
        return data.msg
      },
      // When a value is set
      set (newValue) {
        console.log('set: ', newValue)
        if (newValue === data.msg) {
          return
        }
        data.msg = newValue
        // Update the DOM value when the data changes
        document.querySelector('#app').textContent = data.msg
      }
    })

    / / test
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>
</body>
</html>
Copy the code
<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>DefineProperty Multiple members</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // Simulate the data option in Vue
    let data = {
      msg: 'hello'.count: 10
    }

    // Simulate an instance of Vue
    let vm = {}

    proxyData(data)

    function proxyData(data) {
      // Iterate over all properties of the data object
      Object.keys(data).forEach(key= > {
        // Convert attributes in data to setters/setters for vm
        Object.defineProperty(vm, key, {
          enumerable: true.configurable: true,
          get () {
            console.log('get: ', key, data[key])
            return data[key]
          },
          set (newValue) {
            console.log('set: ', key, newValue)
            if (newValue === data[key]) {
              return
            }
            data[key] = newValue
            // Update the DOM value when the data changes
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }

    / / test
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>
</body>
</html>
Copy the code

vue3.x–Proxy

Proxy, used in ve3. X (new in ES6, not IE, performance optimized by browser), listens directly on objects, not properties.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Proxy</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // Simulate the data option in Vue
    let data = {
      msg: 'hello'.count: 0
    }

    // Simulate the Vue instance
    let vm = new Proxy(data, {
      // The function that performs the agent behavior
      // When accessing members of the VM will be executed
      get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
      },
      // When setting vm members will be executed
      set (target, key, newValue) {
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) {
          return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
      }
    })

    / / test
    vm.msg = 'Hello World'
    console.log(vm.msg)
  </script>
</body>
</html>
Copy the code

Publish subscribe and observer patterns

Publish subscribe model

Assume that there is a “signal center”, a task is completed, to the signal center “publish” (subscribe) a signal, other tasks to the signal center “subscribe” (subscribe) this signal, so as to know when to start to execute, this is the publish subscribe mode.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Vue User-defined events</title>
</head>
<body>
  <script src="./js/vue.js"></script>
  <script>
    // Create a vue instance
    let vm = new Vue()

    // Register event (subscription message)
    vm.$on('dataChange'.() = > {
      console.log('dataChange')
    })

    vm.$on('dataChange'.() = > {
      console.log('dataChange1')})// Trigger event (publish message)
    vm.$emit('dataChange')
  </script>
</body>
</html>
Copy the code

Sibling component communication process

// eventBus.js
// Event center
let eventHub = new Vue()
// ComponentA.vue
/ / publisher
addTodo: function() {  
  // Publish a message (event)
  eventHub.$emit('add-todo', { text: this.newTodoText })  
  this.newTodoText = ' ' 
} 
// ComponentB.vue 
/ / subscriber
created: function() {  
  // Subscribe to message (event)
  eventHub.$on('add-todo'.this.addTodo)
}
Copy the code

Simulate the implementation of vUE custom events

<! DOCTYPEhtml>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Publish subscribe model</title>
</head>
<body>
  <script>
    // Event triggers
    class EventEmitter {
      constructor () {
        // object.create (null) This method sets the Object with no stereotype properties, which can improve performance
        this.subs = Object.create(null)}// Register the event
      $on (eventType, handler) {
        this.subs[eventType] = this.subs[eventType] || []
        this.subs[eventType].push(handler)
      }

      // Triggers the event
      $emit (eventType) {
        if (this.subs[eventType]) {
          this.subs[eventType].forEach(handler= > {
            handler()
          })
        }
      }
    }

    / / test
    let em = new EventEmitter()
    em.$on('click'.() = > {
      console.log('click1')
    })
    em.$on('click'.() = > {
      console.log('click2')
    })

    em.$emit('click')
  </script>
</body>
</html>
Copy the code

Observer mode

In contrast to the publish/subscribe pattern, the observer pattern has no event center, only the subscriber and the publisher, and the publisher needs to know that the subscriber exists.

The Watcher itself has an update method that is called for all subscribers when an event occurs. In vUE’s responsive mechanism, the observer’s update method is called when the data changes. The update method internally updates the view.

In observer mode, the subscriber’s update method is called by the publisher. The target (i.e., the publisher, Dep) keeps track of all subscribers within the publisher, and the publisher notifishes all subscribers when an event occurs. The publisher needs an internal attribute subs to keep track of all subscribers. This attribute is an array to which all observers that depend on the publisher need to be added, so you need to add an addSub() to the observer. This method adds observers to the array. The publisher also needs a notify() method to call update() on all observers when the event occurs.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Observer mode</title>
</head>
<body>
  <script>
    // Publisher - target
    class Dep {
      constructor () {
        // Record all subscribers
        this.subs = []
      }
      // Add subscribers
      addSub (sub) {
        if (sub && sub.update) {
          this.subs.push(sub)
        }
      }
      // Issue a notification
      notify () {
        this.subs.forEach(sub= > {
          sub.update()
        })
      }
    }
    // Subscriber - observer
    class Watcher {
      update () {
        console.log('update')}}/ / test
    let dep = new Dep()
    let watcher = new Watcher()

    dep.addSub(watcher)

    dep.notify()
  </script>
</body>
</html>
Copy the code

conclusion

The observer pattern is scheduled by a specific target, such as the current event, and Dep calls the observer method, so there is a dependency between the observer pattern subscriber and the publisher.

The publish/subscribe pattern is invoked by a unified scheduling center, so publishers and subscribers do not need to be aware of each other’s existence.

Simulation of vUE response principle

Vue

  • function
    • Responsible for receiving initialization parameters (options)
    • Responsible for injecting properties from data into Vue instances, turning them into getters/setters
    • Is responsible for calling an observer to listen for changes to all properties in the data
    • Responsible for calling compiler parsing instructions/interpolation
  • code
// vue.js
class Vue {
  constructor (options) {
    // 1. Save the data of the option through the attribute
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2. Convert data members into getters and setters and inject them into vue instance
    this._proxyData(this.$data)
    // 3. Call the Observer object to listen for data changes
    new Observer(this.$data)
    // 4. Call the Compiler object to parse the instructions and difference expressions
    new Compiler(this)}// Proxy properties
  _proxyData (data) {
    // Iterate over all attributes in data
    Object.keys(data).forEach(key= > {
      // Insert the data attribute into the vue instance
      Object.defineProperty(this, key, {
        enumerable: true.configurable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}
Copy the code

Observer

  • function
    • Is responsible for converting the properties in the Data option to responsive data
    • A property in data is also an object, and that property is converted to reactive data
    • Data change notification is sent
  • code
// observer.js
// Responsible for data hijacking
// Convert the member in $data to a getter/setter
class Observer {
  constructor (data) {
    this.walk(data)
  }
  walk (data) {
    // 1. Check whether data is an object
    if(! data ||typeofdata ! = ='object') {
      return
    }
    // 2. Iterate over all properties of the data object
    Object.keys(data).forEach(key= > {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (obj, key, val) {
    let that = this
    // Create a DEP object that collects dependencies and sends notifications
    let dep = new Dep()
    // If val is an object, convert the properties inside val to reactive data
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true.configurable: true.// getter
      get () {
        // Collect dependencies
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      // setter
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        that.walk(newValue)
        // Send a notification when the data changes
        dep.notify()
      }
    })
  }
}
Copy the code

Compiler

  • function
    • Responsible for compiling templates, parsing instructions/interpolation
    • Responsible for the first rendering of the page
    • Rerender the view when the data changes
  • code
// compiler.js
// Responsible for parsing instructions/interpolation
class Compiler {
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    // Compile the template
    this.compile(this.el)
  }
  // Compile the template to handle text nodes and element nodes
  compile (el) {
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node= > {
      // Process text nodes
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // Process element nodes
        this.compileElement(node)
      }

      // Check whether the node has children. If there are children, call compile recursively
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  /* compileElement handles the first render of the V-text and the first render of the V-Model */
  // Compile element nodes to process instructions
  compileElement (node) {
    // console.log(node.attributes)
    // Iterate over all attribute nodes
    Array.from(node.attributes).forEach(attr= > {
      // Get the name of the element attribute
      let attrName = attr.name
      // Check whether the current attribute name is a directive
      if (this.isDirective(attrName)) {
        // attrName 的形式 v-text  v-model
        // Capture the name of the property and get the text Model
        attrName = attrName.substr(2)
        let key = attr.value
        // Handle different instructions
        this.update(node, key, attrName)
      }
    })
  }
  // Responsible for updating the DOM
  / / create a Watcher
  update (node, key, attrName) {
    // node, attribute name of key data, second half of dir directive
    let updateFn = this[attrName + 'Updater']
    // Because we use this in textUpdater and so on
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  // Process the V-text instruction
  textUpdater (node, value, key) {
    node.textContent = value
    // Create a watcher object for each instruction/interpolation to monitor data changes
    new Watcher(this.vm, key, (newValue) = > {
      node.textContent = newValue
    })
  }
  // Update the data when the view changes
  // Process the v-model instruction
  modelUpdater (node, value, key) {
    node.value = value
    // Create a watcher for each instruction to watch the data change
    new Watcher(this.vm, key, (newValue) = > {
      node.value = newValue
    })
    // Bind both ways to listen for changes in the view
    node.addEventListener('input'.() = > {
      this.vm[key] = node.value
    })
  }
  // Compile the text node to handle differential expressions
  compileText (node) {
    // {{ msg }}
    let reg = / \ {\ {(. +?) \} \} /
    // Get the contents of the text node
    let value = node.textContent
    if (reg.test(value)) {
      // The value in the interpolation is the name of the property
      let key = RegExp.$1.trim()
      // Replace the interpolation with a concrete value
      node.textContent = value.replace(reg, this.vm[key])

      // Create a watcher object to update the view when data changes
      new Watcher(this.vm, key, (newValue) = > {
        node.textContent = newValue
      })
    }
  }
  // Determine whether an element attribute is a directive
  isDirective (attrName) {
    return attrName.startsWith('v-')}// Check whether the node is a text node
  isTextNode (node) {
    return node.nodeType === 3
  }
  // Check whether the node is an element node
  isElementNode (node) {
    return node.nodeType === 1}}Copy the code

Dep

  • function
    • Collect dependencies, add Watcher
    • Inform all observers
  • code
// dep.js
class Dep {
  constructor () {
    // Store all observers
    this.subs = []
  }
  // Add an observer
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // Send a notification
  notify () {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
Copy the code

Watcher

  • function
    • When data changes trigger dependencies, DEP informs all Watcher instances to update the view
    • Adds itself to the DEP object when it instantiates itself
// watcher.js
class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    // Attribute name in data
    this.key = key
    // Call cb to update the view when the data changes
    this.cb = cb
    // Record the current watcher object on the static property of the Dep and add the watcher to the SUBs of the Dep when data is accessed
    Dep.target = this
    // Trigger a getter that tells DEp to record watcher for the current key
    // Triggers the GET method, which calls addSub
    this.oldValue = vm[key]
    / / clear the target
    Dep.target = null
  }
  // Update the view when the data changes
  update () {
    let newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    this.cb(newValue)
  }
}
Copy the code

debugging

Use debugging to deepen your understanding of the code

  1. Debug the process of first rendering the page
  2. Debugging data changes the process of updating a view
<! DOCTYPEhtml>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Mini Vue</title>
</head>
<body>
  <div id="app">
    <h1>Difference expression</h1>
    <h3>{{ msg }}</h3>
    <h3>{{ count }}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app'.data: {
        msg: 'Hello Vue'.count: 100.person: { name: 'zs'}}})console.log(vm.msg)
    // vm.msg = { test: 'Hello' }
    vm.test = 'abc'
  </script>
</body>
</html>
Copy the code

conclusion

  • Review the overall process through the diagram below

  • Vue
    • Record the options passed in and set data/data/data/ EL
    • Inject the data member into the Vue instance
    • Responsible for calling the Observer to implement responsive data processing (data hijacking)
    • Responsible for calling Compiler instructions/interpolation, etc
  • Observer
    • The data was hijacked
      • Converts members of data into getters/setters
      • Responsible for converting multi-level properties into getters/setters
      • If you assign a property to a new object, set the member of the new object to be a getter/setter
    • Add dependencies between Dep and Watcher
    • Data change notification is sent
  • Compiler
    • Responsible for compiling templates, parsing instructions/interpolation
    • Responsible for the first rendering process of the page
    • Re-render when data changes
  • Dep
    • Collect dependencies, add subscribers (Watcher)
    • Notify all subscribers
  • Watcher
    • Adds itself to the DEP object when it instantiates itself
    • Dep notifishes all Watcher instances to update the view when data changes