First, the initial experience of the response principle

The core of VUE responsiveness is data-driven view. How do you make the view change when the data changes? If there is an API that can monitor data changes, ES5 provides Object. DefineProperty and ES6 provides a Proxy to monitor data.

Next, the implementation of vue2. X and VUe3. X response is simulated

1. vue2.x

Responsive Principle = Object.defineProperty + Observer Mode (more on later)

<body>
	<h1 id="elm">{{name}}</h1>
	<button id="btn">Click to initialize rendering</button>
</body>
Copy the code
// vue 2.x converts data into a response
    const elm = document.querySelector('#elm')
    // 1. Reactive initialization
    const data = {
      name: 'reborn'.age: 18.hobbies: {
        a: 'play game'.b: 'play ball'}}// Assume that the VM is a vue instance
    const vm = {}

    // Inject attributes from data into the vue instance. (This is why we can access data in data with this)
    Reflect.ownKeys(data).forEach(key= > {
      Object.defineProperty(vm, key, {
        enumerable: true.configurable: true.// Get the hijacking of the operation
        get() {
          return data[key]
        },
        // Set the hijacking of the new value operation
        set(newVal) {
          data[key] = newVal
        }
      })
    })

    // Convert the data to the response. Simple demo does not consider nested objects
    Reflect.ownKeys(data).forEach(key= > {
      let value = data[key]
      Object.defineProperty(data, key, {
        enumerable: true.configurable: true.// The reason for returning value is as follows: 1. If data[key] continues to be returned, get hijacking will continue to be triggered, resulting in call stack overflow. Use closures to cache the value variable, get set, and change vlaue.
        get() {
          return value
        },

        // The new object is not considered for the time being.
        set(newVal) {
          if (newVal === value) return
          value = newVal
          // 3. Update the view when the simulated duty changes
          elm.textContent = value
        }
      })
    })

    // 2. Page initialization without interpolation parsing
    document.querySelector('#btn').addEventListener('click'.() = > {
      elm.textContent = vm.name
    })

Copy the code

2. vue3.x

The results obtained and the use

<body>
	<h1 id="elm">{{name}}</h1>
	<button id="btn">Click to initialize rendering</button>
</body>
Copy the code
   // vue 3.x converts data into a response
    const elm = document.querySelector('#elm')
    // 1. Reactive initialization
    const data = {
      name: 'reborn'.age: 18.hobbies: {
        a: 'play game'.b: 'play ball'}}// Assume that the VM is a vue instance
    const vm = {}

    const proxyData = new Proxy(data, {
      get(target, key) {
        return target[key]
      },

      set(target, key, val) {
        if (val === target[key]) return
        Reflect.set(... arguments)// Update the view when the simulated value changes
        elm.textContent = val
        return true}})Reflect.ownKeys(proxyData).forEach(key= > {
      Object.defineProperty(vm, key, {
        enumerable: true.configurable: true.// Get the hijacking of the operation
        get() {
          return proxyData[key]
        },
        // Set the hijacking of the new value operation
        set(newVal) {
          proxyData[key] = newVal
        }
      })
    })

    // Page initialization regardless of interpolation parsing
    document.querySelector('#btn').addEventListener('click'.() = > {
      elm.textContent = vm.name
    })

Copy the code

If you are careful to test it, you will see that the demo above shows that changing the value of any property in the data will cause the text content of the ELM DOM element to change to the new value. Now that we’ve been able to hijack every property change, how can we change the values of all dom elements on the page that depend on that property when one property in the data changes? We’ll find out later, so keep reading

3. Difference between Object. DefineProperty and Proxy

defineProperty Proxy
Listening to the object Can only listen to objects {} It can be any type of object, including a native array, a function, or even another proxy
Listening to the individual A property of the listening object Listen to the whole object

4. Vue 3.x changes from object.defineProperty to Proxy

Dig a hole…

Second, responsive system design mode

Publish subscribe && Observer is a design pattern for software

1. Observer mode

Introduce the Wikipedia definition: a target object manages all dependent observer objects and proactively notificates itself when its state changes.

An example of the application of the observer pattern to a VUE responsive system:

<! -- Page layout -->
 <div id="app">
    <! - the head -- -- >
    <div class="header">{{subject}}</div>
    
    <! - main body - - >
    <div class="main">{{subject}}</div>

    <! -- -- -- > at the bottom of the
    <div class="bottom">{{subject}}</div>
  </div>
