Attribute descriptor
Object. DefineProperty (attribute descriptor) is a core API in Vue, which implements property proxying and data hijacking to make data responsive.
Object. DefineProperty syntax
Object.defineProperty( object, prop, descriptor )
- Object: required, target object
- Prop: Required, property name that needs to be defined or modified
- descriptor: Required, property of the target property
- Value: Indicates the value of the target attribute. The default value is undefined
- Writable: indicates whether the target attribute can be overwrited. true: indicates that the attribute can be overwrited. false: indicates that the attribute cannot be overwrited. the default value is false
- enumberable: Whether the target attribute is enumerable (yes
for... in
.Object.keys()
), true: enumerable false: not enumerable The default value is false - The writable, Enumberable, and 64x property of the target can be deleted and reset without any additional control system. The target property cannot be deleted or the property property cannot be reset Default is false
- get:function(){… } : When this property is accessed, the get method is triggered. The get method returns the value as the value of the property
- set:function(value){… } : When the value of this property is modified, the set method is fired. The new value of the property is passed in as an argument to the function
Note: When using the get and set methods, writable and value are not allowed
const person = {}
let initName = 'brother'
ObjectDefineProperty (obj,"name", {get() {
return initName
},
set(newVal) {
initName = newVal
}
})
console.log(person.name) // 'brother'
person.name = 'brother'
console.log(person.name,initName) // 'brother' 'brother'
Copy the code
Implementing the data broker
In Vue, we can access all the properties of Data through the instance because the Vue instance proxies the data property.
class Vue {
constructor(options) {
// Attach the configuration item to the Vue instance
this.$options = options
this._data = options.data
// Initialize data with initData (mount data attribute to Vue instance)
this.initData()
}
initData() {
let data = this._data
let keys = Object.keys(data)
// Traversing data will block all data fields to the Vue instance
for (let i= 0; i<keys.length; i++) {
Object.defineProperty(this,key[i],{
enumberable: true.configurable: true.set: function proxySetter(newVal) {
data[keys[i]] = newVal
},
get: function prosyGetter() {
return data[keys[i]]
}
})
}
}
}
const VM = new Vue({data: {name:"Zhang"}})
console.log(VM)
VM.name = 'bill' // Change the value of the data attribute name
console.log(VM.name) // 'I'
Copy the code
Implementing data hijacking
In the set and GET properties of Vue instance properties, you can hijack the read and write operations of each property, and then do some special logic to achieve responsiveness.
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
this.initData()
}
initData() {
let data = this._data
let keys = Object.keys(data)
// Data broker.// Data hijacking
// Note: The data hijacking operation will be reused later, so it is implemented separately, not with the data broker for loop
for (let i= 0; i<keys.length; i++) {
let value = data[keys[i]]
Object.defineProperty(this,key[i],{
enumberable: true.configurable: true.set: function reactiveSetter(newVal) {
if(newVal===data[keys[i]]) return // Terminate the logic with the same value as before
console.log(`${keys[i]}It's given a new value${newVal}`)
value = newVal
},
get: function reactiveGetter() {
console.log(` access${keys[i]}The value of the `)
return data[keys[i]]
}
})
}
}
}
const VM = new Vue({data: {name:"Zhang"}})
console.log(VM)
VM.name = 'bill' // name is given a new value -- li Si
Copy the code
Recursive deep data hijacking
The above operation only realizes the hijacking of one-dimensional data. If a data is a complex data type, the read and write of the internal attributes of the data cannot be hijacked. So we need to recursively hijack the data. That is:
const VM = new Vue({
data: {person: {name:"Zhang"}}})console.log(VM)
VM.person = 'bill' // person is given a new value -- Li Si
VM.person.name = 'bill' // The setter for name cannot hijack data
Copy the code
class Vue {...initData(){...// Data hijacking
observe(data)
}
}
// 1. Observe the data type and create an Observer instance
function observe(data) {
// 1.1 Returns if the observed data is of a basic type
const type = Object.prototype.toString.call(data)
if(type ! = ='[object Object]'|| (type ! = ='[object Array]')) return
// 1.2 Observation Data involves some complex logic to encapsulate this process as an Observer class
new Observer(data)
}
// 2, Observer class: used to observe data, generate complex logic responsible for handling dependencies Dep instances, etc
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
let keys = Object.keys(data)
// 2.1 Put data hijacking operations in initData here
// Iterate over data, hijacking all attributes passed to data
for (let i= 0; i<keys.length; i++) {
defineReactive(data,keys[i],data[keys[i]])
}
}
}
// defineReactive utility function: used to recursively hijack data and turn it into responsive data
defineReactive(object,key,value){
// 3.1 recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
observe(obj[key])
// 3.2 data hijacking
Object.defineProperty(object,key,{
enumberable: true.configurable: true.// Note: Do not access the object property through object in the setter/getter of the object property or you will get stuck in an infinite loop
set: function reactiveSetter(newVal) {
if(newVal===value) return // Terminate the logic with the same value as before
console.log(`${keys[i]}It's given a new value${newVal}`)
// Update the view
value = newVal
},
get: function reactiveGetter() {
console.log(` access${keys[i]}The value of the `)
return value
}
})
}
Copy the code
Logical combing
letOption = {person: {name:"Zhang"}}let vm = new Vue(option)
Copy the code
1. New Vue instance
At this stage, option instances, option.data, etc., are mounted to the Vue sample, and methods such as initData are called to complete the initialization of the relevant data
2. Data Initialization
This stage is implemented in initData method, which will complete data initialization operations such as 1. Traverse option.data, mount data data such as Person to Vue instance 2
3, Determine the data type, new Observer instance
In this stage, observe the parameter data through the observe method. If the data type is complex, observe the data through the new Observer instance. The argument passed in the initial call is option.data
4. Hijacking data
In this phase of the Observe class instantiation, we first call the instance Walk method to iterate over the incoming data and call defineReactive to add reactive logic to each data
5. Responsiveness of data
In the defineReactive function at this stage, observe is called to pass in data first to realize the recursive hijacking of deep-seated data and the responsive logic of adding data in set/ GET
The realization of the Watcher
Watcher can listen for changes to data and trigger various callbacks, such as template update callbacks, calculated property update callbacks, and so on. Since template updates involve template compilation and VDOM, the Watcher implementation is tested with the Watch option and $watch
1. Add Watcher with option
var vm = new Vue({
data: {msg: 1
},
watch: {// When the data. MSG value changes, the callback is executed with both the changed value and the previous value of data. MSG.
msg(val, oldVal){... }}})Copy the code
2. Call the instance method to add Watcher
var callback = function(val, oldVal) {... }var unwatch = vm.$watch('msg',callback)
// Remove the listener
unwatch('msg',callback)
Copy the code
Implementation approach
Add an event center for each reactive data through which the Watcher is collected. When responsive data changes, it notifies Watcher of updates through the event center. In Vue, the function of the event center is implemented using the abstract class Dep.
Dep class
Add a Dep instance for each responsive Data and save, collect, and distribute watcher through the Dep.
// defineReactive utility function: used to recursively hijack data and turn it into responsive data
function defineReactive(obj, key, value) {
// Recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
observe(obj[key])
// create a new Dep instance for each data and maintain it through closures
let dep = new Dep()
// Data hijacking
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() {
// 4.5. Dep collects dependencies
dep.depend()
return value
}
})
}
// 4, Dep abstract class: responsible for collecting dependencies, notification of dependency updates, etc
class Dep {
// 4.1 subs is used to save all subscribers
constructor(option) { this.subs = []}
The Depend method is used to collect subscriber dependencies
depend() { this.subs.push(/** New callback */)}
// the notify method is used to send subscribers updates
notify(newVal,value) {
this.subs.forEach(watcher= > watcher.update(newVal,value))
}
}
Copy the code
Watcher class
The logic involved in each subscriber callback is complex, so it is isolated into the Watcher class.
// Class Dep: collects dependencies, notifydependency updates, etc
class Dep {
constructor(option) {
// 4.1 subs is used to save all subscribers
this.subs = []
}
The Depend method is used to collect subscriber dependencies
depend() {
// 5.5 if the Watcher instance is initialized
if (Dep.target) {
// 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
this.subs.push(Dep.target)
}
}
// the notify method is used to send subscribers updates
notify(newVal,value) {
Execute Watcher's run method for each subscriber to complete the update
this.subs.forEach(watcher= > watcher.run(newVal,value))
}
}
// The Watcher class triggers dependency collection and handles update callbacks
class Watcher {
constructor(vm, exp, cb) {
Mount Vue instance, data attribute name, and update callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
// 5.2, trigger the getter for data to complete the dependency collection
this.get()
}
get() {
// set the Watcher instance as the target object for Dep dependency collection
Dep.target = this
Trigger data getter interceptor
this.vm[this.exp]
// Clear the dependent target object
Dep.target = null
}
run(newVal,value) {
this.cb.call(this.vm,newVal,value)
}
}
Copy the code
Implement vm. $watch
Subscription callbacks are added dynamically in the Vue class via the $watch instance method
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
this.initData()
}
initData() {....}
// add subscription callback dynamically
$watch(key, cb) {
new Watcher(this, key, cb)
}
}
/ / test
let vm = new Vue({ data: {message: 111} })
vm.$watch("message".function (val, oldVal) {
console.log("Message value changed", val, oldVal);
})
vm.message = 22 // Print message value changed by 22 111
Copy the code
Implementation option. Watch
Subscription callbacks are added dynamically in the Vue class through the initWatch instance method
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
this.initData()
this.initWatch()
}
initData() {....}
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]])
}
}
}
// add subscription callback dynamically
$watch(key, cb) {
new Watcher(this, key, cb)
}
}
/ / test
let vm = new Vue({
data: {
name: "Ha ha ha.".age: 13
},
watch: {
age(newVal, val) {console.log("age", newVal, val); },name(newVal, val) {console.log("name", newVal, val); } } }) vm.age =22
vm.name = "Test"
Copy the code
Asynchronous Watcher
The Watcher callback is executed asynchronously in Vue source code.
Why is the Watcher callback set to execute asynchronously?
- A watcher executed asynchronously avoids the watch callback being executed first
// When watcher calls back to sync
// Mounted If the value of this.age is modified, the age watch callback will be triggered, and the value of this.name will be modified
let vm = new Vue({
data: {
name: "Ha ha ha.".age: 13
},
watch: {
age(newVal, val) {
this.name = 'bill'}},mounted() {
this.age = 20
console.log(this.name) // 'I'}})Copy the code
- 2. Avoid triggering the watch callback multiple times, which is conducive to performance optimization
// When the watcher callback is synchronized, the data monitored by watch is frequently modified. The watch callback will be triggered multiple times. Performance is wasted.
let vm = new Vue({
data: {
name: "Ha ha ha.".age: 13
},
watch: {
age(newVal, val) {
this.name = 'bill'}},mounted() {
this.age = 20
this.age = 21
this.age = 22
this.age = 23}})Copy the code
Implementation approach
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// The Watcher class triggers dependency collection and handles update callbacks
class Watcher {
constructor(vm, exp, cb) {
Mount Vue instance, data attribute name, and update callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
// 5.2, trigger the getter for data to complete the dependency collection
this.get()
}
get() {
// set the Watcher instance as the target object for Dep dependency collection
Dep.target = this
Trigger data getter interceptor
this.vm[this.exp]
// Clear the dependent target object
Dep.target = this
}
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(() = > {
this.cb.call(this.vm, newVal, value)
// 5.10 The task is deleted from the task queue
watcherQueue.splice(index, 1)}}}Copy the code
The realization of $set
What does $set do, and what problems does it solve?
<body>
<script crossorigin="anonymous"
integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
<script>
let VM = new Vue({
data: {
person: {
age: 10}},watch: {
person() {
console.log("Person has changed"); }}})// Pass the object. Property does not turn the property into responsive data, nor does it trigger the callback in Watch that listens on Person
// vm.person. name = 'haha'
// Through the $set of the Vue instance, you can add responsive attributes to the object and trigger a callback in watch that listens on Person
// VM.$set(VM. Person, 'name', 'haha ')
</script>
</body>
Copy the code
Implementation approach
Printing Vue instances we can see that each data has an __ob__ attribute. This __ob__ is the Observer instance, which we configured as a closure for each data and collected and distributed dependencies through the Dep instance. Here you can mount an Observer instance (__ob__) for each data data, modify the data data with $set, and issue data dependent callbacks.
So the implementation of $set is basically as follows:
- In the generated
Observer
Instance, also create a new oneDep
Instance (event center), hanging inObserver
Instance. thenObserver
The instance is mounted to the data data. - The trigger
getter
, not only willWatcher
In the closureDep
Collect a copy of the instance, also in__ob__
theDep
A copy is also collected in the instance. - use
$set
Is triggered manually__ob__.Dep.notify()
Distribute dependency updates. - Before calling notify, you need to call notify
definedReactive
Change the new attribute to reactive.
Full source code as of this stage:
// 6.X is the implementation of $set related source code!!
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
this.initData()
this.initWatch()
}
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]])
}
}
}
$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)
}
}
// 1. Observe the data type and create 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()
// 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++) {
// console.log("definedBeforer",keys[i]);
defineReactive(data, keys[i], data[keys[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
}
})
}
// 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 = []
}
The Depend method is used to collect subscriber dependencies
depend() {
// 5.5 if the Watcher instance is initialized
if (Dep.target) {
// 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
this.subs.push(Dep.target)
}
}
// the notify method is used to send subscribers updates
notify(newVal, value) {
Execute Watcher's run method for each subscriber to complete the update
this.subs.forEach(watcher= > watcher.run(newVal, value))
}
}
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber that triggers dependency collection and handles update callbacks
class Watcher {
constructor(vm, exp, cb) {
Mount Vue instance, data attribute name, and update callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
// console.log("watchID",watcherId);
this.id = ++watcherId
// 5.2, trigger the getter for data to complete the dependency collection
this.get()
}
get() {
// set the Watcher instance as the target object for Dep dependency collection
Dep.target = this
Trigger data getter interceptor
this.vm[this.exp]
// Clear the dependent target object
Dep.target = null
}
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(() = > {
this.cb.call(this.vm, newVal, value)
// 5.10 The task is deleted from the task queue
watcherQueue.splice(index, 1)}}}Copy the code
Implementation of the array processing
Vue2.X has some flaws in its handling of arrays
Possible problems with hijacking an array by array subscript:
Listening subscript callback disorder: using array subscripts to hijack an array may cause listening subscript callback disorder when the order of array elements changes.
let arr = [
{name:"0"},
{name:"1"},
{name:"2"}]// It is possible to hijack each element by array subscript during initialization, but inserting a new element before each subscript 1 causes listening subscript callbacks to be corrupted.
// The callback that arR [1] used to handle element {name:2} becomes the callback that handles the insertion of new elements.
Copy the code
Wasteful performance: An arraylist may contain dozens, hundreds, or even thousands of items when the user is actually manipulating only a dozen items of data. This is obviously out of proportion to our efforts. As Yudhoyono has mentioned, the cost of hijacking all the data in an array is far out of proportion to our benefits. So Vue 2.x abandoned this approach.)
Source code for the array processing
In the source code, there is no use of array subscript to hijack the array, the array to do the following processing 1, array dependency callback collection is also through __ob__.dep implementation. __ob__.dep.notify is manually triggered when the array calls methods like push, pop, etc. 2. On the array prototype object, we insert a new object as the middle layer. When a user calls a method on an array prototype object, it goes through the middle layer first. We can intercept it by executing the original method and dispatching the dependency update after triggering __ob__.dep.notify(). Change each element of an array to a responsive transform array method:
// 7.x is the new code to implement the transform array
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
}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++) {
// console.log("definedBeforer",keys[i]);
defineReactive(data, keys[i], data[keys[i]])
}
}
}
// 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.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
}
})
Copy the code
Change array elements and inserted new data to reactive:
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++) {
// console.log("definedBeforer",keys[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])
}
}
}
// 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
}
})
Copy the code
Implementing computed properties
The characteristics of
1, it is a function, its value is the result of the operation of the function. 2, the change of any data used to calculate the property will cause the change of the calculated property. 3, the calculated property does not exist in the data, need to be initialized separately. 4. The calculated property is read-only and cannot be modified. It doesn’t have setters. 5. The calculated property is lazy, and it doesn’t recalculate immediately when the data it depends on changes, only when you retrieve the calculated property. 6. The calculated attributes are cached. When the data on which they depend has not changed, the results of the calculated attributes will not be retrieved, but the previous calculation results will be used.
Implementation approach
The calculated property itself is also a Watcher callback, except that it may depend on multiple properties. When Watcher is initialized, the second parameter is passed in the key name representing the properties that Watcher depends on. We can pass in and execute a function that evaluates a property, which triggers getters for multiple dependent properties in the evaluated property and collects watcher callbacks for the evaluated property.
Preliminary implementation (8.0-8.8):
// This phase code is 8.0 -- 8.8
class Vue {
constructor(options){...// 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()
}
...
// 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
const watcher = new Watcher(this, computeds[keys[index]],function() {})// 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() {
watcher.get()
returnwatcher.value } }) } } } ... }...let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber that triggers dependency collection and handles update callbacks
class Watcher {
constructor(vm, exp, cb) {
Mount Vue instance, data attribute name, and update callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
// console.log("watchID",watcherId);
this.id = ++watcherId
// 5.2, trigger the getter for data to complete the dependency collection
this.get()
}
get() {
// 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 data getter interceptor
this.value = this.vm[this.exp]
}
// Clear the dependent target object
Dep.target = null
}
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(() = > {
this.cb.call(this.vm, newVal, value)
// 5.10 The task is deleted from the task queue
watcherQueue.splice(index, 1)})}}...Copy the code
Caching and lazy processing of calculated attributes (8.9-8.15):
Lazy = true we make an identifier for the watcher that evaluates the property this.lazy = true, which means that the watcher is lazy. We also add a this.dirty identifier to indicate that the dependency of the calculated property has changed and that the dirty value of the calculated property must be reevaluated and cannot be used in the last calculation.
class Vue {
constructor(options){...// 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()
...
}
// 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
}
return watcher.value
}
})
}
}
}
}
// 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 = []
}
The Depend method is used to collect subscriber dependencies
depend() {
// 5.5 if the Watcher instance is initialized
if (Dep.target) {
// 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
this.subs.push(Dep.target)
}
}
// 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 that triggers dependency collection and handles update callbacks
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 update callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
// 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()
}
}
get() {
// 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 data getter interceptor
this.value = this.vm[this.exp]
}
// 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 {
thiss.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(() = > {
this.cb.call(this.vm, newVal, value)
// 5.10 The task is deleted from the task queue
watcherQueue.splice(index, 1)}}}Copy the code
The test was fine and did not recalculate without changing the data on which the calculated properties depended.
Current bug in calculating properties:
Problem description
When we change the dependency (person) on the evaluated property (x), neither the evaluated property nor the Watch callback that listens on the evaluated property is fired.
- Our code
<script src="./index.js"></script>
<script>
let vm = new Vue({
data: {
person: {
name: "Zhang"}},watch: {
x(oldValue, newValue) {
console.log("X listening trigger"); }},computed: {
x() {
console.log("X calculation trigger");
return JSON.stringify(this.person)
}
}
})
</script>
Copy the code
` `
- In Vue, there are no problems
<script crossorigin="anonymous"
integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
<script>
let vm = new Vue({
data: {
person: {
name: "Zhang"}},watch: {
x(oldValue, newValue) {
console.log("X listening trigger"); }},computed: {
x() {
console.log("X calculation trigger");
return JSON.stringify(this.person)
}
}
})
</script>
Copy the code
Problem a
It is convenient to explain here that we name the calculated attribute and watcher in Watch as Watcher 1 and Watcher 2 respectively.
The callback to Watcher 1 has two prerequisites: the data it depends on changes to dirty data and the watcher is evaluated. We changed the dependency but did not evaluate it. Watcher 2 listens on Watcher 1. When watcher 2 is initialized, watcher 1 is evaluated first.
When the Vue is initialized, Person collects a copy of the # 1 Wtacher in Person.dep. When the value of person changes to notify, watcher should be evaluated once.
// 9.0 calls watcher's get method and evaluates it
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 update callback to Watcher instance
this.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
// 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()
}
}
get() {
// 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]
}
// 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 {
thiss.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)}}}Copy the code
Question 2
When we change the dependent person value for the computed property X, we find that the watch callbacks for the computed property and the watch callbacks for the computed property are still not fired. Let’s print the Person in Vue and the person in our code respectively to check whether there is any problem with the phone they depend on
The normal process
- The VM is initialized, and the DEP of vm. Person collects dependencies, one watcher and two watcher in turn.
- Vm. Person = {name:’ person ‘}, vm. Person = {name:’ person ‘}, vm. The DEP of vm.person updates all watcher with notify
- Because the evaluation property watcher 1 is lazy, calling the get method of watcher 1 does not evaluate. Just mark dirty on watcher 1 as true.
- When watcher 2 is updated, get (evaluate watcher 1) is called first, and the dirty of Watcher 1 is true. Wathcer (calculate attribute x) is executed first to calculate the result, and then the callback of Watcher 2 (x callback in watch) is executed.
Our PERSON DEP only collects watcher number one (calculated property), not Watcher number two. This is also why the watch callbacks that evaluate properties and listen to evaluate properties are not fired
Question 3
Why didn’t we collect watcher Number two?
- Look at our code, in initComputed, to generate watcher number one for the computed property X. Watcher initials the call to watcher.get(), mounts watcher 1 to dep.target in the get method, and executes vm.x.
class Watcher {
constructor(vm, exp, cb, option = {}) {
this.lazy = this.dirty = !! option.lazythis.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
if(! option.lazy) {this.get()
}
}
get() {
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]
}
// Clear the dependent target object
Dep.target = null}}Copy the code
- In vm.x,this.person triggers the getter for Person, and Person. dep collects the number one watcher
function defineReactive(obj, key, value) {
let childOb = observe(obj[key])
let dep = new Dep()
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
}
})
}
class Dep {
constructor(option) {
this.subs = []
}
The Depend method is used to collect subscriber dependencies
depend() {
// 5.5 if the Watcher instance is initialized
if (Dep.target) {
// 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
this.subs.push(Dep.target)
}
}
...
}
Copy the code
- Execute initWatch after initComputed, and in initWatch new number two watcher. When initialized, watcher # 2 is first mounted to dep. target, and get is called to trigger the getter of vm.x.
class Vue {.../ / 1.0
initWatch() {
const watches = this.$options.watch
if (watches) {
const keys = Object.keys(watches)
for (let index = 0; index < keys.length; index++) {
/ / 1.1
new Watcher(this, keys[index], watches[keys[index]])
}
}
}
initComputed() {
const computeds = this.$options.computed
if (computeds) {
const keys = Object.keys(computeds)
for (let index = 0; index < keys.length; index++) {
const watcher = new Watcher(this, computeds[keys[index]], function () {}, {lazy: true })
Object.defineProperty(this, keys[index], {
...
Watcher 2's get method fires the computedGetter for Watcher 1
get: function computedGetter() {
if (watcher.dirty) {
watcher.get()
watcher.dirty = false
}
return watcher.value
}
})
}
}
}
...
class Watcher {
constructor(vm, exp, cb, option = {}) {
this.lazy = this.dirty = !! option.lazythis.vm = vm
this.exp = exp
this.cb = cb
this.id = ++watcherId
if(! option.lazy) {// 2 2 watcher initializes the get method
this.get()
}
}
get() {
// 1.3 The second watcher is mounted to dep.target
Dep.target = this
if (typeof this.exp === 'function') {
this.value = this.exp.call(this.vm)
} else {
// 1.4 Watcher Number two triggers the getter for Watcher number one
this.value = this.vm[this.exp]
}
Dep.target = null}... }Copy the code
- When watcher 1 calls get, dep.targety is overridden by watcehr 1 and watcher 2. As a result, the DEP of Person failed to collect the number two Watcher
solution
- When the watcher is initially mounted to dep. target, we should use a stack to save the Watcher. When a new watcher is generated and collected, pop the new watcher from the stack and mount the previous watcher to dep.target.
- When DEP collects Watcher, Watcher collects DEP. When calculating the properties
getter
When finished, check to see if dep. target has any uncollected Watcher. If there are uncollected watchers, the deP collected by the calculated property Watcher is notified to continue collecting the DEP.target
Complete code (9.0-9.12 code for this stage)
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()
}
initData() {
let data = this._data
let keys = Object.keys(data)
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]]
},
})
}
observe(data)
}
initWatch() {
const watches = this.$options.watch
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)
}
}
function observe(data) {
const type = Object.prototype.toString.call(data)
if(type ! = ='[object Object]'&& (type ! = ='[object Array]')) return
// 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++) {
// console.log("definedBeforer",keys[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) {
// 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
}
})
Copy the code
There was no problem with the test.
Implement the compilation of templates
Having implemented Vue’s response test system, we now need to complete the response to the UI DOM when the data changes.
Let’s start with a little chestnut:
- In the Vue constructor, we create a new watcher, and in the watcher, we manipulate the DOM binding vm.name
- Since the second parameter of watcehr is an evaluation function, Watcher will execute the evaluation function first. In the evaluation function, the getter of vm.name is triggered, and the DEP of vm.name will collect the watcher. When vm.name is changed, vm.name.dep notifts Watcher that the update will execute the evaluation function again
class Vue {
constructor(options){...this.initWatch()
// Simple example: Vue initializes a new watcher to update Html via watcher
new Watcher(this.() = > {
document.querySelector("#app").innerHTML = `<p>The ${this.name}</p>`
}, () = >{})}... }Copy the code
Testing:
The NAME attribute is modified. DOM is updated successfully.
Compilation of templates in Vue
In Vue, the watcher responsible for updating the DOM is called the Render Watcher, and its evaluation function is far more complex than ours
We have problems
- The user can use template syntax, Vue instructions, etc., in the template, I need to process the template first and finally convert it into a function to update the DOM
- It is expensive to update the DOM directly, so we need to update the DOM on demand
Virtual DOM
The virtual DOM is an abstraction layer of the real DOM, which can reduce unnecessary DOM manipulation and achieve cross-platform features. Vue introduces the Virtual DOM(VDOM). What is the Virtual DOM? In a nutshell, it’s a JS object that describes what the CURRENT DOM looks like. Each Vue instance has a render function vm.$options. Render, which the instance can use to generate the VDOM. When a Vue instance is passed DOM or template, it first converts the template string into a rendering function, a process known as compilation.
Parser
The Vue compilation process is roughly divided into three steps
- The first step is to convert the template string to
Element ASTs
The process is throughThe parserThe implementation. - The second step
Element ASTs
Marking static nodes is mainly used to optimize virtual DOM rendering. This process passesThe optimizerimplementation - The third step will be
Element ASTs
convertrender
Function body, the process is throughCode generatorimplementation
AST
AST: Abstract syntax tree, which is a description of source code as it is converted from one type of code to another.
The Vue AST
Type indicates the type of the node. When type is 2, it indicates that the node uses variables. Expression records what variables are used
{
children: [{...}],parent: {},
tag: "div".type: 1.//1- element node 2- variable text node 3- plain text node,
expression:'_s(name)'.//type if 2, return _s(variable)
text:'{{name}}' // The string before the text node is compiled
}
Copy the code
There are two stages to generate an AST: lexical analysis and grammatical analysis
const a = 1
Lexical analysis:
Parse keywords such as const, a, =, 1 in your code and convert them to tokens.
Grammatical analysis:
After the token is processed and combined, the AST is generated in Vue. After the token is parsed, it is processed immediately.
Parse element nodes
This section uses simple HTML templates as an example and does not deal with complex cases such as V-if, V-show, V-for, single tag, and comment. Parsing element nodes, we can identify them with <, which can represent either a start tag or an end tag. If the start tag is, we add a layer to the ast tree hierarchy. If it is the end tag, it falls back to the last level in the AST tree hierarchy. Each layer also records its parent element. You also need a stack to keep track of the current level of the element, push the element onto the stack if it has a start tag, and push the element off the stack if it has an end tag. When it is a text node, the stack is not processed.
The sample
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)
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 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
Test: No problem
<body>
<div id="app">{{name}}<p>The first P tag</p>
<p>The second P tag<i>The label I</i></p>
</div>
<script src="./parse.js"></script>
<script>
const ast = parser(document.getElementById("app").outerHTML)
console.log(ast);
</script>
</body>
Copy the code
Parse text nodes
In the previous parse of the element node, the literal node was completely pulled out without parse. So let’s do that separately. Here we use {{and}} as identifiers to convert the difference expression in the text to the form _S (name)
function parser(html) {...while (html) {
...
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)
...
} else if (html[index + 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
}
Copy the code
test
<body>
<div id="app">My {{name}}, MY year {{age}}<p>The first P tag</p>
<p>The second P tag<i>The label I</i></p>
</div>
<script src="./parse.js"></script>
<script>
const ast = parser(document.getElementById("app").outerHTML)
console.log(ast);
</script>
</body>
Copy the code
Code generator
Previously we passed the template string (
) convert to the abstract syntax tree ASTs. Now we need to convert the abstract syntax tree to the render function using the code generator codeGen
View rendering functions in Vue source code
<body>
<div>123
<p>{{name}}</p>
</div>
<script crossorigin="anonymous"
integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
<script>
let vm = new Vue({
el: "div".data: { name: "Zhang"}})console.log("Vue", vm.$options.render)
</script>
</body>
Copy the code
function anonymous() {
`with(this){ return _c('div',[_v("123\n "),_c('p',[_v(_s(name))])]) }`
}
// Insert this, change the execution of this in the function body, and any variables or functions in the function will be treated as properties or methods of this
// `with(this){
// return this._c('div',[this._v("123\n "),this._c('p',[this._v(this._s(this.name))])])
/ /} `
Copy the code
Render function body:
_c: Element node used to transform the virtual DOM
- Parameter 1 (string) : The label name of the element node
- Parameter 2 (array) : Optional, child node of element node
_v: used to convert the plain text node. _s: Used to get the value of a variable in the text
Convert AST to render function ideas
- Recursive AST node that generates the following format string _C (tag name, tag attribute object, descendant array)
- If you encounter a text node if it’s a plain text node it generates the string _v(text string)
- _v(_s(variable name))
- Create a with(this) method outside the function, passing in the context.
New codegen. Js
// 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)
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 node
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
Test: Importing the parser we completed earlier, the parsed AST generates the body of the rendering function through the code generator
<body>
<div>The label before<p>I {{name}}</p>After the tag</div><! --<div id="app">1{{name}} This is the other content {{age}}</div> -->
<script src="./parse.js"></script>
<script src="./codegen.js"></script>
<script>
const ast = parser(document.querySelector("div").outerHTML)
const render = codegen(ast)
console.log(render);
</script>
</body>
</html>
Copy the code
Configure the parser and code generator in the source code
class Vue {
constructor(options) {
this.$options = options
......
// Simple example: Vue initializes a new watcher to update Html via watcher
// new Watcher(this, () => {
// document.querySelector("#app").innerHTML = `${this.name}
`
}, () => {})
// 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)
}
......
}
Copy the code
Create index.html and import parse.js, codegen.js, and index.js, respectively
<body>
<div>The label before<p>I {{name}}</p>
</div>
<script src="./parse.js"></script>
<script src="./codegen.js"></script>
<script src="./index.js"></script>
<script>
let vm = new Vue({
el: "div".data: {
name: 'ha ha',}})console.log("vm", vm.$options.render);
</script>
</body>
Copy the code
Implementing VDOM (Virtual DOM)
A VDOM is a JS object that describes the current DOM.
Such as:
<ul>
<li>1</li>
<li>2</li>
</ul>
// Corresponding VDOM
{
tag:"ul".attrs: {},children:[
{
tag:"li".attrs: {},chilren:[
{
tag:null.attrs: {},children: [].text:"1"}]}, {tag:"li".attrs: {},chilren:[
{
tag:null.attrs: {},children: [].text:"2"}]}]}Copy the code
The role of VDOM
1. It provides better performance in most cases than brute force refreshing the entire DOM tree.
Manipulating JS objects is fast, but manipulating DOM elements is slow. If the data changes, we can’t just regenerate the DOM from the template string and stuff it into the page, which is obviously a waste of time. We can use VDOM to describe the view and re-create the VDOM tree when the data changes. Compare the two VDOM trees to find the updated DOM node specified by the changed element.
2. VDOM is naturally cross-platform and only needs to call the DOM API of the corresponding platform. You can generate and update views for the platform.
The VDOM is generated by the render function
We do this through the VNode abstract class
// Add _c, _v, _s etc. methods to Vue abstract class 10.x-13.x
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)
}
}
// 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) {
console.log("_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
}
}
}
// Create a VNode abstract class
class VNode {
constructor(tag, attrs, children, text) {
this.tag = tag
this.attrs = attrs
this.children = children
this.text = text
}
}
Copy the code
Test: The virtual DOM was successfully obtained
VDOM diff and patch
VDOM is efficient because it can compare the difference between two VDOM nodes through diff algorithm, and then update the specified DOM node through patch. Before implementing diff and Patch, we need to implement a createEle method that converts the VDOM into the real DOM, which will be used later in DOM updates.
createEle
The createEle function converts the VNode and its children into a real DOM
// 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
}
Copy the code
test
Update views in response
When Vue is initialized, the new Watcher collects the dependency (this.name) through the evaluation function of the Watcher. When the dependency changes, the evaluation function reevaluates and completes the view update.
// Example before 10.0: Vue initializes new a watcher to update Html via watcher
new Watcher(this.() = > {
document.querySelector("#app").innerHTML = `<p>The ${this.name}</p>`
}, () = >{})Copy the code
When dependent data changes, the evaluation function is triggered to update the view immediately. This brute force update of the view is obviously a performance problem, and we need to introduce the virtual DOM to optimize performance.
Train of thought
- First implement a
$mount
Function, called when the real DOM is first mounted, in the originalrender watcehr
The logic of$mount
In the. - implementation
_update
Function that takes the new VDOM, compares the old and new vDOM, and updates the real DOM.render watcehr
The logic is no longer to violently update the view but to call_update
function
// 16. x-17.x: Vue initialization --$mount--new Watcher--_update--patch
class Vue {
constructor(options){...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
}
......
}
Copy the code
Realize the patch
Patch is the most core part of VDOM mechanism. The logic of VDOM to patch in Vue is realized by snabbDomJS library. The main implementation idea here does not exclude complex cases such as node attributes and keys.
Train of thought
- The patch function receives two parameters: the old vDOM and the new vDOM
- When mounted for the first time, patch old VDOM is passed in as real DOM, which needs to be handled separately
- Subsequent updates will fall into these categories
- If the new node does not exist, delete the corresponding DOM
- Call if the new and old node tags have different types or different text
createEle
Generate a new DOM and replace the old DOM - The old node does not exist. The new node exists. The call
createEle
Generate a new DOM and add a new DOM after the original DOM node - Traversing the child nodes recursively performs the above logic
// This stage code: 18.x
// 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
test
We can see that when we change the view binding name value, the view is updated.
The data responsive system, virtual DOM and DOM directed update have been completed to this Vue. Due to the word limit of this article, I will be in another article “detailed explanation of Vue 2.X core source code, hand lu a simple version of the Vue framework (next chapter” process summary and all the code (with notes) display