vue2.0到vue3.0响应式原理

VUE2.0 的响应式原理

本篇文章篇幅较长,已经对2.0响应式原理熟悉的可直接跳过此部分,各取所需,共同交流

在vue中我们使用最多的就是响应式数据了,给我们带来了很多便利,说起响应式数据,也就是数据变了就要更新视图,我们一步一步看一下在vue2.0中是怎么实现这一功能的~
首先我们定义一个数据:

let data = {name: "yangbo"}

然后我们改变这个数据:

data.name = 'yb'

我们都知道在vue中数据改变了我们就要更新视图,所以这时我们就需要一个更新视图的方法:

function updateView(){
    console.log('触发更新视图方法了')
}

这时问题来了,当我们设置这个对象的属性值的时候,怎么去触发我们的updateView()方法呢,这里我们就用到了Object.defineProperty() 方法, 我们通过这个方法来重新定义属性并给对象的属性增加getter和setter, 话不多说开始第一步吧:

我们定义一个观察数据的方法,这个方法里我们调用defineProperty方法去为数据的属性设置getter和setter;
在defineProperty方法中,我们使用了Object.defineProperty() ,当取值的时候就会走get方法,这时我们直接将value返回即可,在重新设置值的时候(比如data.name = 'yb')就会走set方法,在set方法中我们调用更新视图的方法,并将新的值赋给value

function watchData(target) {
  if(typeof target !== 'object' || target === null) {
    return target;
  }
  for (let key in target) {
    defineProperty(target, key, target[key])
  }
}
function defineProperty(target, key, value) {
  // 重新给target 定义key 并增加getter和 setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        updateView();
        value = newVal;
      }
    }
  })
};

以上的代码就完成了第一步,这时问题又来了,我们的数据不可能全是一层的,有可能是多层的数据,比如:

let data = {
    name: 'yangbo',
    info: {
        age: '26'
    }
}

所以,我们要做的是如果内层还是一个对象,那我们就要再次去观察这个对象, 这时我们在defineProperty函数中增加一行代码,这其实就形成了递归:

function defineProperty(target, key, value) {
  // 增加一行代码
  watchData(value);
  // 重新给target 定义key 并增加getter和 setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        updateView();
        value = newVal;
      }
    }
  })
};

至此可以说我们已经为数据实现响应式了,通过以上代码可以看出,如果你的Object层级很深,那么递归是会影响性能的。接下来我们思考一个问题,假如我们使用如下方式给对象赋值会触发几次更新视图的方法:

data.info = {sex: 'man'}
data.info.sex = 'woman'

答案是一次,只有data.info = {sex: 'man'}会触发更新视图的方法,因为我们上面的方法并没有为sex定义getter与setter, 所以我们要再defineProperty中再加一行代码,如下:

function defineProperty(target, key, value) {
  watchData(value);
  // 重新给target 定义key 并增加getter和 setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        // 新增一行代码,如果设置的值是Object,则也为新的值添加getter和setter
        watchData(newVal);
        updateView();
        value = newVal;
      }
    }
  })
};

这时就会执行两次更新视图的方法了,到这里我们基本实现对象的响应式了;但再往细想还是有问题:假如属性不存在,新增的属性会是响应式的吗?假如属性的值是个数组该怎么做呢?
我们一起来看下,以目前的代码,以下这种为数据中数组的改变是不会触发
更新视图的方法的:

let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);

那我们来让这种方式改变数据也会触发更新视图,首先我们看下,我们调用了push方法,这个方法是Array的,所以我们需要对数组的方法进行重写,这里我们不能直接对数组原型上的方法进行重写,而且我们是拿到数组的所有方法并对其中的几个方法进行改写,本例仅对push和pop方法进行改写,我们需要创建一个和Array.property相同的Object,并且修改我们的Object并不会影响到Array.property, 这里就用到了继承,估计你在面试的时候会遇到这个问题。

let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty); // 继承

接着我们重新定义proto上的方法,记住,定义propo上面的方法的时候我们还需要用到ArrayProperty上的方法, 所以这里我们还是调用数组的原始方法,在调用之前我们来调用更新视图的方法,这里不要忘记使用call方法来改变this指向,这里我们做的就是函数劫持

