Why study the principles section?

Response principle

Data driven

In the process of learning vue.js, we often see three concepts:

  • Data driven
  • Data response
  • Bidirectional data binding

The core principle of responsiveness

Vue 2.x and Vue 3.x have different responsive implementations, which we will cover separately.

  • Vue 2.x response based on ES5 Object. DefineProperty implementation.
    • After setting the data, we iterate through all the properties, turning them into getters and setters, so as to update the view when the data changes.

      Object.defineProperty(obj, 'gender', {
            value: 'male'./ / set the value
            writable: true.// Whether writable
            enumerable: true.// Whether traversable
            configurable: true // Check whether subsequent configurations can be performed
          })
      Copy the code
      var genderValue = 'male'
      Object.defineProperty(obj, 'gender', {
        get () {
          console.log('Any custom action required for fetching')
          return genderValue
        },
        set (newValue) {
          console.log('Custom actions required for any Settings')
          genderValue = newValue
        }
      });
      Copy the code

      Cn.vuejs.org/images/data…

      Vue 2 Response principle:

       <div id="app"Word-wrap: break-word! Important; "> < div style =" text-align: center;<script>
          // Declare a data object that emulates the data attribute of the Vue instance
          let data = {
            msg: 'hello'
          }
          // The object that emulates the Vue instance
          let vm = {}
          // Set the properties of data to getters/setters by data hijacking
          Object.defineProperty(vm, 'msg', {
            / / can traverse
            enumerable: true./ / can be configured
            configurable: true,
            get () {
              console.log('Properties accessed')
              return data.msg
            },
            set (newValue) {
              // Update the data
              data.msg = newValue
              // Update the content of the DOM element in the view
              document.querySelector('#app').textContent = data.msg
            }
          });
        </script>
      Copy the code
    • The above version is only a prototype, with the following problems:

      • Only one property is listened for in the operation. Multiple properties cannot be processed
      • Cannot listen for array changes (also exists in Vue)
      • Unable to handle cases where attributes are also objects
      • So let’s improve on that
        <div id="app"Word-wrap: break-word! Important; "> < div style =" text-align: center;<script>
          // Declare a data object that emulates the data attribute of the Vue instance
          let data = {
            msg1: 'hello'.msg2: 'world'.arr: [1.2.3].obj: {
              name: 'jack'.age: 18}}// The object that emulates the Vue instance
          let vm = {}
      
          // Encapsulate as a function for reactive processing of data
          const createReactive = (function () {
              // -- add array method support --
            const arrMethodName = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse']
            // The object used to store the result of processing, ready to replace the array instance's __proto__ pointer
            const customProto = {}
            // To prevent the array instance from being unable to use other array methods
            customProto.__proto__ = Array.prototype
            arrMethodName.forEach(method= > {
              customProto[method] = function () {
              // Make sure the original function is available (this is the array instance)
                const result = Array.prototype[method].apply(this.arguments)
                // Do other custom functions, such as updating views
                document.querySelector('#app').textContent = this
                return result
              }
            })
      
            // The principal function that requires data hijacking is also the function that recursion requires
            return function (data, vm) {
              // Iterate over all properties of the hijacked object
              Object.keys(data).forEach(key= > {
                // Check whether it is an array
                if (Array.isArray(data[key])) {
                  // Change the current array instance's __proto__ to customProto
                  data[key].__proto__ = customProto
                } else if (typeof data[key] === 'object'&& data[key] ! = =null) {
                  // Check whether it is an object. If it is, perform a recursive operation
                  vm[key] = {}
                  createReactive(data[key], vm[key])
                  return
                }
      
                // Set the properties of data to getters/setters by data hijacking
                Object.defineProperty(vm, key, {
                  enumerable: true.configurable: true,
                  get () {
                    console.log('Properties accessed')
                    return data[key]
                  },
                  set (newValue) {
                    // Update the data
                    data[key] = newValue
                    // Update the content of the DOM element in the view
                    document.querySelector('#app').textContent = data[key]
                  }
                })
              })
            }
      
          })()
      
          createReactive(data, vm);
        </script>
      Copy the code
  • Vue 3.x reactive based on ES6 Proxy implementation.
    • The Proxy review

    ProxyObject to create a proxy for an object that intercepts and customizes basic operations (such as property lookups, assignments, enumerations, function calls, etc.)