Copy the code
const vm = new Vue({
    el: '#app'.data: {
      subject: "someContent".other: "otherContent"}})Copy the code

Suppose you have an HTML page whose layout consists of a header, a body, and a bottom and depends on the value of the subject attribute in the data object. For those of you who know responsive, a responsive system is a data-driven view. When the value of the subject property in data changes, the header, body, and bottom contents of the page that depend on the value of that property also change. If you think about it, the target object, the observer object, the state change corresponding to the observer mode which subjects?

In this demo, the target object is the subject attribute of the data object, and all subjects in the page that depend on the subject attribute are the observer object. When the state changes, the corresponding data changes, such as: data.subject = “reborn”. Refer to the following figure:

Implement the observer pattern in code

During the National Day holiday, I made an appointment with my four friends to play basketball on the court, but I forgot to make an appointment in advance because of my carelessness. When you arrive at the stadium, there are no matches, and the owner of the stadium tells you, “Sorry, handsome, the courses are fully booked during the holidays, and I will inform you if there is any cancellation.”

// Observer mode

// The target object
class Subject {
  constructor() {
    this.observes = []
  }

  // Add an observer
  addObserve(observe) {
    if(! observe.update)return
    this.observes.push(observe)
  }

  The event triggers the notification function
  notify(val) {
    if (!this.observes.length) return
    this.observes.forEach(observes= >{ observes.update? .(val) }) }// Remove a single observer
  removeObserve(observe) {
    const targetIdx = this.observes.findIndex(obsv= > obsv === observe)
    this.observes.splice(targetIdx, 1)}// Remove all observers
  removeAllObserves() {
    this.observes = []
  }
}


// The observer object
class Observe {
  constructor(cb) {
    this.cb = cb
  }

  update(val) {
    this.cb(val)
  }
}

/ / analysis:
// Observer => Handsome
// Target object => court
// Status change (event) => The court is empty

// Create the target object
const ballPark = new Subject()

// Create an observer
const handsomeMan = new Observe((date) = > {
  console.log('XDM has a field, '+ date + "Come with me.")})// Subscribe to the course
ballPark.addObserve(handsomeMan)

// The boss notifies you of a cancellation
ballPark.notify('This Wednesday')
Copy the code

By now you should be able to write an observer pattern by hand. Next we will implement the VUE responsive system in conjunction with the responsive first experience demo (vue2.x).

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>

<body>
  <h1 id="elm">{{name}}</h1>
  <! -- 1. Introduce the observer mode code you just wrote -->
  <script src="./observe.js"></script>
  <script>
    // 2. Data hijacking operations
    const data = {
      name: 'reborn'.age: 18.hobbies: {
        a: 'play game'.b: 'play ball'}}// Assume a vue instance
    const vm = {}

    Reflect.ownKeys(data).forEach(key= > {
      Object.defineProperty(vm, key, {
        enumerable: true.configurable: true.get() {
          return data[key]
        },
        set(newVal) {
          data[key] = newVal
        }
      })
    })

    Reflect.ownKeys(data).forEach(key= > {
      let value = data[key]
      // Create the target object
      const dep = new Subject()
      Object.defineProperty(data, key, {
        enumerable: true.configurable: true.get() {
          // Add an observer when the data is accessed
          Subject.isAddObsv && dep.addObserve(Subject.isAddObsv)
          return value
        },

        set(newVal) {
          if (newVal === value) return
          value = newVal
          // Notify all observers when the data changes
          dep.notify(value)
        }
      })
    })


    // 3. First rendering of the page
    function initRender() {
      const elm = document.querySelector("#elm")
      // Create the observer object
      Subject.isAddObsv = new Observe((val) = > {
        // Update the data in the callback when the hijack data has changed.
        elm.textContent = val
      })
      elm.textContent = vm.name
      Subject.isAddObsv = null
    } 

    // Trigger the first screen rendering
    initRender()

  </script>

</body>

</html>
Copy the code

2. Publish and subscribe

The publish/subscribe model is similar to the observer model, but different, like tomatoes and cherry tomatoes.

Wikipedia defines publis-subscribe as a message paradigm in which the sender of a message (called a publisher) does not send the message directly to a specific receiver (called a subscriber). Instead, they categorize published messages into different categories without having to know which subscribers, if any, might exist. Similarly, subscribers can express interest in one or more categories and receive only the messages of interest without knowing which publishers (if any) exist.

