vue,angularjs,react
ps:angularjs与angular区别很大,这里指的angularjs
作为前端三大框架,经常用来对比,我来谈谈我的看法 。
对于使用者来说,我们都是不用操作dom,利用数据变化来更新视图,但是他们是有本质的区别的。
- 数据驱动: react数据变化需要调用函数setState/useState第二个函数 来更新,可以说是一种被动的视图更新,而angularjs和vue我们直接给响应式数据赋值即可。
- react : 数据变化了,被动触发视图更新后,它会从根节点,深度遍历所有节点找出变化的dom。当然react做了很多优化,这个遍历比较是可以中断的,避免影响浏览器的渲染,造成长时间的白屏。找到变化的dom通过链接连接起来,然后进行dom的更新。
- vue :vue的每个响应式的数据都有一个dep订阅中心,每个订阅者watcher订阅它,当数据变化,它会通知订阅的watcher去做视图的更新,这里借用了object.defineproperty方法,让数据在get的时候进行watcher收集,set的时候派发更新。vue的视图更新是以组件的维度,也就是父组件更新可以不会触发子组件的更新。
- angularjs : 每个响应式数据对应一个watcher对象,它结构如下,watchFn返回需要观测的值,listenerFn返回数据变化需要执行的动作。angularjs重写了浏览器所有会引起数据变化的方法,执行到这些方法,会触发angular对所有的watcher检测,达到数据变化驱动视图。
var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { }, valueEq: !!valueEq }
响应式原理
可以改变状应用程序状态更改由以下情况引起:
- Events - 用户事件比如
click,change,input,submit, … - XMLHttpRequests - 比如从远程服务器获取数据
- Timers -
setTimeout(),setInterval()
这里要提到zone.js:zone.js采用猴子补丁(Monkey-patched)的暴力方式将JavaScript中的异步任务都包裹了一层,使得这些异步任务都将运行在zone的上下文中。 并且zone.js对JavaScript中的大多数异步事件都做了包裹封装,它们包括:
- zone.alert;
- zone.prompt;
- zone.requestAnimationFrame、zone.webkitRequestAnimationFrame、zone.mozRequestAnimationFrame;
- zone.addEventListener;
- zone.addEventListener、zone.removeEventListener;
- zone.setTimeout、zone.clearTimeout、zone.setImmediate;
- zone.setInterval、zone.clearInterval
以及对promise、geolocation定位信息、websocket等也进行了包裹封装,你可以在这里找到它们github.com/angular/zon…。
更多用法见: github.com/angular/zon…
当我们执行这些浏览器api时会被劫持,触发下面的$digest()方法。
视图更新
function Scope() {
this.$$watchers = [];
this.$$asyncQueue = [];
this.$$postDigestQueue = [];
this.$$phase = null;
}
Scope.prototype.$beginPhase = function(phase) {
if (this.$$phase) {
throw this.$$phase + ' already in progress.';
}
this.$$phase = phase;
};
Scope.prototype.$clearPhase = function() {
this.$$phase = null;
};
Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
var self = this;
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn || function() { },
valueEq: !!valueEq
};
self.$$watchers.push(watcher);
return function() {
var index = self.$$watchers.indexOf(watcher);
if (index >= 0) {
self.$$watchers.splice(index, 1);
}
};
};
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
if (valueEq) {
return _.isEqual(newValue, oldValue);
} else {
return newValue === oldValue ||
(typeof newValue === 'number' && typeof oldValue === 'number' &&
isNaN(newValue) && isNaN(oldValue));
}
};
//它把所有的监听器运行一次,返回一个布尔值,表示是否还有变更了
Scope.prototype.$$digestOnce = function() {
var self = this;
var dirty;
_.forEach(this.$$watchers, function(watch) {
try {
var newValue = watch.watchFn(self);
var oldValue = watch.last;
if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) {
watch.listenerFn(newValue, oldValue, self);
dirty = true;
}
watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue);
} catch (e) {
(console.error || console.log)(e);
}
});
return dirty;
};
Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
this.$beginPhase("$digest");
// 脏检查(dirty check )
do {
while (this.$$asyncQueue.length) {
try {
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
} catch (e) {
(console.error || console.log)(e);
}
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
this.$clearPhase();
throw "10 digest iterations reached";
}
} while (dirty);
this.$clearPhase();
while (this.$$postDigestQueue.length) {
try {
this.$$postDigestQueue.shift()();
} catch (e) {
(console.error || console.log)(e);
}
}
};
Scope.prototype.$eval = function(expr, locals) {
return expr(this, locals);
};
Scope.prototype.$apply = function(expr) {
try {
this.$beginPhase("$apply");
return this.$eval(expr);
} finally {
this.$clearPhase();
this.$digest();
}
};
Scope.prototype.$evalAsync = function(expr) {
var self = this;
if (!self.$$phase && !self.$$asyncQueue.length) {
setTimeout(function() {
if (self.$$asyncQueue.length) {
self.$digest();
}
}, 0);
}
self.$$asyncQueue.push({scope: self, expression: expr});
};
Scope.prototype.$$postDigest = function(fn) {
this.$$postDigestQueue.push(fn);
};
测试一下
var scope = new Scope();
scope.aValue = "abc";
scope.counter = 0;
var removeWatch = scope.$watch(
function(scope) {
return scope.aValue;
},
function(newValue, oldValue, scope) {
scope.counter++;
}
);
scope.$digest();
console.assert(scope.counter === 1);
scope.aValue = 'def';
scope.$digest();
console.assert(scope.counter === 2);
removeWatch();
scope.aValue = 'ghi';
scope.$digest();
console.assert(scope.counter === 2); // No longer incrementing
angularjs的脏检查,每次数据改变都会检查是否需要重新绑定,有很大的性能问题。 vue和angular都对此进行了重写。