['push', 'pop'].forEach((method) => {
  proto[method] = function() {
    updateView();
    // 调用ArrayProperty上的方法
    ArrayProperty[method].call(this, ...arguments);
  }
});

函数我们已经劫持了,那在watchData这个方法中,我们怎么让target找到proto上的方法呢? 我们判断一下target是不是数组,如果是数组那我们让它的proto指向proto:

function watchData(target) {
  if(typeof target !== 'object' || target === null) {
    return target;
  }
  if(Array.isArray(target)) {
    // 如果是数组,让target的链指向proto
    target.__proto__ = proto;
  }
  for (let key in target) {
    defineProperty(target, key, target[key])
  }
}

以上就搞定了对对象和数组的响应式,下面贴出完整代码:

let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty); // 继承
['push', 'pop'].forEach((method) => {
  proto[method] = function() {
    updateView();
    // 调用ArrayProperty上的方法
    ArrayProperty[method].call(this, ...arguments);
  }
})

function updateView(val) {
  console.log('触发更新视图方法了');
}
function watchData(target) {
  if(typeof target !== 'object' || target === null) {
    return target;
  }
  if(Array.isArray(target)) {
    // 如果是数组,让target的链指向proto
    target.__proto__ = proto;
  }
  for (let key in target) {
    defineProperty(target, key, target[key])
  }
}
function defineProperty(target, key, value) {
  watchData(value);
  // 重新给target 定义key 并增加getter和 setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        watchData(newVal);
        updateView();
        value = newVal;
      }
    }
  })
};
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);
console.log(data.phone);

// let data = {name: 'yangbo', info: {age: 25}}
// watchData(data);
// data.name = 'yb';
// data.info = {sex: 'man'}
// data.info.sex = 'woman'

VUE3.0 的响应式原理

上面我们已经手写了简易版的vue2.0响应式,我们不难发现,在2.0中一上来默认就会对数据进行递归,还有就是对象不存在的属性不能被拦截,带着这些问题我们一起看下vue3.0的响应式实现原理~

首先说下我们现阶段怎么体验vue3.0 :

  1. 将vue3.0源码clone到本地
git clone [email protected]:vuejs/vue-next.git
  1. 安装依赖,然后在根目录执行
npm run dev
  1. 编译好的vue3.0是package/vue/dist/vue.global.js,将vue.global.js引入你的demo就可以使用了,在这之前务必要看下Vue Composition API

接下来我们使用一下vue3.0




  
  
  
  Document


  

vue3.0 的响应式数据是通过es6 的Proxy来实现的,Vue.reactive()返回一个Proxy,从上面的代码可以看出,当let proxy = Vue.reactive({name: 'yangbo'});是应该触发一次更新视图的,而当变更值proxy.name = 'yb';的时候需要再触发一次更新视图,那在vue3.0中是怎么做的呢? vue3.0这里的主要逻辑是通过Vue.effect()这个方法去实现的,effect这个方法会先执行一次,当数据变化的时候会再执行,接下来我们一步一步的区实现,我们这次是用js来实现不是ts,先熟悉思路。

  • 首先我们实现Vue.reactive这个方法
    不知道你是否还记得上面Vue2.0实现响应式的方法,就是一上来就会对数据进行递归,带着这个问题看看vue3.0到底怎么优化的;在这之前还是需要熟悉ES6的Proxy的~
    Vue.reactive返回的是一个响应式对象,所以我们在reactive方法中返回一个响应式对象:
function reactive(target) {
 return createReactive(target);
}

function createReactive(target) {
 // 创建响应式对象
 if(!(typeof target === 'object' && target !== null)) {
   return target;
 }
 // 创建观察者
 let baseHandle = {
   get(target, key, receiver) {
     // 获取
     let datas = Reflect.get(target, key, receiver);
     return datas;
   },
   set(target, key, value, receiver) {
     // 设置
     let res = Reflect.set(target, key, value, receiver);
     console.log(res);
     return res;
   }
 }
 let observed = new Proxy(target, baseHandle);
 return observed;
}
let proxy = reactive({name: 'yangbo'})
proxy.name = 'yb';
console.log(proxy.name);

