MVVM

The MVVM consists of the following three components

  • Interface of the View:
  • Model: Data Model
  • ViewModel: Act as a bridge between View and Model

In the era of JQuery, if you need to refresh the UI, you need to fetch the corresponding DOM and then update the UI, so that the data and business logic is strongly coupled with the page.

In MVVM, THE UI is driven by data. Once the data changes, the CORRESPONDING UI will be refreshed accordingly. If the UI changes, the corresponding data will also be changed. This way you can focus on the flow of data in business processes without having to deal directly with the page. The ViewModel only cares about data and business processing, not how the View handles the data. In this case, the View and Model can be separate, and either side doesn’t have to change the other side, and some reusable logic can be put into a ViewModel. Let multiple views reuse the ViewModel.

In MVVM, the core is two-way data binding, such as dirty data detection in Angluar and data hijacking in Vue.

Dirty data detection

When the specified event is triggered, dirty data detection is performed, and the $digest loop is called to traverse all data observers to determine whether the current value is different from the previous value. If a change is detected, the $watch function is called, and then the $digest loop is called again until no change is found. The loop is at least two times and at most ten times.

Dirty data detection is inefficient, but it can be done regardless of how the data is changed, but this is problematic with two-way binding in Vue. In addition, dirty data detection can realize batch detection of updated values, and then uniformly update UI, which greatly reduces the number of DOM operations. So inefficiency is relative, which is a matter of opinion.

The data was hijacked

Vue uses object.defineProperty () internally for bidirectional binding, which listens for set and GET events.

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // Determine the type
  if(! obj ||typeofobj ! = ='object') {
    return
  }
  Object.keys(obj).forEach(key= > {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // Recursive subattributes
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}
Copy the code

The above code simply implements how to listen for data set and GET events, but this is not enough. You also need to add publish subscribe to attributes when appropriate

<div>
    {{name}}
</div>
Copy the code

{{name}} ::: V-pre adds a publish subscription to the name attribute when parsing the template code above. : : :

// Decouple through Dep
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub is an instance of Watcher
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub= > {
      sub.update()
    })
  }
}
// Global property, with which to configure Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // Point dep. target to yourself
    // Then trigger the getter for the property to add a listener
    // Dep. Target is left blank
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // Get a new value
    this.value = this.obj[this.key]
    // Call the update method to update the Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// Simulate parsing to the action triggered by '{{name}}'
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 
Copy the code

Next, modify the defineReactive function

function defineReactive(obj, key, val) {
  // Recursive subattributes
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter() {
      console.log('get value')
      // Add Watcher to the subscription
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // Execute watcher's update method
      dp.notify()
    }
  })
}
Copy the code

The above implementation of a simple two-way binding, the core idea is to manually trigger the getter of the property once to implement the publication subscription.

Compare Proxy with Object.defineProperty

Object.defineproperty is already bidirectional binding, but it still has some drawbacks.

  1. Data hijacking can only be done on attributes, so you need to deeply traverse the entire object
  2. You can’t listen for data changes with arrays

While it is true that Vue can detect array data changes, it is a hack, and it is flawed.

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack the following functions
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
methodsToPatch.forEach(function (method) {
  // Get the native function
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (. args) {
    // Call the native function
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // Trigger the update
    ob.dep.notify()
    return result
  })
})
Copy the code

Proxy, on the other hand, doesn’t have this problem. It natively listens for array changes and intercepts entire objects directly, so Vue will also replace Object.defineProperty with Proxy in the next big release