Based on the above definition, we may wonder how the publisher and the publisher are related, since the publisher and the subscriber do not care about each other. There needs to be a third party message center as a middleman to coordinate the subscribers and publishers. His responsibility is to maintain the published and subscribed messages, filter and match the published and subscribed messages, so as to indirectly realize the corresponding relationship between publishers and subscribers. (Personal understanding)

From the definition we can get the difference between the publish/subscribe pattern and the observer:

  • The publis-subscribe pattern is loosely coupled in that the publis-subscribers do not need to know about each other and only care about the message itself.
  • The observer mode is strongly coupled, and the observer needs to depend on the target object.

The code implements a publish-subscribe pattern

// Publish/subscribe

// Event scheduling center (message center)
class EventEmitter {
  constructor() {
    this.subs = {}
  }

  // Subscribe to the message
  $on(type, handler) {
    this.subs[type] = this.subs[type] || []
    this.subs[type].push(handler)
  }


  // Publish the message$emit(type, ... args) {if (!this.subs[type]) return
    this.subs[type].forEach(handler= >{ handler(... args) }) }// Remove a single message
  removeMsg(type, cbFn) {
    if(! type || ! cbFn)return 

    const fnInSubsIdx = this.subs[type].findIndex(cb= > cbFn === cb)

    if (fnInSubsIdx === -1) return

    this.subs[type].splice(fnInSubsIdx, 1)}// Remove all messages
  removeAllMsg() {
    this.subs = {}
  }
}

const e = new EventEmitter()

e.$on('playBall'.(val) = > {
  // finished homework
  console.log('come on , go to play ball! ', {val})
})


e.$on('playBall'.(val) = > {
  // no finished homework
  console.log('nonono ', {val})
})


e.$emit('playBall' , 'hhhh')
Copy the code

Currently, vUE internal custom events use the publish-subscribe pattern, as does Tapable, the core library that WebPack relies on. It is important to understand this design pattern

Three, to achieve a simplified version of Vue

So far, we’ve seen how to hijack data in data to listen for changes, and we’ve seen the design patterns for responsive systems. Now based on the demo that we’ve implemented now let’s think about what we need to do to implement a VUE responsive system.

Implementation approach

  • Hijack the data and listen for changes in the data.
  • Responsive system design pattern – Observer pattern, subscribing to data changes, updating views.
  • Page initialization rendering, parsing interpolation, instructions, creating observer subscription target data changes.

Let’s turn this idea into code

Code implementationLook at the directory structure

// index.html<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta  name="viewport" content="width=device-width, Word-wrap: break-word! Important; word-wrap: break-word! Important; word-wrap: break-word! Important; word-wrap: break-word! Important; "> <title>Document</title> </head> <body> <div id="app"> <h1> age {{age}}</h1> <h1 v-text="hobby">v-text</h1> <div v-html="htmlContent">v-html</div> <input type="text" v-model="tall" Placeholder =" placeholder "> <button v-on:click.native="handleClick" src="./js/Watcher.js"></script> <script src="./js/Compiler.js"></script> <script src="./js/Dep.js"></script> <script src="./js/Observe.js"></script> <script src="./js/Vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { name: 'reborn', age: 18, tall: '', obj: { a : 1, b: 2 }, hobby: 'game', htmlContent: '<strong>hello Reborn~~</strong>' }, methods: {{handleClick (e) the console. The log (' event triggered '{e})}}}) < / script > < / body > < / HTML >Copy the code
// vue.js
/ / duties
// create vue
class Vue {
  constructor(options) {
    // Parse the parameters
    this.$options = options
    this.$data = options.data
    this.$el =
      typeof options.el === 'string' ? document.querySelector('#app') : options.el

    // Add data to this instance
    this._proxyData(this.$data)

    // Convert all data in data into responses
    new Observe(this.$data)

    // Parse vue syntax rules
    new Compiler(this.$el, this)}_proxyData(data) {
    Object.keys(data).forEach((key) = > {
      Object.defineProperty(this, key, {
        enumerable: true.configurable: true.get() {
          return data[key]
        },

        set(newVal) {
          if (newVal === data[key]) return
          data[key] = newVal
        }
      })
    })
  }
}
Copy the code
// Observe.js
/ / responsibilities:
// Convert attributes in data to reactive data
// Add watcher when getters are hijacked
// The setter hijack notifies all observers to update the view
class Observe {
  constructor(data) {
    this.walk(data)
  }
  walk (data) {

    Object.keys(data).forEach(key= > {
      this.defineReactive(data, key, data[key])
    })
  }