Const p = new Proxy(target, handler) // target: The target object to wrap with 'Proxy' (this can be any type of object, including native arrays, functions, or even another Proxy). // handler: an object whose properties, usually functions, define the behavior of the agent 'p' when performing various operations.Copy the code
    <script>
        const data = {
          msg1: 'content'.arr: [1.2.3].obj: {
            name: 'william'.age: 18}}const p = new Proxy(data, {
          get (target, property, receiver) {
            console.log(target, property, receiver)
            return target[property]
          },
          set (target, property, value, receiver) {
            console.log(target, property, value, receiver)
            target[property] = value
          }
        });
  </script>
Copy the code
  • Vue3 responsive principle
 <div id="app"Word-wrap: break-word! Important; "> < div style =" text-align: center;<script>
    const data = {
      msg: 'hello'.content: 'world'.arr: [1.2.3].obj: {
        name: 'william'.age: 18}}const vm = new Proxy(data, {
      get (target, key) {
        return target[key]
      },
      set (target, key, newValue) {
        // Update data
        target[key] = newValue
        // Update the view
        document.querySelector('#app').textContent = target[key]
      }
    });
  </script>

Copy the code

Related Design Patterns

Design pattern (DESIGN pattern) is a solution to a variety of common problems in software design.

Observer mode

The Observer pattern refers to the definition of a one-to-many (observed and multiple observers) association between objects. When an object changes state, all other related objects are notified and automatically refreshed.

Core concepts:

  • The Observer the Observer
  • The Subject of observation

<script>
    // The object of observation
    // 1 Add observer
    // 2 Notify all observers
    class Subject {
      constructor () {
        // Store all observers
        this.observers = []
      }
      // Add the observer function
      addObserver (observer) {
        // Check whether the passed parameter is an observer instance
        if (observer && observer.update) {
          this.observers.push(observer)
        }
      }
      // Notify all observers
      notify () {
        // Call the update method for each observer in the observer list
        this.observers.forEach(observer= > {
          observer.update()
        })
      }
    }

    / / observer
    // 1 "update" when a state change occurs to the observed target
    class Observer {
      update () {
        console.log('An event has occurred, act accordingly... ')}}// Functional test
    const subject = new Subject()
    const ob1 = new Observer()
    const ob2 = new Observer()

    // Add the observer to the object to be observed
    subject.addObserver(ob1)
    subject.addObserver(ob2)

    // Tell the observer to perform the operation (in certain scenarios)
    subject.notify();

  </script>
Copy the code

Publish and subscribe

The publish-subscribe pattern can be considered as an advanced decoupled version of the observer pattern. Its characteristics are as follows:

  • By adding a message center between the publisher and the subscriber, all messages are managed through the message center, and the publisher and subscriber are not directly connected, realizing the decoupling of the two.

Core concepts:

  • Message Center Dep
  • Subscribers to the Subscriber
  • The Publisher Publisher

  <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
  <script>
    // Create a Vue instance (message center)
    const eventBus = new Vue()

    // Register event (set subscriber)
    eventBus.$on('dataChange'.() = > {
      console.log('Event Handling function 1')
    })

    eventBus.$on('dataChange'.() = > {
      console.log('Event Handling 2')})// Trigger event (set publisher)
    eventBus.$emit('dataChange');
  </script>
Copy the code

Summary of design patterns

The observer mode consists of the observer and the object of observation and is suitable for operations within the component.

  • Feature: When a special event occurs, the observation object uniformly notifies all observers.

