vue mvvm 数据劫持

409 阅读5分钟

序文:
vue-next已经出新版,vue3.0和vue2.0对于mvvm的实现是有什么区别呢?我们往下看

vue2.0时代
2016年5月份推出了Vue2.0时代
而es6在2015年6月已经推出

当前推出的proxy可以实现对对象的属性监听,但是由于大部分浏览器还不支持proxy
我也想用啊,可是实力不允许啊...

所以vue2.0还是使用了es5的Object.defineProperty去处理
首先,我们来看下Object.defineProperty 是如何去实现的

/**
 * 实现双向绑定的原理
 * 数据劫持
 */
function observe(data){
  if(typeof data !== 'object' || data === null) {
    return data;
  }

  // 遍历的所有的数据,监听数据属性变化
  for(let key in data) {
    defineReactive(data, key, data[key]);
  }
}

/**
 * 监听数据的变化 原谅我这边用了3.0的名字,可以拿这个和3.0做个对比
 */
function defineReactive(data, key, value){
  observe(value); // 内部元素也需要数据劫持
  Object.defineProperty(data, key, {
    get(){
      return value;
    },
    set(newVal){
      updateView();
      value = newVal;
    },
  })
}

function updateView(){
  console.log('视图发生更新');
}

Object.defineProperty可以监听对象数据变化哎,好开心,我的mvvm框架已经实现了
试了下数组,GG了,数组怎么没监听呢,呜呜呜~
数组该怎么玩呢???嗯嗯嗯~数组基本上都是通过操作方法来变更的话,我可以从原型入手啊
那我们再来实现下vue2.0里面如何对数组做操作的呢

//监听的数组对象方法
const OAM = ['push','unshift','shift','splice','reverse','pop','sort']

/**
 * 实现双向绑定的原理
 * 数据劫持
 */
function observe(data){
  if(typeof data !== 'object' || data === null) {
    return data;
  }
  // 如果是数组,则重写数组方法,并重新指向原型
  if(Array.isArray(data)) {
    overrideArrayProto(data);
  }

  // 遍历的所有的数据,监听数据属性变化
  for(let key in data) {
    defineReactive(data, key, data[key]);
  }
}

/**
 * 监听数据的变化
 */
function defineReactive(data, key, value){
  observe(value); // 内部元素也需要数据劫持
  Object.defineProperty(data, key, {
    get(){
      return value;
    },
    set(newVal){
      updateView();
      value = newVal;
    },
  })
}

/**
 * 数组方法劫持
 * @param {*} data 
 */
function overrideArrayProto(data){
  const originalProto = Array.prototype; // 数组的原型
  const newProto = Object.create(originalProto); // 根据原来的数组原型重写原型方法
  OAM.forEach(method => {
    newProto[method] = function(){
      let inserted;
      updateView();
      let args = [].slice.call(arguments);
      originalProto[method].apply(this, args);
      switch(method) {
        case 'push':
        case 'unshift':
          inserted = args.slice(0);
          break;  
        case 'splice':
          inserted = args.slice(2);
          break;  
        default:
          break;  
      }
      // 如果是新增的数据,也需要进行监听
      inserted && observe(inserted);
    }
  })
  data.__proto__ = newProto; // 将原型指向到重写过的新的原型上
}

function updateView(){
  console.log('视图发生更新');
}

这是2.0的实现,那么3.0呢,是不是废铁变青铜呢?

vue3.0时代
proxy都出来这么久了,浏览器该要都支持了吧,好,我要换装备了,新装备给换上

// 判断是否是object对象
function isObject(value) {
  return typeof value === 'object' && value !== null;
}

// 判断当前是否包含当前属性
function hasOwn(target, key){
  return target.hasOwnProperty(key);
}

let toProxy = new WeakMap(); // 弱引用映射表 es6  放置的是 原对象:代理过的对象
let toRaw  = new WeakMap(); // 被代理过的对象:原对象  es6语法
/**
 * 数据监听劫持数据
 * @param {*} data 
 */
function observe(data){
  if(typeof data !== 'object' || data === null) {
    return data;
  }
  let proxy = toProxy.get(data); // 如果已经代理过了 就将代理过的结果返回即可
  if(proxy) {
    return proxy;
  }
  // 如果将代理后的数据继续代理则返回当前的代理对象
  if(toRaw.has(data)) {
    return data;
  }
  // proxy handler方法实现
  let handler = {
    get(target, key, receiver) {
      // 取值有必要的时候再递归
      if(isObject(target[key])) {
        return observe(target[key]);
      }
      let res = Reflect.get(target, key, receiver);
      return res;
    },
    set(target, key, value, receiver) {
      let hasOwnKey = hasOwn(target, key);
      let oldValue = target[key];
      let res = Reflect.set(target, key, value, receiver);
      return res;
    },
    // 删除属性
    deleteProperty(target, key){
      let res = Reflect.deleteProperty(target, key);
      return res;
    }
  }
  let observed = new Proxy(data, handler);
  toProxy.set(data, observed);
  toRaw.set(observed, data);
  return observed;
}

黄金盔甲已换上,从此高大上

上面只是数据拦截,那么不是还有数据变化了,关联变更么?
那就是依赖收集,数据变动了依赖触发

// 判断是否是object对象
function isObject(value) {
  return typeof value === 'object' && value !== null;
}

// 判断当前是否包含当前属性
function hasOwn(target, key){
  return target.hasOwnProperty(key);
}