  // Add data hijacking to the property
  defineReactive(data, key, value) {
    const dep = new Dep()
    let that = this
    // Recursively convert all the data in the tree into responses
    if (typeof value === 'object') this.walk(value)
    Object.defineProperty(data, key, {
      enumerable: true.configurable: true.get() {
        Dep.haveWatcher && dep.addSubs(Dep.haveWatcher)
        return value
      },
      set(newVal) {
        if (value === newVal) return
        value = newVal
        // If the data is {}, the internal attributes are also converted to the response
        if (typeof value === 'object') that.walk(value)
        dep.notify(newVal)
      }
    })
  }
  
}
Copy the code
// Dep.js 
/ / responsibilities:
// create Subject
class Dep {
  constructor() {
    this.subs = []
  }

  addSubs(watcher) {
    if(! watcher.update)return
    this.subs.push(watcher)
  }

  notify(newVal) {
    this.subs.forEach(item= > {
      item.update(newVal)
    })
  }
}
Copy the code
// wather.js
/ / responsibilities:
// Create the observer
class Watcher {
  constructor(vm, prop, cb) {
    this.cb = cb
    Dep.haveWatcher = this
    this.oldValue = vm[prop]
    Dep.haveWatcher = null
  }

  update(newVal) {
    if (newVal === this.oldValue) return
    this.cb && this.cb(newVal)
  }
}
Copy the code
// compile.js
// Compiler:
// 1. Parse vue syntax
// 2. Create watcher when first rendering
class Compiler {
  constructor(el, vm) {
    this.vm = vm
    this.compile(el)
  }

  / / edit
  compile(el) {
    const childrenNode = el.childNodes || []
    childrenNode.forEach((item) = > {
      this.handleNode(item)
      if (childrenNode.length) this.compile(item)
    })
  }

  handleNode(node) {
    if (this.isTextNode(node) && this.isHaveHuaKuohao(node)) {
      this.handleTextNode(node)
    } else if (this.isElmNode(node)) {
      this.handleElmNode(node)
    }
  }

  // Whether text node
  isTextNode(node) {
    return node.nodeType === 3
  }

  // Whether the element node
  isElmNode(node) {
    return node.nodeType === 1
  }

  // Process the file node and parse the interpolation (regardless of the expression in the interpolation)
  handleTextNode(node) { 
    let {textContent} = node
    const getVariableRegexp = / \ {{2} (. +) \} {2} /
    constvariable = textContent.match(getVariableRegexp)? .1]? .trim()const watcher = new Watcher(this.vm, variable, (newValue) = > {
      node.textContent = newValue
    })
    const value = watcher.oldValue
    node.textContent = value
    
