vue的响应式原理

2,858 阅读9分钟

MVVC模式

  • 目的:职责划分、分层 ( 将 Model 层、View 层进行分类 ) 借鉴后端 MVC 思想。对于前端而言就是如何将数据同步到页面上。
  • 首先看看 MVC 模式
    • mvc 就是model view controller,期中model对应数据层,view对应视图层,controller对应控制器,

mvc.png

  • 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中重写了可以改变愿数组的七种方法pushshiftunshiftsortreversepopsplice,当调用这些方法的时候,再触发视图更新。
  • 当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的依赖收集

依赖收集的核心是两个类,DepWatcher,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,刷新页面的Watcherwatchapi的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可以实现懒代理,不必监听深层次对象,衍生出shallowReadonlyshallowReadonly,等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();
        }
    });
}