The publish/subscribe pattern, which consists of a publisher and a subscriber and a message center, is more suitable for complex message types.

  • Feature: When a special event occurs, the message center receives an order to publish and sends a message to the corresponding subscriber based on the event type.

Vue response-based principle simulation

The overall analysis

To simulate Vue’s implementation of responsive data, we first look at the structure of a Vue instance and analyze what attributes and functions are implemented.

Vue

  • Goal: To inject data data into the Vue instance for easy operation within the method.

Observer (publisher)

  • Target: Data hijacking, listening for changes in data and notifying Dep of changes

Dep (Message Center)

  • Target: Store subscribers and manage message delivery

Watcher (subscriber)

  • Target: Subscribe to data changes for view updates

Compiler

  • Objective: To parse the instructions and interpolations in the template and replace them with the corresponding data

Vue class

Function:

  • Receiving configuration Information
  • Convert data properties to getters and setters and inject them into the Vue instance.
  • * Listen for all property changes in data and set it to responsive data
  • * Call parsing functions (parsing interpolation, instructions, etc.)

class Vue {
  constructor (options) {
    // 1 Store properties
    this.$options = options || {}
    this.$data = options.data || {}
    // Determine the type of the EL value and process it accordingly
    const { el } = options
    this.$el = typeof el === 'string' ? document.querySelector(el) : el

    // 2 Inject the data attribute into the Vue instance
    _proxyData(this.this.$data)

    // *3. Create an Observer instance to monitor changes in data properties
    new Observer(this.$data)

    // *4
    new Compiler(this)}}// Inject the attributes of data into the Vue instance
function _proxyData (target, data) {
  Object.keys(data).forEach(key= > {
    Object.defineProperty(target, key, {
      enumerable: true.configurable: true,
      get () {
        return data[key]
      },
      set (newValue) {
        data[key] = newValue
      }
    })
  })
}
Copy the code

The observer class

Function:

  • Monitor attribute changes in data through data hijacking and notify the message center Dep when changes occur
  • Consider that the properties of data may also be objects and are converted to reactive data
class Observer {
  // Take an incoming object and turn its properties into getters/setters
  constructor (data) {
    this.data = data
    // Iterate through the data
    this.walk(data)
  }
  // Encapsulates the method used for data traversal
  walk (data) {
    // Convert all traversed properties to getters and setters
    Object.keys(data).forEach(key= > this.convert(key, data[key]))
  }
  // Encapsulates the method used to transform an object into responsive data
  convert (key, value) {
    defineReactive(this.data, key, value)
  }
}

// Used to define a responsive property for an object
function defineReactive (data, key, value) {
  // Create a message center
  const dep = new Dep()

  // Check whether it is an object, and if so, create a new Observer instance to manage
  observer(value)

  // Perform data hijacking
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true,
    get () {
      console.log('Got a property')
      // * Add subscribers when getters are fired
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set (newValue) {
      console.log('Set properties')
      if (newValue === value) return
      value = newValue
      observer(value)

      // * Notify the message center when data changes
      dep.notify()
    }
  })
}

function observer (value) {
  if (typeof value === 'object'&& value ! = =null) {
    return new Observer(value)
  }
}
Copy the code

Dep class

  • Dep is short for Dependency, meaning “Dependency,” and refers to the Dependency that Dep is used to collect and manage between subscribers and publishers
  • function
    • * Store dependencies for each data collection corresponding to the dependencies
    • Add and store subscribers
    • Inform all observers when data changes