let effectStack = []; // 副作用堆栈
const targetMap = new WeakMap();
let toProxy = new WeakMap(); // 弱引用映射表 es6  放置的是 原对象:代理过的对象
let toRaw  = new WeakMap(); // 被代理过的对象:原对象  es6语法
/**
 * 数据监听劫持数据
 * @param {*} data 
 */
function observe(data){
  if(typeof data !== 'object' || data === null) {
    return data;
  }
  let proxy = toProxy.get(data); // 如果已经代理过了 就将代理过的结果返回即可
  if(proxy) {
    return proxy;
  }
  // 如果将代理后的数据继续代理则返回当前的代理对象
  if(toRaw.has(data)) {
    return data;
  }
  // proxy handler方法实现
  let handler = {
    get(target, key, receiver) {
      // 取值有必要的时候再递归
      if(isObject(target[key])) {
        return observe(target[key]);
      }
      let res = Reflect.get(target, key, receiver);
      // 收集依赖
      track(target,key);
      return res;
    },
    set(target, key, value, receiver) {
      updateView();
      console.log(key,value);
      let hasOwnKey = hasOwn(target, key);
      let oldValue = target[key];
      let res = Reflect.set(target, key, value, receiver);
      if(!hasOwnKey) {
        console.log('新增数据');
        trigger(target,'add',key);
      } else if(oldValue !== value) {
        console.log('数据更新');
        // 触发依赖
        trigger(target,'set',key);
      }
      return res;
    },
    // 删除属性
    deleteProperty(target, key){
      console.log(`删除属性:[key:${key}, value:${target[key]}]`)
      let res = Reflect.deleteProperty(target, key);
      return res;
    }
  }
  let observed = new Proxy(data, handler);
  toProxy.set(data, observed);
  toRaw.set(observed, data);
  return observed;
}

// 响应式副作用
function effect(fn) {
  let effectFn = reactiveEffect(fn); // 实现双向绑定方法
  // 立即执行一次
  effectFn();
}

// 响应式副作用
function reactiveEffect(fn){
  let effect = function(){
    run(effect,fn);
  };
  return effect;
}

/**
 * 执行方法
 * effect: 副作用
 * fn 执行方法
 */
function run(effect,fn) {
  try {
    effectStack.push(effect);
    fn(); // js 线程是单线程的
  } finally {
    effectStack.pop();
  }
}

/**
 * 收集依赖
 * 数据格式如下:
 * {
 *    target: {
 *      key: set[fn]
 *    }
 * }
 */
function track(target,key) {
  // 获取最新的一条副作用
  let effect = effectStack[effectStack.length-1];
  // 如果副作用存在,则需要保存进去
  if(effect) {
    let depTargetMap = targetMap.get(target);
    // 先判断下依赖里面的target是否有,没有则初始化new Map()
    if(!depTargetMap) {
      targetMap.set(target, depTargetMap = new Map())
    }
    let depKeySet = depTargetMap.get(key);
    // 先判断下依赖里面的target下面的key对象是否存在,不存在则创建 new Set()
    if(!depKeySet) {
      depTargetMap.set(key, depKeySet = new Set());
    }
    // 判断当前依赖里面是否有当前副作用, 不存在则add进去
    if(!depKeySet.has(effect)) {
      depKeySet.add(effect);
    }
  }
}

/**
 * 触发副作用
 */
function trigger(target,method,key){
  let depTargetMap = targetMap.get(target);
  if(!depTargetMap) {
    return;
  }
  // 依次遍历副作用依赖方法
  let deps = depTargetMap.get(key);
  if(deps) {
      deps.forEach(fn=>{
        fn();
      })
  }
}

// 数据来执行一把看看
let dataSource = {a:1,b:2};
let data = observe(dataSource);
effect(()=>{ // effect 会执行两次 ,默认先执行一次 之后依赖的数据变化了 会再次执行
  console.log(`数据变更:${data.b}`);
})

总结:

vue3.0相对于vue2.0

  • 2.0 默认会递归
  • 数组改变length 是无效的
  • 对象不存在的属性不能被拦截

Vue3.0的使用

const { value, computed, watch, onMounted, reactive } = Vue;
function usePosition(){
  let position = Vue.reactive({
    x:0,y:0
  })
  function updatePosition(e){
    position.x = e.x;
    position.y = e.y;
    // console.log(`x:${position.x},y:${position.y}`)
  }
  Vue.onMounted(()=>{
    window.addEventListener('mousemove',updatePosition);
  })
  Vue.onUnmounted(()=>{
    window.removeEventListener('mousemove',updatePosition);
  })
  return position;
}
let app = {
  setup(){
    let state = Vue.reactive({name:'wp'});
    let count = reactive({
      value: 0,
    });
    const plusOne = computed(()=>count.value+1);
    const increment = ()=>{ count.value++ };
    watch(()=>count.value*2,val=>{
      console.log('count * 2 is', val);
    });
    let position = usePosition();
    function changeState(){
      state.name = 'zhangsan';
    }
    Vue.effect(()=>{
      console.log(`数据改变:${state.name}`)
    })
    return { // state数据,方法,computed计算属性 都可以在这里返回
        state,
        changeState,
        position,
        count,
        plusOne,
        increment
      }
    },
    template: `<div @click="changeState">{{state.name}}</div>
    <div>-count-{{ count.value }}-</div>
    <div>-plusOne-{{ plusOne }}-</div>
    <button @click="increment">新增</button>
    `
  }
Vue.createApp().mount(app,container);