在vue3.0中,一般proxy会和一个api reflect来一起使用,在get方法中,Reflect.get(target, key, receiver)和target[key]这两种写法是等价的;
在我们代码的set方法中,我们同样使用Reflect.set()来设置值,并且它会返回一个布尔值来标示是否设置成功, 通过以上的代码我们已经可以成功代理一层Object,那多层的Object呢?所以我们在获取的时候,也就是get方法中判断要获取的值是不是Object, 如果Reflect的结果是Object的话,那我们直接把结果代理(递归)

get(target, key, receiver) {
      // 获取
      let datas = Reflect.get(target, key, receiver);
      //  变更了下面这行代码
      return typeof target === 'object' && target !== null ? reactive(datas) : datas;
    }

和2.0相比,2.0是开始就递归,而3.0是在取值的时候判断,有必要才去递归。以上我们实现了对Object的代理,但是我们还要防止同一个Object被多次代理,所以这里我们用到了new WeakMap()弱引用
我们来定义两个WeakMap用于判断当前对象是否被代理过:

let sourceProxy = new WeakMap(); // 存放原对象和代理过的对象
/**
 * sourceProxy
 * {
 *    原对象:代理过的对象
 * }
 * **/
let toRaw = new WeakMap(); // 存放被代理过的对象和原对象
/**
 * toRaw
 * {
 *    代理过的对象:原对象
 * }
 * **/

然后我将createReactive方法改为如下:

function createReactive(target) {
  // 创建响应式对象
  if(!(typeof target === 'object' && target !== null)) {
    return target;
  }
  // 判断target是否被代理过
  if(sourceProxy.get(target)) {
    return sourceProxy.get(target);
  }
  // 判断代理过的里面是否有target, 防止一个对象被多次代理
  if (toRaw.has(target)) {
    return target;
  }
  // 创建观察者
  let baseHandle = {
    get(target, key, receiver) {
      // 获取
      let datas = Reflect.get(target, key, receiver);
      //  变更了下面这行代码
      return typeof target === 'object' && target !== null ? reactive(datas) : datas;
    },
    set(target, key, value, receiver) {
      // 设置
      let res = Reflect.set(target, key, value, receiver);
      console.log(res);
      return res;
    }
  }
  let observed = new Proxy(target, baseHandle);
  // 设置weakmap
  sourceProxy.set(target, observed);
  toRaw.set(observed, target);
  return observed;
}

接着我们要实现依赖收集(发布订阅),还记得前面我们说的effect方法吗?它默认会执行一次,当依赖数据变化的时候会再次执行,现在我们实现这个effect,这个也是响应式的核心 ;首先我们这个effect方法接收一个函数,并且我们要把这个函数变成响应式函数,并且让这个函数默认执行一次:

function effect(fn) {
  let effect = createReactiveFn(fn); // 创建响应式的函数
  effect(); // 默认执行一次
}

接着我们来编写createReactiveFn方法 :

let stacks = [];
function createReactiveFn(fn) {
  let eFn = function() {
    // 执行fn并且将fn存入栈中
    return carryOut(eFn, fn)
  }
  return eFn;
}
function carryOut(effect, fn) {
  // 执行fn并且将fn存入栈中
  stacks.push(effect);
  fn();
}

在取值的时候,也就是get方法中,我们就要对值和effect做好映射,当值发生变化的时候,我们直接执行值对应的effect,接下来我们先实现这部分创建关联的方法, 最后我们要一种如下的数据结构:

{
  target: {
    key: [fn, fn]
  }
}

我们编写subscribeTo来创建关联:

let targetMap = new WeakMap();
function subscribeTo(target, key) {
  let effect = stacks[stacks.length - 1];
  if (effect) { //如果有,则创建关联
    let maps = targetMap.get(target);
    if (!maps) {
      targetMap.set(target, maps = new Map);
    }
    let deps = maps.get(key);
    if (!deps) {
      maps.set(key, deps = new Set());
    }
    if(!deps.has(effect)) {
      deps.add(effect);
    }
  }
}

