Realization of the array of hijacking
As mentioned in the previous article, object.defineProperty can’t be used to hijack an array, so what does Vue do to solve this problem? Vue is a prototype object that creates an Array. The prototype of this object points to array. prototype. The specific implementation code is as follows:
const arrayProto = Object.create(Array.prototype)
let methodName = ['push'.'pop'.'shift'.'unshift'.'reverse'.'sort'.'splice']
methodName.forEach(method= > {
arrayProto[method] = function(. args) {
// The element of an array push can be an object, and it is reactive to handle the element of an array as an object
if(method === 'push') {
this.__ob__.observeArray(args)
}
const result = Array.prototype[method].apply(this, args)
this.__ob__.dep.notify()
return result
}
})
// Then use the Observer method above to determine whether the current listening object is an array
class Observer {
constructor(data) {
this.dep = new Dep()
if(Array.isArray(data)) {
data.__proto__ = arrayProto
this.observeArray(data)
} else {
this.walk(data)
}
Object.defineProperty(data, '__ob__', {
value: this.enumerable: false.writable: true.configurable: true})}// Treat the elements of the array as objects, and make them responsive
observeArray(arr) {
for(let i = 0; i < arr.length; i++) { observe(arr[i]) } } ... }Copy the code
To realize the computed
Those of you familiar with VUE know that computed attributes depend on data in data, and that computed attributes are automatically updated when data in data changes, and that attributes in computed attributes cannot be set manually. It should have the advantages of inertia and caching. In addition to the use of computed data, You mentioned in the official website that expressions in vUE templates are designed for simple calculations, and complex logical operations involved in templates should be included in calculation attributes. Now we start implementing a computed property:
class MVue{
constructor(){
this.ininComputed()
// Watcher can listen on attributes in computed
this.initWatcher()
}
...
initComputed() {
let computed = this.$options.computed
if(computed) {
Reflect.ownKeys(computed).forEach(watcher= > {
const compute = new Watcher(this, computed[watcher], () = > {})
Object.defineProperty(this, watcher,{
enumerable: true.configurable: true.get: function computedGetter() {
compute.get()
return compute.value
},
set: function computedSetter() {
console.warn('Compute property not assignable')}})})}}}Copy the code
This is just a simple implementation of computed, but we all know that the computed property has two characteristics:
-
- The calculated property is
inert
When other attributes on which the calculated attribute depends change, the calculated attribute is not immediately reexecuted until it is retrieved.
- The calculated property is
-
- The calculated property is
The cache
If there is no change in other attributes that the calculated attribute depends on, the calculated attribute will not be recalculated even if it is re-evaluated.
- The calculated property is
class Watcher {
constructor(vm, exp, cb, options = {}) {
this.lazy = this.dirty = !! options.lazythis.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watchId
if(!this.lazy) {
this.get()
}
}
get() {
Dep.target = this
if(typeof this.exp === 'function') {this.value = this.exp.apply(this.vm)
} else {
this.value = this.vm[this.exp]
}
Dep.target = null
}
// The run method does not implement laziness when lazy is true
update() {
// If it is lazy, do not execute it
if(this.lazy) {
this.dirty = true
} else {
this.run()
}
}
run() {
if(watcherQueue.includes(this.id)) {
return
}
watcherQueue.push(this.id)
Promise.resolve().then(() = >{
this.cb.call(this.vm)
watcherQueue.pop()
})
}
}
// Change initComputed as follows
// Initialize the calculated properties
initComputed() {
let computed = this.$options.computed
if(computed) {
Reflect.ownKeys(computed).forEach(watcher= > {
const compute = new Watcher(this, computed[watcher], () = > {}, { lazy: true })
Object.defineProperty(this, watcher,{
enumerable: true.configurable: true.get: function computedGetter() {
// Get cache if compute is dirty!
if(compute.dirty) {
compute.get()
compute.dirty = false
}
// Return the result of the last read
return compute.value
},
set: function computedSetter() {
console.warn('Compute property not assignable')}})})}}Copy the code
Template compilation
Simple implementation of vUE template compilation, we can use
new Watcher(this.() = > {
document.querySelector('#app').innerHTML = `<p>The ${this.name}</p>`
}, () = >{})
Copy the code
1. However, this implementation can use template syntax and requires some processing of the template, which is ultimately converted into a function that performs DOM updates 2. Replacing all the DOM directly is expensive, so it’s best to update the DOM as needed.
In order to minimize unnecessary DOM manipulation and achieve cross-platform features, Virtual dom is introduced in VUE
What is the vdom? It’s basically a JS object that describes what the DOM looks like.
To get the VDOM of the current instance, each instance needs to have a render function to generate the VDOM, called the render function
When a Vue instance is passed dom or template, the first step is to convert the template string into a rendering function, which is compiled.
Vue compilation principle
- 1. Convert template strings to the Element AST parser.)
- 2. Static node marks for AST to optimize VDOM rendering
- 3. Use element ASTs to generate the render function code string
Note: AST is a code into another code, is a description of source code. Vue will treat the template structure as a string. Here, I will simulate the template compilation in VUE to implement the element node Parser, as follows: (An AST structure is generated after parser)
/ / the parser
function parser(html){
let stack = []
let root
let currentParent
while (html) {
let index = html.indexOf('<')
// There are text nodes in front of it
if (index > 0) {
let text = html.slice(0, index)
const element = {
parent: currentParent,
type: 3,
text
}
currentParent.children.push(element)
// Intercepts HTML as the rest of the HTML except for the text node
html = html.slice(index)
} else if( html[index + 1]! = ='/' ) {
// There is no text node in front and a start tag
let gtIndex = html.indexOf('>')
const element = {
type: 1.tag: html.slice(index + 1, gtIndex),
parent: currentParent,
children: []}if(! root) { root = element }else {
currentParent.children.push(element)
}
stack.push(element)
currentParent = element
html = html.slice(gtIndex + 1)}else {
// End tag
let gtIndex = html.indexOf('>')
stack.pop()
currentParent = stack[stack.length - 1]
html = html.slice(gtIndex + 1)}}return root
}
// Parse a text node
function parseText(text) {
let originText = text
let type = 3
let tokens = []
while(text) {
let start = text.indexOf('{{')
let end = text.indexOf('}} ')
if(start ! = = -1&& end ! = = -1) {
type = 2
if(start > 0) {
tokens.push(JSON.stringify(text.slice(0, text)))
}
let exp = text.slice(start + 2, end)
tokens.push(`_s(${exp}) `)
text = text.slice(end + 2)}else {
tokens.push(JSON.stringify(text))
text = ' '}}let element = {
text: originText,
type
}
if(type === 2) {
element.expression = tokens.join('+')}return element
}
Copy the code
After the AST is generated by Parser, we need to convert the AST into a rendering function.
- 1. Recursive AST, the following structure is generated when the element node is encountered
_c(Tag name, attribute object, descendant array)
- 2. If a text node is encountered, the following structure is generated for plain text
_v(text string)
- 3. If a text node with a variable is encountered, it is generated
_v(_s(variable name))
- 4. In order to make variables can be retrieved normally, generate the last string package layer
with(this)
- 5. Finally, generate a function from the string as a function and mount it to
vm.$options
on
$option.render. Call (vm); $option.render. Call (vm);
// type: 1 element node 2 text node with variables 3 plain text node
// The core method converts the AST to the render function
function generate(ast) {
let code = genElement(ast)
return {
render: `with(this)${code}`}}// Convert the element node
function genElement(el) {
let children = genChildren(el)
return `_c(The ${JSON.stringify(el.tag)}, {}, ${children}) `
}
// Iterate over descendant nodes
function genChildren(el){
if(el.children.length) {
return '['+ el.children.map(child= > genNode(child)) +'] '}}// Convert the text node
function genText(node) {
if(node.type === 2) {
return `_v(${node.expression}) `
} else if( node.type === 3 ) {
return `_v(The ${JSON.stringify(node.text)}) `}}// Convert nodes
function genNode(node) {
// Element node
if(node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
}
Copy the code
The next installment will continue to implement a VDOM, along with some analysis of common vUE principles