I believe that after reading this article, you and I have the same idea.
The Observer Pattern is used when there is a one-to-many relationship between objects. For example, when an object is modified, dependent objects are automatically notified. The observer model belongs to the behavioral model.
In order to implement the data changes affect the view, VUE uses the observer mode to associate the data with the page rendering. Dependencies are collected through the DEP, and when data changes, the corresponding Watcher is notified to update the view.
How does that work?
1. Create render Watcher and initialize it
We’ve wrapped the ability to update views in a Watcher
export function lifecycleMixin() {
Vue.prototype._update = function (vnode) {}}export function mountComponent(vm, el) {
vm.$el = el;
let updateComponent = () = > {
// Render the virtual node to the page
vm._update(vm._render());
}
new Watcher(vm, updateComponent, () = > {}, true);
}
Copy the code
class Watcher {
// vm,updateComponent,()=>{console.log(' updated view ')},true
constructor(vm,exprOrFn,cb,options){
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.id = id++;
// By default, exprOrFn should execute exprOrFn. What does exprOrFn do? Render (go to vm for value)
this.getter = exprOrFn;
this.deps = [];
this.depsId = new Set(a);this.get(); // Default initialization takes a value
}
get(){ // The getter method can be called again later when the user updates
// defineproperty.get, each property can collect its own watcher
// I want one property to correspond to multiple Watcher, and one watcher to correspond to multiple properties
pushTarget(this); // Dep.target = watcher
this.getter(); // the render() method goes to the vm and values vm._update(vm._render).
popTarget(); // Dep.target = null; If dep. target has a value, this variable is used in the template
}
update(){ // Update operations in VUE are asynchronous
// Every time you update this
queueWatcher(this); // I want to cache watcher and update it later
}
run(){ // Follow up with other functions
this.get();
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this)}}}Copy the code
2. Store watcher at render time
Call deP’s pushTarget method to store watcher
class Watcher{
// ...
get(){
pushTarget(this);Dep. Target = watcher Stores the current watcher
this.getter(); popTarget(); }}Copy the code
let id = 0;
class Dep{
constructor(){
this.id = id++; }}let stack = [];
export function pushTarget(watcher){
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length-1];
}
export default Dep;
Copy the code
3. Dependency collection of objects
- Properties that are used for page rendering in vUE, need to be collected by dependency collection, collection of object rendering
watcher
- When I value, I add one to each of the attributes
dep
Property to store the renderwatcher
(The same Watcher can have multiple DEPs).watcher
的get
The method callrender
.render
Way to govm
Value up, value is calleddefineProperty
的get
Methods.
dep.depend()
= > notificationdep
storewatcher
= >Dep.target.addDep()
= > notificationwatcher
storedep
Realize bidirectional storage.
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){ // If the value is watcher
dep.depend(); // Let Watcher save dep, and let deP save watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // Notify render Watcher to update}});Copy the code
Dep implementation
Each DEP has an ID unique identifier, and adding a DEP to watcher can be de-weighted by id. The deP is used to collect relationships, and each attribute is assigned a DEP, which can hold the Watcher.
let id = 0;
class Dep{ // Each attribute is assigned a deP. The deP can store the watcher, and the watcher also stores the DEP
constructor(){
this.id = id++;
this.subs = []; // Use it to store watcher
}
depend(){
// dep. target = Dep; // dep. target = Dep
if(Dep.target){
Dep.target.addDep(this); }}addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher= >watcher.update());
}
}
Dep.target = null; / / a
export function pushTarget(watcher) {
Dep.target = watcher;
}
export function popTarget(){
Dep.target = null
}
export default Dep
Copy the code
Watcher implementation
Store dePs in watcher (one DEP for each attribute, multiple dePs for each attribute)
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
let id = 0;
class Watcher {
// vm,updateComponent,()=>{console.log(' updated view ')},true
constructor(vm,exprOrFn,cb,options){
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.id = id++;
// By default, exprOrFn should execute exprOrFn. What does exprOrFn do? Render (go to vm for value)
this.getter = exprOrFn;
this.deps = [];
this.depsId = new Set(a);this.get(); // Default initialization takes a value
}
get(){ // The getter method can be called again later when the user updates
// defineproperty.get, each property can collect its own watcher
// I want one property to correspond to multiple Watcher, and one watcher to correspond to multiple properties
pushTarget(this); // Dep.target = watcher
this.getter(); // the render() method goes to the vm and values vm._update(vm._render).
popTarget(); // Dep.target = null; If dep. target has a value, the variable is used in the template. For example, IF I'm outside of vue, the vm. XXX value does not need to collect watcher
}
update(){ // Update operations in VUE are asynchronous
// Every time you update this
queueWatcher(this); // I want to cache watcher and update it later
}
run(){ // Follow up with other functions
this.get();
}
addDep(dep){
let id = dep.id;
// Watcher did some deweighting when adding dep
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this)}}}Copy the code
4. Dependency collection of arrays
- To an array of
__ob__
Add adep
For storagewatcher
- Called when the array variation method is called
dep.notify()
To notifywatcher
update - If it is an array nested array, pass
dependArray()
Recursive collectionwatcher
this.dep = new Dep(); // Designed specifically for arrays
if (Array.isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
function defineReactive(data, key, value) {
let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend(); // Collect array dependencies}}return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify();
}
})
}
arrayMethods[method] = function (. args) {
// ...
ob.dep.notify()
return result;
}
Copy the code
Recursively collect array dependencies
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend(); // Collect array dependencies
if(Array.isArray(value)){ // If the inside is still an array
dependArray(value);// Keep collecting dependencies}}}function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i];
current.__ob__ && current.__ob__.dep.depend();
if (Array.isArray(current)) {
dependArray(current)
}
}
}
Copy the code
Vue asynchronous update nextTick
It is obviously not reasonable to update the view every time the data is changed. So vue view updates are asynchronous, call update several times, first cache watcher in queue, deduplicate by ID, then update together.
1. Implement the queue mechanism
update(){
queueWatcher(this);
}
Copy the code
import { nextTick } from ".. /utils";
let queue = [];
let has = {}; // Do the list maintenance to store the watcher
function flushSchedulerQueue(){
for(let i =0 ; i < queue.length; i++){
queue[i].run(); // vm.name = 123?
}
queue = [];
has = {};
pending = false;
}
let pending = false;
// Wait for synchronous code to complete before executing asynchronous logic
export function queueWatcher(watcher) {
// I want to update the page as soon as possible before clearing the macro task
const id = watcher.id;
if (has[id] == null) {
queue.push(watcher);
has[id] = true;
// Start batch update operation (anti-shake)
if(! pending){ nextTick(flushSchedulerQueue,0);
pending = true; }}}Copy the code
2. Implementation principle of nextTick
const callbacks = [];
function flushCallbacks() {
callbacks.forEach((cb) = > cb());
waiting = false;
}
//nextTick is asynchronous
// Vue2 takes compatibility into account
//1. Support promise, promise.then
MutationObserver flushCallbacks mutationObserver flushCallbacks mutationObserver flushCallbacks
//3.setImmediate
//4.setTimeout
Vue3 no longer considers compatibility issues
let timerFn = () = > {};
if (Promise) {
timerFn = () = > {
Promise.resolve().then(flushCallbacks);
};
} else if (MutationObserver) {
let textNode = document.createTextNode(1);
let observe = new MutationObserver(flushCallbacks);
observe.observe(textNode, {
characterData: true}); timerFn =() = > {
textNode.textContent = 3;
};
} else if (setImmediate) {
timerFn = () = > {
setImmediate(flushCallbacks);
};
} else {
timerFn = () = > {
setTimeout(flushCallbacks);
};
}
let waiting = false;
export function nextTick(cb) {
callbacks.push(cb);
if(! waiting) { timerFn(flushCallbacks,0);
waiting = false; }}Copy the code
summary
At this point, we have implemented the dependency collection principle.
In reactive data, arrays and objects are treated in two different ways. So are the collections that now depend on.
Does it bother you?