还记得之前写的将effect入栈吗stacks.push(effect); 当我们做完关联之后,其实这个effect在栈里就没有意义了,所以我们将它清出栈:

function carryOut(effect, fn) {
  // 执行fn并且将fn存入栈中
  stacks.push(effect);
  fn();
  stacks.pop(effect);
}

现在我们已经创建关联了,那在更新值的时候,也就是set方法中我们应该去取targetMap中的effect执行:

function trigger(target, type, key) {
  let tMap = targetMap.get(target);
  if (tMap) {
    let keyMaps = tMap.get(key);
    // 将key对应的effect执行
    if (keyMaps) {
      keyMaps.forEach((eff) => {
        eff();
      })
    }
  }
}
以下贴出本篇文章实现的完整代码:
let sourceProxy = new WeakMap(); // 存放原对象和代理过的对象
let toRaw = new WeakMap(); // 存放被代理过的对象和原对象
function reactive(target) {
  return createReactive(target);
}
function createReactive(target) {
  // 创建响应式对象
  if(!(typeof target === 'object' && target !== null)) {
    return target;
  }
  // 判断target是否被代理过
  if(sourceProxy.get(target)) {
    return sourceProxy.get(target);
  }
  // 判断代理过的里面是否有target, 防止一个对象被多次代理
  if (toRaw.has(target)) {
    return target;
  }
  // 创建观察者
  let baseHandle = {
    get(target, key, receiver) {
      // 获取
      let datas = Reflect.get(target, key, receiver);
      // 订阅
      subscribeTo(target, key); // 当key变化的时候重新执行effect
      //  变更了下面这行代码
      return typeof target === 'object' && target !== null ? reactive(datas) : datas;
    },
    set(target, key, value, receiver) {
      // 设置
      let oldValue = target[key];
      let res = Reflect.set(target, key, value, receiver);
      // 此处的判断是屏蔽无意义的修改
      if (!target.hasOwnProperty(key)) {
        // 新增属性
        console.log('新增');
        trigger(target, 'add', key);
      } else if (oldValue !== value) {
        // 修改属性
        console.log('修改');
        trigger(target, 'set', key);
      }
      console.log(res);
      return res;
    }
  }
  let observed = new Proxy(target, baseHandle);
  // 设置weakmap
  sourceProxy.set(target, observed);
  toRaw.set(observed, target);
  return observed;
}
let stacks = [];
let targetMap = new WeakMap();
// 响应式
function effect(fn) {
  let effect = createReactiveFn(fn); // 创建响应式的函数
  effect(); // 默认执行一次
}
function createReactiveFn(fn) {
  let eFn = function() {
    // 执行fn并且将fn存入栈中
    return carryOut(eFn, fn)
  }
  return eFn;
}
function carryOut(effect, fn) {
  // 执行fn并且将fn存入栈中
  try {
    stacks.push(effect);
    fn();
  } finally {
    stacks.pop(effect);
  }
}
function subscribeTo(target, key) {
  let effect = stacks[stacks.length - 1];
  if (effect) { //如果有,则创建关联
    let maps = targetMap.get(target);
    if (!maps) {
      targetMap.set(target, maps = new Map);
    }
    let deps = maps.get(key);
    if (!deps) {
      maps.set(key, deps = new Set());
    }
    if(!deps.has(effect)) {
      deps.add(effect);
    }
  }
}
function trigger(target, type, key) {
  let tMap = targetMap.get(target);
  if (tMap) {
    let keyMaps = tMap.get(key);
    // 将key对应的effect执行
    if (keyMaps) {
      keyMaps.forEach((eff) => {
        eff();
      })
    }
  }
}
// let proxy = reactive({name: 'yangbo'})
// proxy.name = 'yb';
// console.log(proxy.name);
let proxyObj = reactive({name: 'yangbo'});
effect(() => {
  console.log(proxyObj.name);
})

proxyObj.name = 'yb'

以上简单模仿了vue3.0中对响应式数据的实现,使用js实现的,感兴趣可以看看Vue3.0源码的TypeScript的写法和实现方式,本篇文章仅供参考~

参考:
Proxy
vue-next
Vue Composition API

你可能感兴趣的:(vue2.0到vue3.0响应式原理)