May 31, 2021 Article blog
This article is from the public number: justjavac, author justjavac
I once introduced Hongmun's Javascript framework, these days finally compiled the JS warehouse through, during which I stepped on a lot of pits, but also contributed a few PRs to Hongmun.
Today we're going to analyze the JS framework in the Hongmun system line by line.
All of the code in this article is based on the current latest version of Hongmun (version 677ed06 with submission date 2020-09-10).
Hongmun's use of Javascript to develop GUI is a pattern similar to WeChat's small program and light application. I n this MVVM mode, V is actually borne by C. The Javascript code is just one of the ViewModel layers.
The Hongmun JS framework is zero-dependent and uses only some npm packages during development packaging. T he packaged code does not rely on any npm packages. Let's first look at what the JS code written using the Hon Mun JS framework looks like.
export default {
data: {
return { count: 1 };
},
increase() {
++this.count;
},
decrease() {
--this.count;
},
}
If I don't tell you it's Hongmun, you'll even think it's vue or a small program. If you take the JS out of use separately (out of the HungMun system), the code looks like this:
const vm = new ViewModel({
data() {
return { count: 1 };
},
increase() {
++this.count;
},
decrease() {
--this.count;
},
});
console.log(vm.count); // 1
vm.increase();
console.log(vm.count); // 2
vm.decrease();
console.log(vm.count); // 1
All JS code in the repository implements a responsive system that acts as ViewModel in the MVVM.
Let's analyze it line by line.
There are four directories in the src directory, for a total of 8 files. O ne of them is a unit test. T here is also 1 performance analysis. R emove the 2 index .js files, and the total number of useful files is 4. It is also the focus of this analysis.
src
├── __test__
│ └── index.test.js
├── core
│ └── index.js
├── index.js
├── observer
│ ├── index.js
│ ├── observer.js
│ ├── subject.js
│ └── utils.js
└── profiler
└── index.js
The first is the entry file,
src/index.js
with only 2 lines of code:
import { ViewModel } from './core';
export default ViewModel;
It's actually a re-export.
Another similar file is
src/observer/index.js
which is also 2 lines of code:
export { Observer } from './observer';
export { Subject } from './subject';
Observer and subject implement an observer pattern. S ubject is the subject, that is, the observer. O bserver is the observer. T he subject needs to be proactively notified when there are any changes. This is responsive.
Both files are used in
src/observer/utils.js
so let's analyze the utils files first.
Divided into 3 parts.
export const ObserverStack = {
stack: [],
push(observer) {
this.stack.push(observer);
},
pop() {
return this.stack.pop();
},
top() {
return this.stack[this.stack.length - 1];
}
};
The first is to define a stack for the observer, following the principle of last-in, first-out, internal use of
stack
arrays for storage.
push
like the
push
function of the array,
observer
an observer observer at the top of the stack.
pop
like the
pop
function, removes the observer at the top of the stack and returns the deleted observer.
top
unlike
pop
operation,
top
is to take out the top element of the stack, but not delete.
export const SYMBOL_OBSERVABLE = '__ob__';
export const canObserve = target => typeof target === 'object';
A string constant SYMBOL_OBSERVABLE is
SYMBOL_OBSERVABLE
For the convenience of the back.
A function
canObserve
is defined, and whether the target can be observed. O
nly objects can be observed, so
typeof
used to determine the type of target. W
ait, there seems to be something wrong. I
f
target
is
null
the function also returns
true
I
f
null
is not observable, then this is a bug.
(At the time of writing, I had mentioned a PR and asked if this behavior was expected.)
export const defineProp = (target, key, value) => {
Object.defineProperty(target, key, { enumerable: false, value });
};
There's nothing to explain this, which is that
Object.defineProperty
code is too long to define a function to avoid code duplication.
Let's analyze the observer
src/observer/observer.js
in 4 parts.
The first part
export function Observer(context, getter, callback, meta) {
this._ctx = context;
this._getter = getter;
this._fn = callback;
this._meta = meta;
this._lastValue = this._get();
}
Constructor. Accept 4 parameters.
context
current observer is in the afternoon and the type is
ViewModel
When the third argument callback is called,
this
the
context
of the function.
getter
type is a function that gets the value of a property.
callback
type is a callback function that executes when a value changes.
meta
metadata.
Observers do not pay attention
meta
metadata.
On the last line of the constructor,
this._lastValue = this._get()
Let's analyze
_get
function.
Part II
Observer.prototype._get = function() {
try {
ObserverStack.push(this);
return this._getter.call(this._ctx);
} finally {
ObserverStack.pop();
}
};
ObserverStack
is the stack analyzed above to store all observers. P
ut the current observer on the stack and get the current value
_getter
C
ombined with the first part of the constructor, this value is stored
_lastValue
property.
Once this process has been performed, the observer has been initialized.
Part III
Observer.prototype.update = function() {
const lastValue = this._lastValue;
const nextValue = this._get();
const context = this._ctx;
const meta = this._meta;
if (nextValue !== lastValue || canObserve(nextValue)) {
this._fn.call(context, nextValue, lastValue, meta);
this._lastValue = nextValue;
}
};
This section implements the Dirty Checking mechanism when data is updated. C ompare the updated value with the current value, and if it is different, execute the callback function. I f this callback function is a rendering UI, you can render on demand. If the values are the same, then check to see if the new values you set can be observed and decide whether or not to execute the callback function.
Observer.prototype.subscribe = function(subject, key) {
const detach = subject.attach(key, this);
if (typeof detach !== 'function') {
return;
}
if (!this._detaches) {
this._detaches = [];
}
this._detaches.push(detach);
};
Observer.prototype.unsubscribe = function() {
const detaches = this._detaches;
if (!detaches) {
return;
}
while (detaches.length) {
detaches.pop()();
}
};
Subscribe and unsubscribe.
We used to talk about observers and observers. T here's another way to say about the Observer pattern, called subscription/publish mode. This part of the code, on the other hand, implements a subscription to the subject.
The
attach
method is called first to subscribe. I
f the subscription succeeds,
subject.attach
method returns a function that is unsubscribed when called.
In order to be able to unsubscribe in the future, this return value must be saved.
The implementation of subject should have been guessed by many people. O
bservers subscribe to subject, so all subject needs to do is notify the observer when the data changes.
How does subject know that the data has changed, using
Object.defineProperty
for property hijacking, as with vue2.
Let's analyze the
src/observer/subject.js
in 7 parts.
The first part
export function Subject(target) {
const subject = this;
subject._hijacking = true;
defineProp(target, SYMBOL_OBSERVABLE, subject);
if (Array.isArray(target)) {
hijackArray(target);
}
Object.keys(target).forEach(key => hijack(target, key, target[key]));
}
Constructor. T
here's basically nothing hard about it. S
et
_hijacking
property to
true
to indicate that the object has been hijacked.
Object.keys
hijacks each property throughout history.
If it is an array, call
hijackArray
Part II Two static methods.
Subject.of = function(target) {
if (!target || !canObserve(target)) {
return target;
}
if (target[SYMBOL_OBSERVABLE]) {
return target[SYMBOL_OBSERVABLE];
}
return new Subject(target);
};
Subject.is = function(target) {
return target && target._hijacking;
};
Subject's constructor is not called directly externally, but is encapsulated in
Subject.of
static method.
If the target cannot be observed, return the target directly.
If
target[SYMBOL_OBSERVABLE]
is not
undefined
the target has been initialized.
Otherwise, the constructor is called to initialize Subject.
Subject.is
is used to determine whether the target has been hijacked.
Part III
Subject.prototype.attach = function(key, observer) {
if (typeof key === 'undefined' || !observer) {
return;
}
if (!this._obsMap) {
this._obsMap = {};
}
if (!this._obsMap[key]) {
this._obsMap[key] = [];
}
const observers = this._obsMap[key];
if (observers.indexOf(observer) < 0) {
observers.push(observer);
return function() {
observers.splice(observers.indexOf(observer), 1);
};
}
};
This method is familiar, yes, as called in
Observer.prototype.subscribe
above. T
he role is that an observer uses to subscribe to a topic.
This approach is "How the topic is subscribed".
Observers maintain a hash table
_obsMap
for this topic. T
he key of the hash table is the key that needs to be subscribed to. F
or example, one observer subscribes to changes
name
property, while another observer subscribes to changes in the
age
property.
And property changes can be subscribed by multiple observers at the same time, so the value stored by the hash table is an array, and each element of the data is an observer.
Part IV
Subject.prototype.notify = function(key) {
if (
typeof key === 'undefined' ||
!this._obsMap ||
!this._obsMap[key]
) {
return;
}
this._obsMap[key].forEach(observer => observer.update());
};
When a property changes, notify the observer who subscribes to the property. T
raverse each observer and call the observer's
update
method.
As we mentioned above, dirty checks are done within this method.
Part V
Subject.prototype.setParent = function(parent, key) {
this._parent = parent;
this._key = key;
};
Subject.prototype.notifyParent = function() {
this._parent && this._parent.notify(this._key);
};
This part is used to deal with the problem of nested property.
It's something like this:
{ user: { name: 'JJC' } }
Part 6
function hijack(target, key, cache) {
const subject = target[SYMBOL_OBSERVABLE];
Object.defineProperty(target, key, {
enumerable: true,
get() {
const observer = ObserverStack.top();
if (observer) {
observer.subscribe(subject, key);
}
const subSubject = Subject.of(cache);
if (Subject.is(subSubject)) {
subSubject.setParent(subject, key);
}
return cache;
},
set(value) {
cache = value;
subject.notify(key);
}
});
}
This section shows how to use
Object.defineProperty
for property hijacking. W
hen you set a property, set (value) is called, a new value is set, and then the notify method of subject is called. T
here is no check here, and calls as soon as the property is set, even if the property has the same new value as the old value.
Notify informs all observers.
Part 7 Hijack array method.
const ObservedMethods = {
PUSH: 'push',
POP: 'pop',
UNSHIFT: 'unshift',
SHIFT: 'shift',
SPLICE: 'splice',
REVERSE: 'reverse'
};
const OBSERVED_METHODS = Object.keys(ObservedMethods).map(
key => ObservedMethods[key]
);
ObservedMethods
the array functions that need to be hijacked.
The first capital is used to do key, and the lowercase is the method that needs to be hijacked.
function hijackArray(target) {
OBSERVED_METHODS.forEach(key => {
const originalMethod = target[key];
defineProp(target, key, function() {
const args = Array.prototype.slice.call(arguments);
originalMethod.apply(this, args);
let inserted;
if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) {
inserted = args;
} else if (ObservedMethods.SPLICE) {
inserted = args.slice(2);
}
if (inserted && inserted.length) {
inserted.forEach(Subject.of);
}
const subject = target[SYMBOL_OBSERVABLE];
if (subject) {
subject.notifyParent();
}
});
});
}
The hijacking of arrays is different from objects, and
Object.defineProperty
cannot be used.
We need to hijack 6 array methods. These are head addition, head deletion, tail addition, tail deletion, replacement/deletion of a few items, array reversal.
The hijacking of the array is achieved by rewriting the array method. B
ut here's a note that every element of the data has been observed, but when new elements are added to the array, they haven't been observed.
Therefore, the code also needs to determine if the current method is
push
unshift
splice
then the new elements need to be placed in the observer queue.
The above is
W3Cschool编程狮
about
Hongmun's Javascript framework line-by-line source code interpretation
of the relevant introduction, I hope to help you.