Data driven
In the development process, we only need to focus on the data, not how to render the relational data to the view
- Data responsive: when data is modified, the view will be updated to avoid DOM operation
- Two way binding: data change and view change; View change, data change
Publish subscribe mode and observer mode
It defines a one to many dependency between objects. When the state of the target object changes, all objects that depend on it will be notified.
Observer mode
- Publisher: it has a notify method, which calls the update method of the observer object when changes occur
- Observer: with update method
class Subscriber { constructor () { this.subs = [] } add(sub) { this.subs.push(sub) } notify() { this.subs.forEach(handler => { handler.update() }) } } class Observer { constructor (name) { this.name = name; } update() { console.log('Receive notice', this.name); } } const subscriber = new Subscriber(); const jack = new Observer('jack'); const tom = new Observer('tom'); subscriber.add(jack) subscriber.add(tom) subscriber.notify()
Publish subscribe mode
Different from the observer mode, the publish subscribe mode introduces the event center between the publisher and the observer. So that the target object does not directly notify the observer, but sends the notification through the event center.
class EventController { constructor() { this.subs = {} } subscribe(key, fn) { this.subs[key] = this.subs[key] || [] this.subs[key].push(fn) } publish(key, ...args) { if (this.subs[key]) { this.subs[key].forEach(handler => { handler(...args) }); } } } const event = new EventController() event.subscribe('onWork', time => { console.log('go to work', time) }); event.subscribe('offWork', time => { console.log('Are you off duty?', time); }); event.subscribe('onLaunch', time => { console.log('ate', time); }); event.publish('offWork', '18:00:00'); event.publish('onLaunch', '20:00:00');
summary
- The observer mode is scheduled by specific targets. When an event is triggered, the publisher will actively call the observer's methods, so there is a dependency between the publisher and the observer
- The publish subscribe mode is uniformly scheduled by the event center because there is no strong dependency between the publisher and the subscriber
Data responsive core principle
Vue2
When you pass an ordinary JavaScript object into the Vue instance as the data option, Vue will traverse all the properties of the object and use Object.defineProperty to convert all these properties into getters / setters. Object.defineProperty is a feature in ES5 that cannot shim, which is why Vue does not support IE8 and earlier browsers.
function proxyData(data) { // Traverse all properties of the data object Object.keys(data).forEach(key => { // Convert the attribute in data into vm setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // Data changes, updating DOM values document.querySelector('#app').textContent = data[key] } }) }) }
Due to JavaScript limitations, Vue cannot detect changes in the following arrays:
- When an array item is set directly by index, for example: vm.items[indexOfItem] = newValue
- When modifying the length of an array, for example: vm.items.length = newLength
The performance of Object.defineProperty in the array is consistent with that in the object. The index of the array can be regarded as the key in the object.
- getter and setter methods can be triggered when accessing or setting the value of the corresponding element through the index
- The index will be added by push ing or unshift ing. The newly added attributes need to be manually initialized before they can be observe d.
- Deleting an element through pop or shift will delete and update the index, and trigger setter and getter methods.
These two points are briefly summarized in the official documents as "unable to be implemented due to JavaScript limitations". In fact, the reason is not because of the vulnerability of Object.defineProperty, but because of performance problems.
Vue3
- Proxy is a new syntax in ES 6, which is not supported by IE, and the performance is optimized by the browser.
- Proxy listens directly to an object, not a property. defineProperty listens to a property in the object.
let vm = new Proxy(data, { // Functions that perform proxy behavior // When a member accessing the vm executes get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // When the vm member is set, the set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } })
Implementation process
Vue
- Receive the initialized parameters and inject them into the Vue real column $options attribute
- Inject the attribute in data into the $data attribute of Vue real column and generate getter/setter
- Call observer to listen for property changes in data
- Call compiler to parse instruction / interpolation expression
class Vue { constructor (options) { // 1. Save the data of options through attributes this.$options = options || {}; this.$data = options.data || {}; this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el; // 2. Convert the members in data into getter s and setter s and inject them into vue instances this._proxyData(this.$data); // 3. Call the observer object to listen for data changes new Observer(this.$data); // 4. Call the compiler object to parse the instruction and difference expression new Compiler(this); } _proxyData(data) { Object.keys(data).forEach(key => { // Inject the data attribute into the vue instance Object.defineProperty(this, key, { configurable: true, enumerable: true, get () { return data[key]; }, set (newValue) { if (newValue === data[key]) { return; } data[key] = newValue; } }) }) } }
Observer
- Convert attributes in data into responsive data
- Send notification of data change
class Observer { constructor(data) { this.walk(data); } walk(data) { // 1. Judge whether data is an object if (!data || typeof data !== 'object') { return } // 2. Traverse all the attributes of the data object Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(data, key, val) { // If val is an object, convert the attributes inside val into responsive data this.walk(val) const that = this; // Collect dependencies and send notifications const dep = new Dep(); Object.defineProperty(data, key, { configurable: true, enumerable: true, get() { // Collection dependency Dep.target && dep.addSubs(Dep.target); return val; }, set(newVal) { if (newVal === val) { return; } val = newVal; // If newValue is an object, convert the properties inside newValue into responsive data that.walk(newVal); // Send notification dep.notify(); } }) } }
Compiler
- Compile template, parse instruction
- Page rendering, re render the view when the data changes
class Compiler { constructor(vm) { this.el = vm.$el; this.vm = vm; this.compile(this.el); } // Compile templates to handle text nodes and primitive ancestor nodes compile (el) { Array.from(el.childNodes).forEach(node => { // Processing text nodes if (this.isTextNode(node)) { this.compileText(node) } else if (this.isElementNode(node)) { // Processing element nodes this.compileElement(node) } // Judge whether the node has child nodes. If there are child nodes, call compile recursively if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // Compile the text node and process the difference expression compileText (node) { const reg = /\{\{(.+?)\}\}/ if (reg.test(node.textContent)) { const key = RegExp.$1.trim(); node.textContent = node.textContent.replace(reg, this.vm[key]); // Create a watcher object to update the view when the data changes new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }); } } // Compile element nodes, process instructions compileElement (node) { Array.from(node.attributes).forEach(attr => { if (this.isDirective(attr.name)) {} const attrName = attr.name.substr(2); const key = attr.value; const updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) }) } // v-text textUpdater (node, value, key) { node.textContent = value; // Create a watcher object to update the view when the data changes new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }); } // v-model modelUpdater (node, value, key) { node.value = value; // Create a watcher object to update the view when the data changes new Watcher(this.vm, key, (newValue) => { node.value = newValue }); // Bidirectional binding node.addEventListener('input', () => { this.vm[key] = node.value }); } // Determine whether the element attribute is an instruction isDirective (attrName) { return attrName.startsWith('v-'); } // Judge whether the node is a text node isTextNode (node) { return node.nodeType === 3; } // Determine whether the node is an element node isElementNode (node) { return node.nodeType === 1; } }
Dep
- Publisher
- Add observers in getter s and send notifications in setter s
class Dep { constructor() { // Store all observers this.subs = []; } // Add observer addSubs(sub) { if (sub && sub.update) { this.subs.push(sub); } } // Send notification notify() { this.subs.forEach(sub => { sub.update() }); } }
Watcher
- Add to dep when instantiating
- When the data changes, dep notifies all watcher s to update the view
class Watcher { constructor(vm, key, cb) { // vue real column this.vm = vm; // Attribute name in data this.key = key; // The callback function updates the view this.cb = cb; // Record the watcher object to the static attribute target of the Dep class Dep.target = this; // Trigger the get method, and addSub will be called in the get method this.oldValue = vm[key]; // Leave blank to prevent other properties from being affected Dep.target = null; } // Update the view when the data changes update() { const newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue); } }
Bidirectional binding
- Listen to the textbox input event and update the node value
// Bidirectional binding node.addEventListener('input', () => { this.vm[key] = node.value });