MVVC模式
- 目的:职责划分、分层 ( 将 Model 层、View 层进行分类 ) 借鉴后端 MVC 思想。对于前端而言就是如何将数据同步到页面上。
- 首先看看 MVC 模式
- mvc 就是model view controller,期中model对应数据层,view对应视图层,controller对应控制器,
- MVVM 分别是 model viewModel view 目的是为了实现分层的。
- 借助了后台的分层思想来实现代码的划分。
- 前端借助了后端 mvc 但是发现所有的逻辑都放在 controller 这一层,逻辑非常臃肿。难以维护。 隐藏 controller 这一层,MVVM 模式就是可以直接将数据映射到视图上,同样可以自动监控视图的变化,视图变化后可以更新数据 。vue里面提供了一个指令可以实现双向绑定 v-model
vue2以及vue3的数据劫持
vue2的数据代理劫持
在vue中,响应式的原理就是对数据进行了劫持,当对数据进行取值操作的时候,就会对这个数据进行依赖收集,当这个数据发生改变的时候,就会触发视图更新,刷新页面。
- 在vue2中,使用的数据劫持方法是
Object.defineProperty这个api,在get中进行depend依赖收集,在set中进行notify通知依赖的watcher去重新渲染(视图更新) - 通过observe一个对象进行数据劫持
class Observe {
constructor(data) {
// 给已经代理过的对象添加一个__ob__属性,并且这个属性不可枚举,值指向这个实例
Object.defineProperty(data, '__ob__', {
enumerable: false,
value: this
})
// 如果代理目标是数组,调用处理数组的方法observeArray
if (Array.isArray(data)) {
this.observeArray(data);
} else {
this.walk(data);
}
}
observeArray(array) {
// 改变array的原型链
array.__proto__ = proxyPrototype;
// 数组元素可能是对象,也需要进行代理
array.forEach(item => {
observe(item);
});
}
// 对普通对象进行代理
walk(data) {
// 代理对象的每一个属性
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
}
// 数据代理
function defineReactive(data, key, value) {
const dep = new Dep();
// 递归代理
let childOb = observe(value);
Object.defineProperty(data, key, {
get() {
// 在这里进行数据劫持逻辑,详细请看后面的讲解
return value;
},
set(newValue) {
// 如果新值和旧值相等
if (value === newValue) return;
// 新的值可能是对象,对新的值进行递归代理
childOb = observe(newValue);
value = newValue;
// 通知依赖的watcher去重新渲染(更新视图)
dep.notify();
}
})
}
function observe(data) {
if (typeof data !== 'object' || data == null) return;
// 如果已经代理过了,就不要再代理了
if (data.__ob__) {
return data;
}
// 对data进行代理
return new Observe(data);
}
- 由于数组可能是由接口返回的数据,元素数量可能很多,全部深层次递归使用
Object.defineProperty劫持成本很大,而且我们通过索引的方式修改数组的场景由不多,所以vue2中重写了可以改变愿数组的七种方法push,shift,unshift,sort,reverse,pop,splice,当调用这些方法的时候,再触发视图更新。 - 当observe数组的时候,会改变这个数组的原型链,然后循环数组处理可能是对象的元素
// 保存旧的原型链指向
const oldArrayPrototype = Array.prototype;
// 创建一个代理原型链
const proxyPrototype = Object.create(oldArrayPrototype);
// 遍历数组的七种会改变愿数组的方法
['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach(method => {
proxyPrototype[method] = function (...args) {
let ob = this.__ob__;
// 记录新增的元素,后续
let insert;
switch (method) {
case 'push':
case 'unshift':
insert = args;
break;
case 'splice':
insert = args.slice(2);
break;
default:
break;
}
// 对新增加的属性进行代理观测
insert && ob.observeArray(insert);
// 原生方法执行结果
let result = oldArrayPrototype[method].call(this, ...args);
return result;
}
})
export default proxyPrototype;
observeArray(array) {
// proxyPrototype为上面的
array.__proto__ = proxyPrototype;
array.forEach(item => {
observe(item);
});
}
vue2的依赖收集
依赖收集的核心是两个类,Dep和Watcher,Dep,Dep的作用是记录Watche,Watcher中也记录着Dep,两者是双向记忆的,Watcher的作用是记录更新视图的函数
Dep
- Dep的作用就是利用闭包,记录属性和对象收集到的Watcher
function defineReactive(data,key,value){ // 闭包
// 创建一个dep实例
let dep = new Dep()
let childOb = observe(value);
Object.defineProperty(data,key,{
get(){
// 如果Dep.target有值,说明值是一个watcher(在Watcher类中添加上来的)
if(Dep.target){
// 依赖收集 记录当前属性对应的watcher
dep.depend()
}
return value;
},
set(newValue){ // vm.xxx = {a:1} 赋值一个对象的话 也可以实现响应式数据
if(newValue === value) return
childOb = observe(newValue)
value = newValue;
dep.notify(); // 通知依赖的watcher去重新渲染
}
})
}
let did = 0;
// 作用是收集watcher
class Dep{
constructor(){
this.id = did++;
this.watchers = []
}
// watcher 和 dep是一个多对多的关系
depend(){
// 调用Watcher的addDep方法(Dep.target为当前的Watcher)
Dep.target.addDep(this); // 让watcher去记录dep
}
// 在Watcher中调用的这个方法,将当前Watcher添加到watchers里面
addWatcher(watcher){
this.watchers.push(watcher)
}
// 属性更新的时候调用这个方法,通知所有的Watcher调用update方法更新
notify(){
this.watchers.forEach(watcher=>watcher.update());
}
}
Dep.target = null;
Watcher
Watcher有三种,computed中用到的Watcher,刷新页面的Watcher,watchapi的Watcher。
- 首先是更新页面的
Watcher,在执行更新函数的时候,vue会将更新函数交给Watcher处理,放到一个更新队列中,当依赖收集到的属性变化时,就会触发这个属性的dep所收集到的所有Watcher执行更新方法,为了减少操作dom,这个执行方式是异步批处理的。 - vue中的nextTick也是放到了这个队列中依次执行,所以
nextTick中可以获取到队列前面执行后的最新状态。
let wid = 0;
class Watcher{
constructor(vm,fn,cb,options){
this.vm = vm;
this.fn = fn;
this.cn = cb;
this.options = options
// 储存dep实例
this.deps = [];
this.depsId = new Set()
this.id = wid++
this.get(); // 实现页面的渲染
}
// new Watcher后就会执行
get(){
// 将当前的Watcher放到Dep.target上,再次取值操作的时候Watcher就会被dep收集了
Dep.target = this
// 调用更新方法,这个过程会编译模板,会有取值操作进入Object.defineProperty的get方法中。
// 然后就会创建一个dep实例,执行dep.depend(),收集当前Watcher
this.fn();
// 只有在渲染的时候才有Dep.target属性
Dep.target = null;
}
// 添加依赖当前Watcher记录dep
addDep(dep){
let id = dep.id;
// 去重操作
if(!this.depsId.has(id)){
this.deps.push(dep)
this.depsId.add(id)
dep.addWatcher(this)
}
}
// 加入异步更新队列
update(){
queueWatcher(this);
}
run(){
this.get();
}
}
- queueWatcher
let queue = [];
let has = {};
let pending = false;
// 执行任务队列
function flushSchedularQueue(){
queue.forEach(watcher=>watcher.run())
queue = [];
has = {};
pending = false
}
export function queueWatcher(watcher){
let id = watcher.id;
if(has[id] == null){
queue.push(watcher)
has[id] = true;
// 批处理,只开启一个定时器,之后的同步操作都会将回调放到queue中,一起执行就可以了
if(!pending){
// 异步执行(nextTick是内部实现的异步方法,内部加了兼容性降级处理promise、MutationObserver、setImmediate、setTimeout)
// 将用户的nextTick和渲染的nextTick回调放在一起处理,不需要再开多个定时器了
nextTick(() => {
flushSchedularQueue();
});
pending = true;
}
}
}
vue3的数据代理劫持
Object.defineProperty这个api存在着一些问题,比如必须要深层次递归监听一个对象内所有的属性,性能并不太好,并且不能监听数组的length改变
- 而vue3使用的proxy对比Object.defineProperty就会有很多优势,proxy监听的是对象本身,而非属性,所以就不需要考虑数组的性能问题,直接可以监听数组的变化,包括数组长度变化,并且proxy可以实现懒代理,不必监听深层次对象,衍生出
shallowReadonly,shallowReadonly,等api。 - 对于数组需要做一些特殊的处理,比如调用
push方法的时候,length属性也会变化,就会触发两次set,给数组增加一个元素,修改长度,因为修改长度,再次触发,需要屏蔽掉length属性的依赖收集。
vue3的依赖收集
- vue3中effect方法充当了Watcher的角色,用户的更新方法会交给effect处理
/**
* effectStack栈,用于嵌套的effect,下面这种情况
* 获取name的effect对应是外层effect,获取age对应的是内层effect
* 获取like对应的又应该是外层的effect,可以使用栈结构解决
* effect(()=>{
* console.log(proxy.name)
* effect(()=>{
* console.log(proxy.age);
* })
* console.log(proxy.like)
* })
*/
const effectStack: any[] = [];
// 执行当前回调函数fn,收集依赖过程中的effect
let activeEffect;
let id = 0;
// 构建effect
function createReactiveEffect(fn, options) {
const effect = function () {
// 由于返回fn后还需要进行弹栈和更改activeEffect操作,所以使用try-finally,因为finally一定会执行
try {
effectStack.push(effect);
activeEffect = effect;
// 执行fn,进行依赖收集,对应的effect对应当前的activeEffect
return fn();
} finally {
// 弹栈
effectStack.pop();
// activeEffect执行栈顶的effect
activeEffect = effectStack[effectStack.length - 1];
}
}
effect.id = id++;
effect._isEffect = true;
effect.options = options;
effect.deps = [];
// 返回这个effect
return effect;
}
export function effect(fn, options: any = {}) {
// 创建一个effect
const effect = createReactiveEffect(fn, options);
// 如果不是惰性的effect,立即执行一次
if (!options.lazy) {
effect();
}
// 返回effect
return effect;
}
/*
创建一个WeakMap进行依赖关系
WeakMap{
{name:'zf',age:12}:{
age:new Set(effect),
name:new Set(effect),
},
}
*/
const targetMap = new WeakMap();
// 依赖收集函数(构建依赖关系)
export function track(target, type, key) {
if (!activeEffect) return;// 用户只是取值,并且取得值不在effect中
// 过去目标对象的依赖Map
let depsMap = targetMap.get(target);
// 如果目标对象没有依赖Map,则创建一个,并将新创建的Map赋值给depsMap
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
// 根据当前的depsMap获取key对应的dep依赖effec集合
let dep = depsMap.get(key);
// 如果没有对应的Set集合
if (!dep) {
// 为depsMap创建一个key,Set映射,并将新创建的Set赋值给当前dep
depsMap.set(key, dep = new Set())
}
// 如果当前dep Set集合没有当前的activeEffect,将当前的activeEffect添加到dep中
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
}
}
// 触发更新
export function trigger(target, type, key, newValue?, oldValue?) {
// 去映射表找到对应target的depsMap
const depsMap = targetMap.get(target);
// 改了属性,这个属性没有在effect中使用
if (!depsMap) return;
const effectsSet = new Set();
const add = (effects) => {
if (effects) {
effects.forEach(effect => effectsSet.add(effect));
}
}
// 1.如果更改的数组长度 小于依赖收集的长度 要触发重新渲染
// 2.如果调用了push方法 或者其他新增数组的方法(必须能改变长度的方法), 也要触发更新
// 特殊处理 ,因为length作为key并没有被依赖收集,需要手动触发
if (key === "length" && Array.isArray(target)) {
depsMap.forEach((dep, k) => {
if (k > newValue || k === "length") {
add(dep);// 更改后的数组长度,比收集到的数组长度小
}
})
} else {
// 从depsMap中取出当前key对应的set集合,添加到effectsSet中
add(depsMap.get(key));
switch (type) {
case "add":
if (Array.isArray(target) && isIntegerKey(key)) {
// 增加属性,length变化,因为前面对length改变进行了屏蔽,所以需要触发length的依赖收集触发
add(depsMap.get("length"));
}
}
}
// 执行所有需要执行的effect函数,重新收集依赖
effectsSet.forEach((effect: any) => {
if (effect.options.schedular) {
// 自己实现逻辑
effect.options.schedular(effect());
} else {
effect();
}
});
}