“Detailed explanation of Vue 2.X core source code, hand lu a simple version of the Vue framework (part 1)” recorded the implementation of Vue responsive system, virtual DOM generation, DOM directional update, etc.. This article summarizes the source code implementation process
Vue source code implementation process
The Vue instance is initialized
In this stage, data is mounted ($options, _data) and Vue options are initialized, such as data initialization, computed initialization, watch initialization, etc.
Data proxy
Iterate over all the data and proxy the data to the Vue instance via Object.defineProperty
Hijacking of data
Register a listener Observer for each data data object and declare an event center Dep instance for it, traversing the data and passing it into defineReactive(data[key]). Data hijacking is done in defineReactive, watcher dependencies are collected through getter, and updates through Dep through setter. Dep notifies all Watcher updates.
Rendering functions with the virtual DOM
Vue initializations call mount, declare render Watcher on mount, on mount, in mount and evaluate it. The render Watcher evaluation function will first call the render function to generate the virtual DOM, and pass the virtual DOM into the _update method to update the DOM node Diff contrast directionally. When Dep notifies Render Watcher of updates, it repeats this logic.
All code and annotation
Vue responsive system and VDOM part
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
this.initData()
// 8.4 Both the initialization of computed properties and the initialization of data must be placed before the initialization of Watch, because watch can detect them only after the initialization of computed properties and data is completed.
this.initComputed()
this.initWatch()
// 10.0 uses parsers and code generators to generate rendering functions
if (this.$options.el) {
// 10.1 Obtaining a Template String
let html = document.querySelector("div").outerHTML
// 10.2 Generate an abstract syntax tree
let ast = parser(html)
// 10.3 Generate the body of the render function
let funCode = codegen(ast).render
// 10.4 Generate the render function and mount it to the Vue instance
this.$options.render = new Function(funCode)
Call $mount to update the view
this.$mount(this.$options.el)
}
}
$mount(el) {
16.1 Mount the container root node to the Vue instance
this.$el = document.querySelector(el)
// create new render Watcher
this._watcher = new Watcher(this.() = > {
16.3 Generating the Virtual DOM
const vnode = this.$options.render.call(this)
16.4 Call _update to update the view
this._update(vnode)
}, () = >{})}_update(vnode) {
//17.0 has the last vnode
if (this._vnode) {
17.1 Call patch and pass in the last vnode and this vnode
patch(this._vnode, vnode)
} else {
// 17.2 Passing the real DOM node when mounting the Vue instance for the first time
patch(this.$el, vnode)
}
17.3 Save the vNode
this._vnode = vnode
}
// 11.0 Generate element node
_c(tag, attrs, children, text) {
return new VNode(tag, attrs, children, text)
}
12.0 Generate a plain text node
_v(text) {
return new VNode(null.null.null, text)
}
// 13.0 Get variable content
_s(val) {
// 13.1 Returns an empty string if the value is null
if (val === null || val === undefined) {
return ' '
// 13.2 If it is an object
} else if (typeof val === 'object') {
return JSON.stringify(val)
// 13.3 If the value is a number or a string
} else {
return val
}
}
initData() {
let data = this._data
let keys = Object.keys(data)
// Data broker
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(this, keys[i], {
enumerable: true.configurable: true.set: function proxySetter(newVal) {
data[keys[i]] = newVal
},
get: function proxyGetter() {
return data[keys[i]]
},
})
}
// Data hijacking
observe(data)
}
initWatch() {
const watches = this.$options.watch
// The watch option exists
if (watches) {
const keys = Object.keys(watches)
for (let index = 0; index < keys.length; index++) {
new Watcher(this, keys[index], watches[keys[index]])
}
}
}
// 8.3 Initializes the calculated properties separately
initComputed() {
const computeds = this.$options.computed
if (computeds) {
const keys = Object.keys(computeds)
for (let index = 0; index < keys.length; index++) {
// 8.5 The second argument is passed to the computed attribute function
// 8.15 The watcher that calculates attribute initialization needs to be marked lazy
const watcher = new Watcher(this, computeds[keys[index]], function () {}, {lazy: true })
// 8.6 mount the Watcher to the Vue instance
Object.defineProperty(this, keys[index], {
enumerable: true.configurable: true.// 8.7 Does not allow users to modify computing attributes
set: function computedSetter() {
console.warn("Please do not modify the calculated properties")},// 8.8 evaluates using watcher's get method and returns the result of the evaluation
get: function computedGetter() {
// 8.9 Re-evaluate if only watcher is dirty
if (watcher.dirty) {
watcher.get()
// 8.10 Update the dirty status
watcher.dirty = false
}
// 9.12 Determine in the getter of the calculated property whether there are more Watcher to collect
if (Dep.target) {
for (let i = 0; i < watcher.deps.length; i++) {
// 9.13 Remove the DEP from watcher and continue collecting the remaining watcher
watcher.deps[i].depend()
}
}
return watcher.value
}
})
}
}
}
$watch(key, cb) {
new Watcher(this, key, cb)
}
// 6.6 __ob__ mount, dependency collection is complete
$set(targt, key, value) {
constoldValue = { ... targt }// 6.7 Makes the new attribute passed in also responsive
defineReactive(targt, key, value)
// 6.8 Manually sending dependency updates
targt.__ob__.dep.notify(oldValue, targt)
}
}
// observe the data type and return an Observer instance
function observe(data) {
const type = Object.prototype.toString.call(data)
// 1.1 Returns if the observed data is of a basic type
if(type ! = ='[object Object]'&& (type ! = ='[object Array]')) return
// 1.2 Observation Data involves some complex logic to encapsulate this process as an Observer class
/ / 1.2 new Observer (data)
// 6.3 Return the Observer instance and receive it in defineReactive.
if (data.__ob__) return data.__ob__
return new Observer(data)
}
// 2, Observer class: Observer/listener, used to observe data, generate complex logic responsible for handling dependencies Dep instances, etc
class Observer {
constructor(data) {
// 6.1 Mount a Dep instance for an Observer instance (event center)
this.dep = new Dep()
// The 7.5 array cannot call walk because walk hijacks subscripts via defineProperty, resulting in dependency callback errors, etc
if (Array.isArray(data)) {
// 7.6 Overwrite the prototype object with our modified array prototype
data.__proto__ = ArrayMethods
// 7.7 Make all the children of the array responsive
this.observeArray(data)
} else {
// 2.1 Change all attributes of data to responsive
this.walk(data)
}
// 6.2 Attach an Observer instance to the non-enumerable attribute __ob__ for external $set use
Object.defineProperty(data, "__ob__", {
value: this.enumerable: false.configurable: true.writable: true})}walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
// 7.8 Makes all children of the passed array responsive
observeArray(arr) {
for (let i = 0; i < arr.length; i++) {
observe(arr[i])
}
}
}
// defineReactive utility function: used to recursively hijack data and turn it into responsive data
function defineReactive(obj, key, value) {
// 3.1 Recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
[key] / / 3.1 observe (obj)
// 6.4 Receive Observer instances to collect dependencies on Watcher for the attribute Dep
let childOb = observe(obj[key])
// create a new Dep instance for each data and maintain it through closures
let dep = new Dep()
// 3.2 Data hijacking the key of the current data object
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.set: function reactiveSetter(newVal) {
if (newVal === value) return
// 4.4 Dep assigns dependency updates
dep.notify(newVal, value)
value = newVal
},
get: function reactiveGetter() {
Closure Dep collection relies on Watcher
dep.depend()
The observe function does not return an Observer instance if the data is of a simple type. If yes, collect a dependency for the Dep of the Observer instance
if (childOb) childOb.dep.depend()
return value
}
})
}
9.1 Adding a stack to store depTarget
let targetStack = []
// Dep class: event center, responsible for collecting dependencies, notifying dependency updates, etc
class Dep {
constructor(option) {
// 4.1 subs is used to save all subscribers
this.subs = []
}
// 9.7 After collecting the DEP, call dep.addSub to collect the Watcher
addSub(watcher) {
this.subs.push(watcher)
}
The Depend method is used to collect subscriber dependencies
depend() {
// 5.5 if the Watcher instance is initialized
if (Dep.target) {
// 5.6 For each data Watcher instance, the dep. target is set first and the getter for data is triggered to complete the dependency collection
// this.subs.push(Dep.target)
// 9.6 Watcher collects DEP
Dep.target.addDep(this)}}// the notify method is used to send subscribers updates
notify(newVal, value) {
Execute Watcher's run method for each subscriber to complete the update
// 8.12 Depend on update Before sending an update, check whether the update is required
this.subs.forEach(watcher= > watcher.update(newVal, value))
}
}
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber, trigger dependency collection, processing callback
class Watcher {
constructor(vm, exp, cb, option = {}) {
// 8.13 watcher Added the new parameter option to set watcher by default
this.lazy = this.dirty = !! option.lazyMount Vue instance, data attribute name, and processing callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
// 9.8 watcher is used to save collected DEPs
this.deps = []
// 8.14 Lazy Watcher initialization does not require collecting dependencies
if(! option.lazy) {// 5.2, trigger the getter for data to complete the dependency collection
this.get()
}
}
addDep(dep) {
// 9.9 Since watcher may collect dePs multiple times per 9.0 evaluation, it terminates if it has already collected dePs
if (this.deps.indexOf(dep) ! = = -1) return
// 9.10 Collecting DEPs
this.deps.push(dep)
// 9.11 let DEP collect watcher
dep.addSub(this)}get() {
// 9.2 If deP collection depends on watcehr, add it to the stack first
targetStack.push(this)
// set the Watcher instance as the target object for Dep dependency collection
Dep.target = this
Check whether a function is passed when evaluating attributes before collecting dependencies
if (typeof this.exp === 'function') {
// 8.2 Execute the function and evaluate
this.value = this.exp.call(this.vm)
} else {
Trigger the data getter interceptor to evaluate it
this.value = this.vm[this.exp]
}
// 9.3 Let Watcher off the stack after evaluating and collecting dependencies
targetStack.pop()
// 9.4 Check whether there are uncollected watcher in the stack
if (targetStack.length) {
// 9.5 Get watcher at the top of the stack
Dep.target = targetStack[targetStack.length - 1]}else {
// Clear the dependent target object
Dep.target = null}}8.11 Call update before run to determine whether to run directly
update(newVal, value) {
Do not run when the current watcher is lazy. Instead, it marks Watcher as dirty data and waits for the user to fetch the results before running
if (this.lazy) {
this.dirty = true
} else {
this.run(newVal, value)
}
}
run(newVal, value) {
// 5.8 If the task already exists in the task queue, the task is terminated
if (watcherQueue.indexOf(this.id) ! = = -1) return
// 5.9 Add the current watcher to the queue
watcherQueue.push(this.id)
const index = watcherQueue.length - 1
Promise.resolve().then(() = > {
// 9.0 relies on updates to evaluate the watcher property to solve the problem of not triggering the calculation attribute watcher
this.get()
this.cb.call(this.vm, newVal, value)
// 5.10 The task is deleted from the task queue
watcherQueue.splice(index, 1)}}}// 7.0 gets the array prototype object
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// Declare an array method that needs to be modified
const methods = ['push'.'pop']
// 7.2 Modify the array method
methods.forEach(method= > {
ArrayMethods[method] = function (. args) {
const oldValue = [...this]
// 7.9 Change the newly inserted data to responsive
if (method === 'push') {
this.__ob__.observeArray(args)
}
// 7.3 Pass parameters to execute the original method
const result = Array.prototype[method].apply(this, args)
// 7.4 Sending dependency updates
this.__ob__.dep.notify(oldValue, this)
return result
}
})
The 14.0 VNode abstract class implements the virtual DOM node
class VNode {
constructor(tag, attrs, children, text) {
this.tag = tag
this.attrs = attrs
this.children = children
this.text = text
}
}
// 15.0 Generate the real DOM
function createEle(vnode) {
// 15.1 is a literal node
if(! vnode.tag) {const el = document.createTextNode(vnode.text)
// 15.2 Save the node
vnode.ele = el
return el
}
// 15.3 is an element node
const el = document.createElement(vnode.tag)
vnode.ele = el
// 15.4 Convert the child node into a real DOM and insert it into the parent node
vnode.children.map(createEle).forEach(e= > {
el.appendChild(e)
})
return el
}
// 18.6 Check whether the old and new nodes are changed
function changed(oldNode, newNode) {
returnoldNode.tag ! == newNode.tag || oldNode.text ! == newNode.text }function patch(oldNode, newNode) {
const isRealyElement = oldNode.nodeType
// 18.0 When oldNode=this.$el is first mounted for the element node page
if (isRealyElement) {
let parent = oldNode.parentNode
18.1 Replace the vUE container node with the new node generated by VDOM
parent.replaceChild(createEle(newNode), oldNode)
return
}
Last patch will mount ele on newNode
let el = oldNode.ele
// 18.3 New VDOM node exists Mount DOM to vdom.ele. Ele will be used in patch next time
if (newNode) {
newNode.ele = el
}
let parent = el.parentNode
18.4 If the new VDOM node does not exist, delete the corresponding node in the DOM
if(! newNode) { parent.removeChild(el)18.5 Label types or text of new and Old Nodes are Inconsistent
} else if (changed(oldNode, newNode)) {
18.7 Call createEle to generate a new DOM node to replace the old ONE
parent.replaceChild(createEle(newNode), el)
18.8 Comparing Child Nodes
} else if (newNode.children) {
let newLength = newNode.children.length
let oldLength = oldNode.children.length
18.9 Traverse all children of the old and new VDOM nodes
for (let index = 0; index < newLength || index < oldLength; index++) {
The old vDOM of the child node does not exist. Call createEle to generate a DOM and insert it into the parent node el
if (index > oldLength) {
el.appendChild(createEle(newNode.children[index]))
} else {
18.11 The comparison of other child nodes is achieved by calling patch
patch(oldNode.children[index], newNode.children[index])
}
}
}
}
Copy the code
Parser part
ElementASTs (ElementASTs)
{/ * * * * children: [{...}], * parent: {}, * tag: "div", * type: 1, //1- element node 2- text node with variable 3- plain text node, * expression:'_s(name)', //type if 2 returns _s(variable) * text:'{{name}}' // Text node before compilation string *} */
function parser(html) {
// Stack: records the level of the current element
let stack = []
// The root element node
let root = null
// The parent node of the current element
let currentParent = null
// 1.0 continuously parses the template string
while (html) {
let index = html.indexOf("<")
/ / 2.1 if the element has a text node example: before the HTML = "{{name}} < div > 1 < / div > < / root >"
if (index > 0) {
// 2.2 Intercept the text before the label
let text = html.slice(0, index)
5.4 Call the parseText utility function to parse the text
let element = parseText(text)
// 5.5 Add parent attributes to text nodes
element.parent = currentParent
2.3 Push the literal node into the children of the parent element
currentParent.children.push(element)
// 2.4 Truncate the part that has been processed
html = html.slice(index)
/ / 1.0 if to start tag Example: the HTML = "< root > {{name}} < div > 1 < / div > < / root >"
} else if (html[index + 1]! = ='/') {
// 1.1 Get the element type
let gtIndex = html.indexOf(">")
let eleType = html.slice(index + 1, gtIndex).trim()
EleType = 'div id="app"' after processing: eleType = 'div'
let emptyIndex = eleType.indexOf("")
let attrs = {}
if(emptyIndex ! = = -1) {
// 1.3 Get the element tag attribute
attrs = parseAttr(eleType.slice(emptyIndex + 1))
eleType = eleType.slice(0, emptyIndex)
}
1.4 Creating an AST Node
const element = {
children: [],
attrs,
parent: currentParent,
tag: eleType,
type: 1
}
// 1.5 has no root element node
if(! root) { root = element }else {
// 1.6 Pushes the current element node into children of the parent element
currentParent.children.push(element)
}
// 1.7 Parsing to the start of the element tag pushes the element up the stack
stack.push(element)
// 1.8 Updates the current parent element
currentParent = element
// 1.9 truncates the part that has been processed
html = html.slice(gtIndex + 1)
HTML = ""
} else {
let gtIndex = html.indexOf(">")
// 3.1 Parse to the element's end tag hierarchy and unstack one
stack.pop()
// 3.2 Update the current parent element
currentParent = stack[stack.length - 1]
// 3.3 Cut off the part that has been processed
html = html.slice(gtIndex + 1)}}return root
}
// Parse text nodes
function parseText(text) {
Unparsed text
let originText = text
// May be plain text or variable text default: plain text
let type = 3
// The text node of the element node may be composed of multiple segments
/ / example: < p > I {{name}}, I {{age}} < / p > token = [' my '{{name}},' my ', {{age}}]
let token = []
while (text) {
let start = text.indexOf({{" ")
let end = text.indexOf("}}")
//4.0 If interpolation exists
if(start ! = = -1&& end ! = = -1) {
// 4.1 Marks the text node type as text with variables
type = 2
// 4.2 There is plain text before interpolation
if (start > 0) {
// 4.3 Advance token before interpolation in plain text
token.push(JSON.stringify(text.slice(0, start)))
}
// 4.4 Get the expression in the interpolation
let exp = text.slice(start + 2, end)
// 4.5 Parse expressions and advance tokens
token.push(`_s(${exp}) `)
// 4.6 Cut off the part that has been processed
text = text.slice(end + 2)
// 5.0 There is no interpolation
} else {
// 5.1 Terminates text to push tokens directly
token.push(JSON.stringify(text))
text = ' '}}let element = {
text: originText,
type
}
// 5.3 If type is 2, the variable text node requires expression
if (type === 2) {
element.expression = token.join("+")}return element
}
// Parse the tag attributes
function parseAttr(eleAttrs) {
let attrs = {}
attrString = eleAttrs.split("")
attrString.forEach(e= > {
if (e && e.indexOf("=")! = = -1) {
const attrsArr = e.split("=")
attrs[attrsArr[0]] = attrsArr[1]}else {
attrs[e] = true}});return attrs
}
Copy the code
Code generator section
// Convert AST to render function body
/**{children: [{...}], parent: {}, tag: "div", type: 1, //1- element node 2- text node 3- text node, expression: '_s (name)', / / the type if is 2, it returns _s (variable) text: '{{name}} / / text string} * / node before compilation
function codegen(ast) {
// The first layer of the 1.0 AST must be an element node
let code = genElement(ast)
return {
// When the rendering function is executed, passing this to change the direction of this in the function body.
render: `with(this){return ${code}} `}}// Convert the element node
function genElement(el) {
// 2.1 Obtaining child nodes
let children = genChildren(el)
// 2.0 returns _c(tag name, tag attribute object, tag child node array), and changes the tag name to JSON string
return `_c(The ${JSON.stringify(el.tag)}.The ${JSON.stringify(el.attrs)}.${children}) `
}
// Convert the text node
function genText(node) {
// 5.0 Text nodes with variables
if (node.type === 2) {
// node.expression Any variable is evaluated by this.[node.expression]!!!!
return `_v(${node.expression}) `
}
// 5.1 Plain text nodes must become JSON strings or they will be treated as variables
return `_v(The ${JSON.stringify(node.text)}) `
}
// Determine the node to which the type is transferred
function genNode(node) {
// 4.0 Check the node type
if (node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
}
// Convert the child node
function genChildren(node) {
// 3.0 Checks whether there are child nodes
if (node.children && node.children.length > 0) {
// 3.1 Convert all child nodes [child node 1, child node 2... , recursively convert all child nodes genNode--genElement--genChildren--genNode
return ` [${node.children.map(node => genNode(node))}] `}}Copy the code