    // Dependencies are collected where the page is used
    
    
  }
  // Parses are required for curly braces
  isHaveHuaKuohao(node) {
    const isHaveHuaKuohaoRegexp = / \ {{2} (. +) \} {2} /
    return isHaveHuaKuohaoRegexp.test(node.textContent)
  }

  // Process element nodes and parse instructions
  handleElmNode(node) {
    const attrubuts = Array.from(node.attributes) || []
    attrubuts.forEach((attr) = > {
      if (this.isDirectives(attr)) {
        // The current element node has instructions
        const name = attr.name
        const value = attr.value
        // Different instructions are given to different functions
        const drtFn = this.createFnName(name)
        this[drtFn]? .(node, value, name) } }) }// Check whether there are instructions on the element node
  isDirectives(attr) {
    constattrText = attr? .nodeNamereturn attrText.startsWith('v-') || attrText.startsWith(The '@')}// This section handles different types of instructions
  createFnName(name) {
    // We need to consider registering the event @click v-on:click
    if (name.includes(The '@') || name.includes(':')) {
      name = name.slice(2.4)}else {
      name = name.slice(2)}return name + 'DrtFn'
  }

  // v-text
  textDrtFn(node, prop) {
    prop = prop.trim()
    const watcher = new Watcher(this.vm, prop, (newVal) = > {
      node.textContent = newVal
    })
    node.textContent = watcher.oldValue
    // Add the dependent data on the page to the dependency
  }

  // v-model
  modelDrtFn(node, prop) {
    prop = prop.trim()
    const watcher = new Watcher(this.vm, prop, (newVal) = > {
      node.value = newVal
    })

    node.value = watcher.oldValue

    // V-model implements bidirectional data binding and listens for input time

    node.addEventListener('input'.(e) = > {
      this.vm[prop] = e.target.value
    })
  }

  // v-html
  htmlDrtFn(node, prop) {
    prop = prop.trim()
    const watcher = new Watcher(this.vm, prop, (newVal) = > {
      node.innerHTML = newVal
    })
    node.innerHTML = watcher.oldValue
  }

  // v-on
  onDrtFn(node, prop, attrName) {
    let eventFn, eventName, options

    const { eName, sign } = this.handleDirectName(attrName)
    eventFn = this.handleEventFn(prop.trim())
    // Normal event binding
    // Inline handler methods that can pass values to events
    // Handle event modifiers
    // Key modifiers (too complex, need to change the code again, will not be handled)
    options = {}

    node.addEventListener(eName, eventFn, options)
  }

  handleDirectName(name) {
    let eName, sign
    const startIdx = name.indexOf(':') > 0 ? name.indexOf(':') : name.indexOf(The '@')
    const eventNameEndIdx = name.indexOf(".") > 0 ? name.indexOf(".") : name.length
    eName = name.slice(startIdx + 1, eventNameEndIdx)
    const signStartIdx = eventNameEndIdx > 0 && eventNameEndIdx < name.length ? eventNameEndIdx : null

    if (signStartIdx) sign = name.slice(signStartIdx + 1, name.length)
    // Handle the case with modifiers

    return { eName, sign }
  }

  // Handle event modifiers
  // handleEvent

  // Inline handler methods that can pass values to events
  handleEventFn(fnName) {
    let fn
    const { methods } = this.vm.$options
    const callFnRegxp = /(\w+)(\(.+\))/
    const isCallFn = callFnRegxp.test(fnName)
    if (isCallFn) {
      let paramsArr = []
      // Function call case
      const res = fnName.match(callFnRegxp)
      consteventName = res? .1]
      constparams = res? .2]

      if(! eventName)return new Function(a)// Remove the parentheses
      const str = params.replace(/ \ [? \]? /g.' ').trim()


      // Process parameters, converting variables into real values
      str.split(', ')? .forEach(item= > {
        const isVariable = this.isVariable(item)
        if (isVariable) {
          // $event needs to pass in an object
          if(item ! = ='$event') {
            item = this.vm[item]
          }
          paramsArr.push(item)
        } else {
          paramsArr.push(item)
        }
      })

      // Generate function
      fn = (e) = > {
        // Replace '$event' with the bit event object e
        const arr = paramsArr.map( item= >  {
          if (item === '$event') {
            return e
          }

          return item
        })
        returnmethods[eventName](... arr) } }else {
      // No function call,
      fn = methods[fnName]
    }
    return fn

  }

  // Check whether it is all true
  isVariable(str) {
    // Not 23423, no ' 'string
    str = str.trim()
    const isNumRegexp = /^\d+$/
    const isStrRegexp = / ^ '{1}.? '{1} $/
    const isStrOrNum = isNumRegexp.test(str) || isStrRegexp.test(str)

    return! isStrOrNum } }Copy the code

Talk about your understanding of VUE responsive system

How much have you learned from reading this article? What if the interviewer asks you: What do you think of vue responsive? What would you say?

To answer this question from the two points of design idea and concrete implementation:

First of all, the core of the responsive system is data-driven view. When the data changes, the view will also change, so there needs to be an API that can listen to the changes of data, Object. DefinedProperty Or Proxy to realize the monitoring of data. Finally, the observer pattern is used in an “elegant” design to update the content of all views on the page that depend on the data after the data changes.

Specific implementation:

  1. Vue will data hijack all the attributes in the data via object.defineProperty and create a target Object (publisher) for each attribute.
  2. Its main responsibilities in the getter are two things, collecting dependencies (adding observers) and returning the value of the target it accesses.
  3. The main responsibilities in the setter are two things, changing the target value, and notifying all observers to update the view.
  4. At the first rendering of the page, create an observer for the hijacked data the view depends on and add the observer to the publisher, create a callback function, modify the DOM in the callback function, and wait for the data to update before the publisher calls all the watcher callbacks to update the view.