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.
- Data hijacking can only be done on attributes, so you need to deeply traverse the entire object
- 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
- New-node
tagName
orkey
Unlike 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 - New-node
tagName
和key
Same as the old one, start traversing the subtree - 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
- Walk through the old property list to see if each property still exists in the new property list
- Iterate through the new property list to see if the value of the property that exists in both lists has changed
- 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
- Iterate through the old node list to see if each node still exists in the new node list
- Iterate over the new node list to see if there are any new nodes
- 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
- Determine the difference between the two lists
- 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
- Walk through the tree in depth, pulling out changes that need to be made
- 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
- Create DOM objects through JS simulation
- Determine the difference between two objects
- 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.