let onWatch = (obj, setBind, getLogger) = > {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value); }};return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) = > {
  value = v
}, (target, property) = > {
  console.log(`Get '${property}'=${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
Copy the code

Routing principle

Front-end routing implementation is actually very simple, the essence is to monitor URL changes, and then match routing rules, display the corresponding page, and do not need to refresh. Currently there are only two ways to implement routing for a single page

  • Hash pattern
  • The history mode

www.test.com/#/ is a Hash URL. When the Hash value after # changes, no data is requested from the server. The hashchange event is used to listen for changes in the URL and jump to the page.

History mode is a new feature of HTML5 that is much nicer than Hash urls

Virtual Dom

The code address

Why the Virtual Dom

As we all know, manipulating the DOM is a performance-intensive affair, so we can consider using JS objects to simulate DOM objects, since manipulating JS objects takes much less time than manipulating the DOM.

For example

// Suppose we simulate a ul that contains 5 Li's
[1.2.3.4.5]
// Replace the above li here
[1.2.5.4]
Copy the code

From the above example, we can see at a glance that the third Li in the previous UL has been removed, and four and five have been replaced.

If the above corresponds to the DOM, this is the code below

// Delete the third li
ul.childNodes[2].remove()
// Switch the fourth li and the fifth li
let fromNode = ul.childNodes[4]
let toNode = node.childNodes[3]
let cloneFromNode = fromNode.cloneNode(true)
let cloenToNode = toNode.cloneNode(true)
ul.replaceChild(cloneFromNode, toNode)
ul.replaceChild(cloenToNode, fromNode)
Copy the code

Of course, in actual operation, we also need to give each node a label, as the basis to judge that it is the same node. This is why nodes in Vue and React’s official recommended lists use unique keys to ensure performance.

So since DOM objects can be simulated by JS objects, the corresponding DOM can also be rendered by JS objects

Here is a simple implementation of a JS object that emulates a DOM object

export default class Element {
  / * * *@param {String} tag 'div'
   * @param {Object} props { class: 'item' }
   * @param {Array} children [ Element1, 'text']
   * @param {String} key option
   */
  constructor(tag, props, children, key) {
    this.tag = tag
    this.props = props
    if (Array.isArray(children)) {
      this.children = children
    } else if (isString(children)) {
      this.key = children
      this.children = null
    }
    if (key) this.key = key
  }
  / / rendering
  render() {
    let root = this._createElement(
      this.tag,
      this.props,
      this.children,
      this.key
    )
    document.body.appendChild(root)
    return root
  }
  create() {
    return this._createElement(this.tag, this.props, this.children, this.key)
  }
  // Create a node
  _createElement(tag, props, child, key) {
    // Create a node with a tag
    let el = document.createElement(tag)
    // Set the node properties
    for (const key in props) {
      if (props.hasOwnProperty(key)) {
        const value = props[key]
        el.setAttribute(key, value)
      }
    }
    if (key) {
      el.setAttribute('key', key)
    }
    // Add child nodes recursively
    if (child) {
      child.forEach(element= > {
        let child
        if (element instanceof Element) {
          child = this._createElement(
            element.tag,
            element.props,
            element.children,
            element.key
          )
        } else {
          child = document.createTextNode(element)
        }
        el.appendChild(child)
      })
    }
    return el
  }
}
Copy the code

Description of Virtual Dom algorithm

Now that we have implemented the DOM through JS emulation, the next challenge is to determine the difference between the old object and the new object.

DOM is a multi-fork tree structure. If you need to completely compare the differences between two trees, the time complexity will be O(n ^ 3), which is definitely unacceptable. So the React team optimized the algorithm to achieve O(n) complexity to compare differences.

The key to achieving O(n) complexity is to compare nodes only at the same level, not across layers, given that few DOM elements are moved across layers in real business.

So there’s a two-step algorithm for determining differences

  • The object is first traversed from top to bottom and left to right, the depth of the tree, adding indexes to each node to facilitate the final rendering of the differences
  • Once the node has child elements, determine if the child elements are different

Virtual Dom algorithm implementation

Tree recursion

First of all, we will implement the recursive algorithm of the tree. Before implementing the algorithm, we will consider several situations when the two nodes are compared

  1. New-nodetagNameorkeyUnlike the old one, this means that the old node needs to be replaced and there is no longer a need to traverse the child elements of the old node because the entire old node has been deleted
  2. New-nodetagNamekeySame as the old one, start traversing the subtree
  3. No new nodes, so nothing to do
import { StateEnums, isString, move } from './util'
import Element from './element'

export default function diff(oldDomTree, newDomTree) {
  // To record the difference
  let pathchs = {}
  // The initial index is 0
  dfs(oldDomTree, newDomTree, 0, pathchs)
  return pathchs
}

function dfs(oldNode, newNode, index, patches) {
  // Used to save changes to the subtree
  let curPatches = []
  // There are three cases to judge
  // 1. There is no new node, so there is nothing to do
  // 2. The tagName and 'key' of the new node are different from those of the old node
  // 3. The new node has the same tagName and key as the old node, and starts traversing the subtree
  if(! newNode) { }else if (newNode.tag === oldNode.tag && newNode.key === oldNode.key) {
    // Determine whether the attribute has changed
    let props = diffProps(oldNode.props, newNode.props)
    if (props.length) curPatches.push({ type: StateEnums.ChangeProps, props })
    // Walk through the subtree
    diffChildren(oldNode.children, newNode.children, index, patches)
  } else {
    // The node is different and needs to be replaced
    curPatches.push({ type: StateEnums.Replace, node: newNode })
  }

  if (curPatches.length) {
    if (patches[index]) {
      patches[index] = patches[index].concat(curPatches)
    } else {
      patches[index] = curPatches
    }
  }
}
Copy the code

Determine changes to properties

Determining property changes is also a three-step process

  1. Walk through the old property list to see if each property still exists in the new property list
  2. Iterate through the new property list to see if the value of the property that exists in both lists has changed
  3. In the second step, check to see if there are any properties that do not exist in the old property column list
function diffProps(oldProps, newProps) {
  // Determine Props in three steps
  // Iterate over the oldProps to see if any properties are deleted
  // Then traverse newProps to see if any property values have been modified
  // Finally check to see if any attributes are added
  let change = []
  for (const key in oldProps) {
    if(oldProps.hasOwnProperty(key) && ! newProps[key]) { change.push({prop: key
      })
    }
  }
  for (const key in newProps) {
    if (newProps.hasOwnProperty(key)) {
      const prop = newProps[key]
      if(oldProps[key] && oldProps[key] ! == newProps[key]) { change.push({prop: key,
          value: newProps[key]
        })
      } else if(! oldProps[key]) { change.push({prop: key,
          value: newProps[key]
        })
      }
    }
  }
  return change
}

Copy the code

Judge list difference algorithm implementation

This algorithm is the most core algorithm in the entire Virtual Dom, and let me tell you one by one. The main steps here are actually similar to determining attribute differences, which are also divided into three steps

  1. Iterate through the old node list to see if each node still exists in the new node list
  2. Iterate over the new node list to see if there are any new nodes
  3. In the second step, determine whether the node has moved

PS: This algorithm only deals with nodes with keys

function listDiff(oldList, newList, index, patches) {
  // Retrieve all keys from both lists for easy traversal
  let oldKeys = getKeys(oldList)
  let newKeys = getKeys(newList)
  let changes = []

  // Used to save the changed node data
  // Saving with this array has the following advantages
  // 1. The index of the deleted node can be obtained correctly
  // 2. Switch node positions only need to operate DOM once
  // 3. Use the diffChildren function to judge, just need to traverse
  // Nodes exist in both trees, but are not necessary for new or removed nodes
  // Try again
  let list = []
  oldList &&
    oldList.forEach(item= > {
      let key = item.key
      if (isString(item)) {
        key = item
      }
      // Find if the new children contains the current node
      // If not, delete it
      let index = newKeys.indexOf(key)
      if (index === -1) {
        list.push(null)}else list.push(key)
    })
  // Iterate over the changed array
  let length = list.length
  // Because deleting an array element changes the index
  // Delete from back to front to keep index unchanged
  for (let i = length - 1; i >= 0; i--) {
    // Determine whether the current element is empty, which means that it needs to be deleted
    if(! list[i]) { list.splice(i,1)
      changes.push({
        type: StateEnums.Remove,
        index: i
      })
    }
  }
  // Iterate through the new list to see if any nodes are added or moved
  // Also add and move nodes to 'list'
  newList &&
    newList.forEach((item, i) = > {
      let key = item.key
      if (isString(item)) {
        key = item
      }
      // Find the current node in the old children
      let index = list.indexOf(key)
      // New node not found, need to be inserted
      if (index === -1 || key == null) {
        changes.push({
          type: StateEnums.Insert,
          node: item,
          index: i
        })
        list.splice(i, 0, key)
      } else {
        // If you need to move it, you need to move it
        if(index ! == i) { changes.push({type: StateEnums.Move,
            from: index,
            to: i
          })
          move(list, index, i)
        }
      }
    })
  return { changes, list }
}

function getKeys(list) {
  let keys = []
  let text
  list &&
    list.forEach(item= > {
      let key
      if (isString(item)) {
        key = [item]
      } else if (item instanceof Element) {
        key = item.key
      }
      keys.push(key)
    })
  return keys
}
Copy the code

Iterate over child elements for identification

For this function, there are only two main functions

  1. Determine the difference between the two lists
  2. Label the node

In general, what this function does is simple

function diffChildren(oldChild, newChild, index, patches) {
  let { changes, list } = listDiff(oldChild, newChild, index, patches)
  if (changes.length) {
    if (patches[index]) {
      patches[index] = patches[index].concat(changes)
    } else {
      patches[index] = changes
    }
  }
  // Record the last node traversed
  let last = null
  oldChild &&
    oldChild.forEach((item, i) = > {
      let child = item && item.children
      if (child) {
        index =
          last && last.children ? index + last.children.length + 1 : index + 1
        let keyIndex = list.indexOf(item.key)
        let node = newChild[keyIndex]
        // Only the existing nodes are traversed, other new or deleted nodes are not traversed
        if (node) {
          dfs(item, node, index, patches)
        }
      } else index += 1
      last = item
    })
}
Copy the code

Rendering differences

We can already figure out the difference between the two trees using our previous algorithm. Now that you know the difference, you need to update the DOM locally, so let’s take a look at the final step of the Virtual DOM algorithm

This function has two main functions

  1. Walk through the tree in depth, pulling out changes that need to be made
  2. Local DOM update

Overall this part of the code is pretty easy to understand

let index = 0
export default function patch(node, patchs) {
  let changes = patchs[index]
  let childNodes = node && node.childNodes
  // The depth traversal is the same as in diff
  if(! childNodes) index +=1
  if (changes && changes.length && patchs[index]) {
    changeDom(node, changes)
  }
  let last = null
  if (childNodes && childNodes.length) {
    childNodes.forEach((item, i) = > {
      index =
        last && last.children ? index + last.children.length + 1 : index + 1
      patch(item, patchs)
      last = item
    })
  }
}

function changeDom(node, changes, noChild) {
  changes &&
    changes.forEach(change= > {
      let { type } = change
      switch (type) {
        case StateEnums.ChangeProps:
          let { props } = change
          props.forEach(item= > {
            if (item.value) {
              node.setAttribute(item.prop, item.value)
            } else {
              node.removeAttribute(item.prop)
            }
          })
          break
        case StateEnums.Remove:
          node.childNodes[change.index].remove()
          break
        case StateEnums.Insert:
          let dom
          if (isString(change.node)) {
            dom = document.createTextNode(change.node)
          } else if (change.node instanceof Element) {
            dom = change.node.create()
          }
          node.insertBefore(dom, node.childNodes[change.index])
          break
        case StateEnums.Replace:
          node.parentNode.replaceChild(change.node.create(), node)
          break
        case StateEnums.Move:
          let fromNode = node.childNodes[change.from]
          let toNode = node.childNodes[change.to]
          let cloneFromNode = fromNode.cloneNode(true)
          let cloenToNode = toNode.cloneNode(true)
          node.replaceChild(cloneFromNode, toNode)
          node.replaceChild(cloenToNode, fromNode)
          break
        default:
          break}})}Copy the code

The last

The realization of Virtual Dom algorithm is the following three steps

  1. Create DOM objects through JS simulation
  2. Determine the difference between two objects
  3. Rendering differences
let test4 = new Element('div', { class: 'my-div'},'test4'])
let test5 = new Element('ul', { class: 'my-div'},'test5'])

let test1 = new Element('div', { class: 'my-div' }, [test4])

let test2 = new Element('div', { id: '11' }, [test5, test4])

let root = test1.render()

let pathchs = diff(test1, test2)
console.log(pathchs)

setTimeout(() = > {
  console.log('Start updating')
  patch(root, pathchs)
  console.log('End of update')},1000)
Copy the code

Of course, the current implementation is a little rough, but it’s good enough to understand the Virtual Dom algorithm.

Summary: What is the principle of two-way data binding for Vue?

Vue. js adopts data hijacking combined with publiser-subscriber mode. It hijacks the setter and getter of each attribute through Object.defineProperty() to publish messages to subscribers when data changes and trigger corresponding listening callback. It is mainly divided into the following steps:

1. Perform recursive traversal of the observe data object, including the property of the child property object, with setters and getters. If you assign a value to that object, the setter will be triggered and the data will be monitored

2. Compile parses the template instruction, replaces variables in the template with data, initializes render page view, binds the corresponding node of each instruction to update function, adds subscribers to listen to data, receives notification once the data changes, updates the view

3. Watcher subscribers serve as a bridge between The Observer and Compile. Update () = dep = deP = deP = deP = deP = deP = deP = deP = deP = deP = deP

4. MVVM, as the entry of data binding, integrates Observer, Compile and Watcher, uses Observer to monitor its model data changes, and uses Compile to parse and Compile template instructions. Finally, Watcher is used to build a communication bridge between Observer and Compile to achieve data change -> view update; View Interactive Changes (INPUT) -> Bidirectional binding effect of data model changes.