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:
- Install the Parcel packing tool
- Import Snabbdom
Official document: github.com/snabbdom/sn…
- Install Snabbdom
- NPM install [email protected]
- 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
- 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
- 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
- attributes
- 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…
- The source address
- 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