background
We all know the difference between watch and computed in use. So let’s dig a little deeper and see how they differ in implementation principles.
Implementation principle of Watch
-
Type: {[key: string] : string | Function | Object | Array}
-
Detail: an object whose key is the expression to observe and whose value is the corresponding callback function. The value can also be a method name, or an object that contains options. The Vue instance will call $watch() at instantiation time, iterating through each property of the Watch object.
1. Initialize the Watch
export function initState(vm) {
// Initialize watch on vm
if (opts.watch) {
initWatch(vm, opts.watch);
}
function initWatch(vm, watch) {
// Ignore the case where the value is the method name for now
for (key in watch) {
let handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); }}else{ createWatcher(vm, key, handler); }}}function createWatcher(vm, key, handler) {
return vm.$watch(key, handler);
}
// Extension methods on the Vue prototype are added in the form of mixins.
export function stateMixin(Vue) {
Vue.prototype.$watch = function(key, handler, options = {}) {
options.user = true;
// tag this.user to distinguish between rendering watcher and user watcher
new Watcher(this, key, handler, options); }}Copy the code
2. The watcher
- The expression
key/exprOrFn
To a function for later callsthis.getter
this.getter
Value of callObject.defineProperty
theget
In the methoddep.depend
Collect currentwatcher
- For the first time,
new Watcher
When calledget
Method to save the initialthis.value=this.get()
. Second user updaterun()
Call againget
Method to save the new valuenewValue=this.get()
And executes the callback function.
class Watcher {
constructor(vm, exprOrFn, callback, options) {
// ...
this.user = !! options.user
if(typeof exprOrFn === 'function') {this.getter = exprOrFn;
}else{
this.getter = function (){ // Convert an expression to a function
let path = exprOrFn.split('. ');
let obj = vm;
for(let i = 0; i < path.length; i++){ obj = obj[path[i]]; }returnobj; }}this.value = this.get(); // Log the initial value to the value property
}
get() {
pushTarget(this); // Save the user-defined watcher
const value = this.getter.call(this.vm); // Execute function (dependent collection)
popTarget(); / / remove the watcher
return value;
}
run(){
let value = this.get(); // Get a new value
let oldValue = this.value; // Get the old value
this.value = value;
if(this.user){ // If the user watcher calls the callback passed in by the user
this.callback.call(this.vm,value,oldValue)
}
}
}
Copy the code
Implementation principle of computed Data
- type:
{ [key: string]: Function | { get: Function, set: Function } }
- detailed: Calculation properties are not executed by default. The result of the evaluated property is cached unless dependent on the response
property
Change is what recalculates. Note that if a dependency (such as a non-reactive property) is outside the instance scope, the calculated property is not updated.
1. Initialize computed
Create a Watcher for each attribute key for computed
export function initState(vm) {
// Initialize computed on the VM
if(opts.computed) { initComputed(vm, opts.computed); }}function initComputed(vm, computed) {
const watchers = vm._computedWatchers = {}
for (let key in computed) {
/ / check
const userDef = computed[key];
// Revalue get when the dependent property changes
let getter = typeof userDef == 'function' ? userDef : userDef.get;
// Each attribute is essentially a watcher
// Map the watcher to the property
watchers[key] = new Watcher(vm, getter, () = > {}, { lazy: true }); // Not executed by default
// Define the key on the VMdefineComputed(vm, key, userDef); }}Copy the code
Define the key on the VM so that computed values can be fetched directly on the page
this._computedWatchers
Contains all computed properties- through
key
You can get the correspondingwatcher
thewatcher
Included in thegetter
If thedirty
δΈΊture
The callevaluate
function defineComputed(vm, key, userDef) {
let sharedProperty = {};
if (typeof userDef == 'function') {
sharedProperty.get = createComputedGetter(key)
} else {
sharedProperty.get = createComputedGetter(key);
sharedProperty.set = userDef.set ;
}
Object.defineProperty(vm, key, sharedProperty); // Computed is a defineProperty
}
Copy the code
Create a cache getter, get the value of the computed property, and go to this function.
function createComputedGetter(key) {
return function computedGetter() {
// This._computedWatchers contains all computed attributes
// The watcher contains the getter
let watcher = this._computedWatchers[key]
// Dirty means to call the user's getter
if(watcher.dirty){ // Determine if you need to reapply for a job based on the dirty attribute
watcher.evaluate();// this.get()
}
// If dep. target has a value that needs to be collected up after the current value is set
if(Dep.target){
watcher.depend(); // There are multiple dePs in watcher
}
return watcher.value
}
}
Copy the code
2. The watcher
class Watcher {
constructor(vm, exprOrFn, cb, options) {
/ /...
this.lazy = !! options.lazy;this.dirty = options.lazy; // If the attribute is calculated, the default is lazy:true,
this.getter = exprOrFn; // computed[key]/computed[key].get
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
const value = this.getter.call(this.vm);
popTarget();
return value
}
update() {
if(this.lazy){
this.dirty = true;
}else{
queueWatcher(this); }}evaluate(){
this.dirty = false; // False indicates that the value is overevaluated
this.value = this.get(); // User getter execution
}
depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend(); //lastName,firstName collect render watcher}}}Copy the code
The implementation of computed tomography is rather convoluted πππππ Let’s clear the air with a few questions QS1: How to cache computed attributes? By defining a dirty attribute on Watcher. When dirty is true, evaluate is called.
QS2: How do I reevaluate the calculated attribute? Change dependency values –> trigger set –> trigger dep.notify –> watcher. Update –> calculate watcher –> this. Call evaluate to reevaluate.
QS3: How do I update a view to calculate the value change of a property dependency? Currently, there is only one calculated property wacher on the deP of the dependent value (if not used in the page). To update the view, put the render Wacher into the DEP of the dependent data, so that the view can update if the dependent property changes.
conclusion
The difference between watch and computed is that the watch implementation assigns a Watcher to each key of the watch object, and the value this.get() is used to collect the current user watcher and save the initial value. When the key changes, watcher.run() is triggered, the new value is saved, and the cb callback is executed. Implement computed by assigning a lazy Watcher to each key of a computed object, which is not executed by default and is executed only when the value is specified. Object.defineproperty defines each key for computed on the VM. The current render Watcher is collected by key dependency values to implement dependency changes and view updates. The cache effect is implemented with the dirty attribute.