class Dep {
  constructor () {
    // Store subscribers
    this.subs = []
  }
  // Add subscribers
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // How to notify the subscriber
  notify () {
    // Iterate over the subscribers and perform the update function
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
Copy the code

Watcher class

Function:

  • When you instantiate Watch, add yourself to the DEP object
  • When data changes trigger deP, deP informs all corresponding Watcher instances to update the view
class Watcher {
  constructor (vm, key, cb) {
    // The current Vue instance
    this.vm = vm
    // Attribute name of the subscription
    this.key = key
    // The callback to be performed after the data changes
    this.cb = cb

    // Store the current subscriber instance to the Dep class before triggering the Getter
    Dep.target = this
    // Record the value of the property before the change, which is used for update status detection (causes the property Getter to fire)
    this.oldValue = vm[key]
    // Clear the target to store the next Watcher instance
    Dep.target = null
  }
  // Encapsulates the ability to update views when data changes
  update () {
    const newValue = this.vm[this.key]
    // If the data is unchanged, no update is required
    if (newValue === this.oldValue) return
    // If the data changes, call the updated callback
    this.cb(newValue)
  }
}
Copy the code

Compiler class

The DOM is used here, whereas vue uses the Virtual DOM

Function:

  • Compile the template and parse the internal instructions and interpolations
  • Render the page for the first time
  • Rerender the view when the data changes
class Compiler {
  constructor (vm) {
    this.vm = vm
    this.el = vm.$el

    // Initialize the template compilation method
    this.compile(this.el)
  }
  // Base template method
  compile (el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node= > {
      // Check the type of node (text node, element node)
      if (isTextNode(node)) {
        // Compile the text node content
        this.compileText(node)
      } else if (isElementNode(node)) {
        // Compile the element node content
        this.compileElement(node)
      }
      // Check whether the current node has child nodes
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // Wrap the text node compilation method
  compileText (node) {
    const reg = / \ {\ {(. +?) \}\}/g
    // Remove unnecessary whitespace and newlines from the content
    const value = node.textContent.replace(/\s/g.' ')
    // Declare that the data stores multiple segments of text
    const tokens = []
    // The index of the location that has been operated
    let lastIndex = 0
    // Records the initial index of the currently extracted content
    let index
    let result
    while (result = reg.exec(value)) {
      // The initial index of the extracted content
      index = result.index
      // Process plain text
      if (index > lastIndex) {
        // Store the contents of the middle part in tokens
        tokens.push(value.slice(lastIndex, index))
      }
      // Handle the contents of the interpolation (remove whitespace)
      const key = result[1].trim()
      // Obtain the corresponding attribute value according to the key and store it in tokens
      tokens.push(this.vm[key])
      // Update lastIndex to get the next content
      lastIndex = index + result[0].length
      // Create subscriber Watcher to live subscribe to data changes
      const pos = tokens.length - 1
      new Watcher(this.vm, key, newValue= > {
        // Change the data in tokens
        tokens[pos] = newValue
        node.textContent = tokens.join(' ')})}if (tokens.length) {
      // Render the page initially
      node.textContent = tokens.join(' ')}}// Encapsulate element node processing
  compileElement (node) {
    // Get the attribute node
    Array.from(node.attributes).forEach(attr= > {
      // Save the property name and check the function of the property
      let attrName = attr.name
      if(! isDirective(attrName))return
      // Get the specific name of the directive
      attrName = attrName.slice(2)
      // Get the value of the directive, which represents the name of the responsive data
      let key = attr.value
      // Encapsulate the update method, which is used to assign functions to different instructions
      this.update(node, key, attrName)
    })
  }
  // The method used to assign instructions
  update (node, key, attrName) {
    // Name processing
    let updateFn = this[attrName + 'Updater']
    // Check and call
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }
  / / v - text processing
  textUpdater (node, key, value) {
    // Set the content for the element
    node.textContent = value
    // Subscription data changed
    new Watcher(this.vm, key, newValue= > {
      node.textContent = newValue
    })
  }
  / / v - model processing
  modelUpdater (node, key, value) {
    // Set the data for the element
    node.value = value
    // Subscription data changed
    new Watcher(this.vm, key, newValue= > {
      node.value = newValue
    })
    // Listen for input events to achieve bidirectional binding
    node.addEventListener('input'.() = > {
      this.vm[key] = node.value
    })
  }
}

// Check whether the node is an element node
function isElementNode (node) {
  return node.nodeType === 1
}

// Check whether the node is a text node
function isTextNode (node) {
  return node.nodeType === 3
}

// Check whether the attribute name is a directive
function isDirective (attrName) {
  return attrName.startsWith('v-')}Copy the code

Function review and summary

  • Vue class
    • Inject the data attribute into the Vue instance
    • Call the Observer for responsive data processing
    • Call Compiler to compile the template
  • The Observer class
    • Converts properties of data to getters/setters
    • Add subscriber Watcher to Dep
    • Notify Dep when data changes are sent
  • Dep class
    • Collect dependencies, add subscribers (Watcher)
    • Notify the subscriber
  • Watcher class
    • The subscriber is created when the template is compiled, and the subscription data changes
    • When notified by Dep, the template function in Compiler is called to update the view
  • Compiler
    • Compile templates, parse instructions and interpolations
    • Responsible for the first rendering of the page and re-rendering after data changes

Virtual DOM

Course Objectives:

  • What is the virtual DOM and what does it do
  • Understand how to use the Virtual DOM, Snabbdom basic use
  • Snabbdom source parsing

What is a Virtual DOM

  • The Virtual DOM (Virtual DOM) is a common JS object to describe the DOM object

  • Use the Virtual DOM to describe the real DOM

Why use the Virtual DOM

  • The era of slash-and-burn development at the front end
  • The MVVM framework addresses view and state synchronization issues
  • The template engine simplifies view operations, and there is no way to track state
  • The virtual DOM tracks state changes
  • See github for a description of the motivations for virtual-DOM
    • The virtual DOM maintains the state of the program, tracking the last state
    • Update the real DOM by comparing the two state differences

Version:0.9 StartHTML:0000000105 EndHTML:0000001513 StartFragment:0000000141 EndFragment:0000001473

Functions of the virtual DOM

  • Maintain the relationship between view and state
  • Improved rendering performance in complex view situations
  • cross-platform
    • Browser platform rendering
    • DOM Native applications (Weex/React Native)
    • Server render SSR(nuxt.js/nex.js)
    • Small program (MPvue/UNI-app), etc

Virtual DOM library

  • Snabbdom
    • The virtual DOM used internally in vue.js 2.x is the modified Snabbdom
    • About 200 SLOC (Single line of code)
    • Extensible through modules
    • Source code is developed in TypeScript
    • One of the fastest Virtual DOM
  • virtual-dom

Use of Snabbdom:

  1. Install the Parcel packing tool

  1. Import Snabbdom

Official document: github.com/snabbdom/sn…

  • Install Snabbdom
  • Import Snabbdom
    • Snabbdom’s two core functions init and h()
    • Init () is a higher-order function that returns patch()
    • H () returns the virtual node VNode, which we saw when we used vue.js
  • Importing methods in official documents

  • Parcel/webPack 4 does not support the exports field in package.json

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

// 1 Create a VNode using the h function
let vNode = h('div#box.container'.'New Content')

// Get the mount element
const dom = document.querySelector('#app')

// 2 Get the patch function from init
const patch = init([])

// 3 Render vNode to DOM through patch
let oldVNode = patch(dom, vNode)

// 4 Create a new VNode and update it to oldVNode
vNode = h('p#text.abc'.'This is the content of the p tag.')
patch(oldVNode, vNode)
Copy the code
  1. Contain child nodes
import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'

const patch = init([])

// Create a VNode with child nodes
// - The array of argument 2 is a list of child nodes, and vNode should be passed inside
let vNode = h('div#container', [
  h('h1'.'Title text'),
  h('p'.'Content text')])// Get the mount element
const dom = document.querySelector('#app')

/ / render vNode
const oldVNode = patch(dom, vNode)

// Clear the operation update page, h('! ') to generate a comment node
patch(oldVNode, h('! '))
Copy the code
  1. Snabbdom related content
  • Function of modules
    • Snabbdom’s core library does not handle the properties/styles/events of DOM elements. It can be implemented by registering the modules provided by Snabbdom by default
    • Modules in Snabbdom can be used to extend the functionality of Snabbdom
    • Snabbdom modules are implemented by registering global hook functions
  • Official provided module
    • attributes
      • To set the attributes of a DOM element, use setAttribute ()
      • Deals with properties of Boolean type
    • props
      • Similar to the Attributes module, set the element[attr] = calue for the DOM element
      • Properties of type Boolean are not handled
    • dataset
      • Set custom properties for data-*
    • class
      • Switching class styles
      • Note: Elements are styled with the SEL selector
    • style
      • Set inline styles to support animation
      • delayed/remove/destroy
    • eventlisteners
      • Event listener module
  • Module Usage Steps
    • Import the required modules
    • Registers modules in init()
    • Use the module as the second argument to the h() function
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

// 1 Import module (please spell the name of the import correctly)
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'

// 2 Register the module (add the module's capabilities to the patch function)
const patch = init([
  styleModule,
  eventListenersModule
])

// 3 Use modules
let vNode = h('div#box', {
  style: {
    backgroundColor: 'green'.height: '200px'.width: '200px'
  }
}, [
  h('h1#title', {
    style: {
      color: '#fff'
    },
    on: {
      click () {
        console.log('I clicked the h1 tag')}}},'Here's the title.'),
  h('p'.'Here's the content text.')])const dom = document.getElementById('app')
patch(dom, vNode)
Copy the code

Snabbdom source code parsing

  • How to learn source code
    • The macro understanding
    • Look at the source code with the target in mind
    • The process of looking at the source code should be easy to understand
    • debugging
    • The resources
  • The core of Snabbdom
    • Init () sets the module and creates the patch() function
    • Use the h() function to create a JavaScript object (VNode) that describes the real DOM
    • Patch () Compares the old and new Vnodes
    • Update the changes to the real DOM tree
  • Snabbdom source
    • The source address
      • Github.com/snabbdom/sn…
      • Current version: V2.1.0
    • Cloning code
      • Git clone -b v2.1.0 –depth=1github.com/snabbdom/sn…
  • Introduction to h function
    • Create a VNode object
    • The h function in Vue

  • Function overloading
    • A function with a different number of arguments or argument types
    • There is no concept of overloading in JavaScript
    • TypeScript has overloading, but the implementation of overloading still adjusts the parameters in code
    • Function overload – number of arguments
    • Function overload – Parameter type
  • Vnode function
import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'

export type Key = string | number

export interface VNode {
  sel: string | undefined
  data: VNodeData | undefined
  children: Array<VNode | string> | undefined
  elm: Node | undefined
  text: string | undefined
  key: Key | undefined
}

exportinterface VNodeData { props? : Props attrs? : Attrsclass? :Classes
  style? :VNodeStyle
  dataset? :Dataset
  on? :On
  hero? :Hero
  attachData? :AttachData
  hook? :Hooks
  key? :Key
  ns? :string // for SVGs
  fn? : ()=> VNode // for thunksargs? : any[]// for thunks
  [key: string]: any // for any other 3rd party module
}

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined) :VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
Copy the code
  • Patch overall process analysis
    • patch(oldVnode, newVnode)
    • Render the changed content of the new node to the real DOM, and finally return the new node as the old node for the next processing
    • Check whether the new and old Vnodes are the same (the key and SEL of the node are the same).
    • If it is not the same node, delete the previous content and re-render
    • If the VNode is the same, check whether the new VNode has text. If the VNode has text and is different from the oldVnode’s text, update the text directly
    • If the new VNode has children, check whether the children have changed
  • The init function
    • Receive an array containing the declarations of the module’s related functions, lifecycle functions
    • A higher-order function that returns a patch function