在2019.10.5日发布了Vue3.0预览版源码,可以直接看 github源码。
新版Vue 3.0计划并已实现的主要架构改进和新功能:

  • 编译器(Compiler)
    • 使用模块化架构
    • 优化 “Block tree”
    • 更激进的 static tree hoisting 功能 (检测静态语法,进行提升)
    • 支持 Source map
    • 内置标识符前缀(又名”stripWith”)
    • 内置整齐打印(pretty-printing)功能
    • 移除 Source map 和标识符前缀功能后,使用 Brotli 压缩的浏览器版本精简了大约10KB
  • 运行时(Runtime)

    • 速度显著提升
    • 同时支持 Composition API 和 Options API,以及 typings
    • 基于 Proxy 实现的数据变更检测
    • 支持 Fragments (允许组件有从多个根结点)
    • 支持 Portals (允许在DOM的其它位置进行渲染)
    • 支持 Suspense w/ async setup()

      目前不支持IE11 说明:
      Vue2.0响应式原理机制 - defineProperty,这个原理老生常谈了,就是拦截对象,给对象的属性增加setget方法,因为核心是defineProperty所以还需要对数组的方法进行拦截,
      Object.defineProperty缺点

      • 无法监听数组的变化
      • 需要深度遍历,浪费内存

在学习Vue3.0之前,你必须要先熟练掌握ES6中的 ProxyReflect 及 ES6中为我们提供的 MapSet两种数据结构.
Vue3.0响应式原理机制- Proxy
先应用再说原理:

  1. let p = Vue.reactive({name:'youxuan'});
  2. Vue.effect(()=>{ // effect方法会立即被触发
  3. console.log(p.name);
  4. })
  5. p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法

源码是采用ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!

reactive方法实现

  1. 通过proxy 自定义获取、增加、删除等行为
  2. function reactive(target){
  3. // 创建响应式对象
  4. return createReactiveObject(target);
  5. }
  6. function isObject(target){
  7. return typeof target === 'object' && target!== null;
  8. }
  9. function createReactiveObject(target){
  10. // 判断target是不是对象,不是对象不必继续
  11. if(!isObject(target)){
  12. return target;
  13. }
  14. const handlers = {
  15. get(target,key,receiver){ // 取值
  16. console.log('获取')
  17. let res = Reflect.get(target,key,receiver);
  18. return res;
  19. },
  20. set(target,key,value,receiver){ // 更改 、 新增属性
  21. console.log('设置')
  22. let result = Reflect.set(target,key,value,receiver);
  23. return result;
  24. },
  25. deleteProperty(target,key){ // 删除属性
  26. console.log('删除')
  27. const result = Reflect.deleteProperty(target,key);
  28. return result;
  29. }
  30. }
  31. // 开始代理
  32. observed = new Proxy(target,handlers);
  33. return observed;
  34. }
  35. let p = reactive({name:'youxuan'});
  36. console.log(p.name); // 获取
  37. p.name = 'webyouxuan'; // 设置
  38. delete p.name; // 删除

我们继续考虑多层对象如何实现代理
由于我们只代理了第一层对象,所以对age对象进行更改是不会触发set方法的,但是却触发了get方法,这是由于 p.age会造成 get操作

  1. get(target, key, receiver) {
  2. // 取值
  3. console.log("获取");
  4. let res = Reflect.get(target, key, receiver);
  5. return isObject(res) // 懒代理,只有当取值时再次做代理,vue2.0中一上来就会全部递归增加getter,setter
  6. ? reactive(res) : res;
  7. }

这里我们将``p.age``取到的对象再次进行代理,这样在去更改值即可触发``set``方法
我们继续考虑数组问题
我们可以发现Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化

  1. let p = reactive([1,2,3,4]);
  2. p.push(5);

但是这样会触发两次set方法,第一次更新的是数组中的第4项,第二次更新的是数组的length
我们来屏蔽掉多次触发,更新操作

  1. set(target, key, value, receiver) {
  2. // 更改、新增属性
  3. let oldValue = target[key]; // 获取上次的值
  4. let hadKey = hasOwn(target,key); // 看这个属性是否存在
  5. let result = Reflect.set(target, key, value, receiver);
  6. if(!hadKey){ // 新增属性
  7. console.log('更新 添加')
  8. }else if(oldValue !== value){ // 修改存在的属性
  9. console.log('更新 修改')
  10. }
  11. // 当调用push 方法第一次修改时数组长度已经发生变化
  12. // 如果这次的值和上次的值一样则不触发更新
  13. return result;
  14. }

解决重复使用reactive情况

  1. // 情况1.多次代理同一个对象
  2. let arr = [1,2,3,4];
  3. let p = reactive(arr);
  4. reactive(arr);
  5. // 情况2.将代理后的结果继续代理
  6. let p = reactive([1,2,3,4]);
  7. reactive(p);

通过hash表的方式来解决重复代理的情况

  1. const toProxy = new WeakMap(); // 存放被代理过的对象
  2. const toRaw = new WeakMap(); // 存放已经代理过的对象
  3. function reactive(target) {
  4. // 创建响应式对象
  5. return createReactiveObject(target);
  6. }
  7. function isObject(target) {
  8. return typeof target === "object" && target !== null;
  9. }
  10. function hasOwn(target,key){
  11. return target.hasOwnProperty(key);
  12. }
  13. function createReactiveObject(target) {
  14. if (!isObject(target)) {
  15. return target;
  16. }
  17. let observed = toProxy.get(target);
  18. if(observed){ // 判断是否被代理过
  19. return observed;
  20. }
  21. if(toRaw.has(target)){ // 判断是否要重复代理
  22. return target;
  23. }
  24. const handlers = {
  25. get(target, key, receiver) {
  26. // 取值
  27. console.log("获取");
  28. let res = Reflect.get(target, key, receiver);
  29. return isObject(res) ? reactive(res) : res;
  30. },
  31. set(target, key, value, receiver) {
  32. let oldValue = target[key];
  33. let hadKey = hasOwn(target,key);
  34. let result = Reflect.set(target, key, value, receiver);
  35. if(!hadKey){
  36. console.log('更新 添加')
  37. }else if(oldValue !== value){
  38. console.log('更新 修改')
  39. }
  40. return result;
  41. },
  42. deleteProperty(target, key) {
  43. console.log("删除");
  44. const result = Reflect.deleteProperty(target, key);
  45. return result;
  46. }
  47. };
  48. // 开始代理
  49. observed = new Proxy(target, handlers);
  50. toProxy.set(target,observed);
  51. toRaw.set(observed,target); // 做映射表
  52. return observed;
  53. }

到这里reactive方法基本实现完毕,接下来就是与Vue2中的逻辑一样实现依赖收集和触发更新原理.png

  1. get(target, key, receiver) {
  2. let res = Reflect.get(target, key, receiver);
  3. + track(target,'get',key); // 依赖收集
  4. return isObject(res)
  5. ?reactive(res):res;
  6. },
  7. set(target, key, value, receiver) {
  8. let oldValue = target[key];
  9. let hadKey = hasOwn(target,key);
  10. let result = Reflect.set(target, key, value, receiver);
  11. if(!hadKey){
  12. + trigger(target,'add',key); // 触发添加
  13. }else if(oldValue !== value){
  14. + trigger(target,'set',key); // 触发修改
  15. }
  16. return result;
  17. }

track的作用是依赖收集,收集的主要是effect,我们先来实现effect原理,之后再完善 tracktrigger方法

effect实现

effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。

  1. let school = {name:'youxuan'}
  2. let p = reactive(school);
  3. effect(()=>{
  4. console.log(p.name); // youxuan
  5. })

我们来实现effect方法,我们需要将effect方法包装成响应式effect

  1. function effect(fn) {
  2. const effect = createReactiveEffect(fn); // 创建响应式的effect
  3. effect(); // 先执行一次
  4. return effect;
  5. }
  6. const activeReactiveEffectStack = []; // 存放响应式effect
  7. function createReactiveEffect(fn) {
  8. const effect = function() {
  9. // 响应式的effect
  10. return run(effect, fn);
  11. };
  12. return effect;
  13. }
  14. function run(effect, fn) {
  15. try {
  16. activeReactiveEffectStack.push(effect);
  17. return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性
  18. } finally {
  19. activeReactiveEffectStack.pop(effect);
  20. }
  21. }

当调用fn()时可能会触发get方法,此时会触发track

  1. const targetMap = new WeakMap();
  2. function track(target,type,key){
  3. // 查看是否有effect
  4. const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
  5. if(effect){
  6. let depsMap = targetMap.get(target);
  7. if(!depsMap){ // 不存在map
  8. targetMap.set(target,depsMap = new Map());
  9. }
  10. let dep = depsMap.get(target);
  11. if(!dep){ // 不存在set
  12. depsMap.set(key,(dep = new Set()));
  13. }
  14. if(!dep.has(effect)){
  15. dep.add(effect); // 将effect添加到依赖中
  16. }
  17. }
  18. }

当更新属性时会触发trigger执行,找到对应的存储集合拿出effect依次执行

  1. function trigger(target,type,key){
  2. const depsMap = targetMap.get(target);
  3. if(!depsMap){
  4. return
  5. }
  6. let effects = depsMap.get(key);
  7. if(effects){
  8. effects.forEach(effect=>{
  9. effect();
  10. })
  11. }
  12. }

我们发现如下问题

  1. let school = [1,2,3];
  2. let p = reactive(school);
  3. effect(()=>{
  4. console.log(p.length);
  5. })
  6. p.push(100)

新增了值,effect方法并未重新执行,因为push中修改length已经被我们屏蔽掉了触发trigger方法,所以当新增项时应该手动触发length属性所对应的依赖。

  1. function trigger(target, type, key) {
  2. const depsMap = targetMap.get(target);
  3. if (!depsMap) {
  4. return;
  5. }
  6. let effects = depsMap.get(key);
  7. if (effects) {
  8. effects.forEach(effect => {
  9. effect();
  10. });
  11. }
  12. // 处理如果当前类型是增加属性,如果用到数组的length的effect应该也会被执行
  13. if (type === "add") {
  14. let effects = depsMap.get("length");
  15. if (effects) {
  16. effects.forEach(effect => {
  17. effect();
  18. });
  19. }
  20. }
  21. }

ref实现

ref可以将原始数据类型也转换成响应式数据,需要通过.value属性进行获取值

  1. function convert(val) {
  2. return isObject(val) ? reactive(val) : val;
  3. }
  4. function ref(raw) {
  5. raw = convert(raw);
  6. const v = {
  7. _isRef:true, // 标识是ref类型
  8. get value() {
  9. track(v, "get", "");
  10. return raw;
  11. },
  12. set value(newVal) {
  13. raw = newVal;
  14. trigger(v,'set','');
  15. }
  16. };
  17. return

问题又来了我们再编写个案例

  1. let r = ref(1);
  2. let c = reactive({
  3. a:r
  4. });
  5. console.log(c.a.value);

这样做的话岂不是每次都要多来一个.value,这样太难用了
get方法中判断如果获取的是ref的值,就将此值的value直接返回即可

  1. let res = Reflect.get(target, key, receiver);
  2. if(res._isRef){
  3. return res.value
  4. }

computed实现

computed 实现也是基于 effect 来实现的,特点是computed中的函数不会立即执行,多次取值是有缓存机制的
先来看用法:

  1. let a = reactive({name:'youxuan'});
  2. let c = computed(()=>{
  3. console.log('执行次数')
  4. return a.name +'webyouxuan';
  5. })
  6. // 不取不执行,取n次只执行一次
  7. console.log(c.value);
  8. console.log(c.value);
  9. function computed(getter){
  10. let dirty = true;
  11. const runner = effect(getter,{ // 标识这个effect是懒执行
  12. lazy:true, // 懒执行
  13. scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect
  14. dirty = true;
  15. }
  16. });
  17. let value;
  18. return {
  19. _isRef:true,
  20. get value(){
  21. if(dirty){
  22. value = runner(); // 执行runner会继续收集依赖
  23. dirty = false;
  24. }
  25. return value;
  26. }
  27. }
  28. }

修改effect方法

  1. function effect(fn,options) {
  2. let effect = createReactiveEffect(fn,options);
  3. if(!options.lazy){ // 如果是lazy 则不立即执行
  4. effect();
  5. }
  6. return effect;
  7. }
  8. function createReactiveEffect(fn,options) {
  9. const effect = function() {
  10. return run(effect, fn);
  11. };
  12. effect.scheduler = options.scheduler;
  13. return effect;
  14. }
  15. trigger时判断
  16. deps.forEach(effect => {
  17. if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect
  18. effect.scheduler(); // 将dirty设置为true,下次获取值时重新执行runner方法
  19. }else{
  20. effect(); // 否则就是effect 正常执行即可
  21. }
  22. });
  23. let a = reactive({name:'youxuan'});
  24. let c = computed(()=>{
  25. console.log('执行次数')
  26. return a.name +'webyouxuan';
  27. })
  28. // 不取不执行,取n次只执行一次
  29. console.log(c.value);
  30. a.name = 'zf10'; // 更改值 不会触发重新计算,但是会将dirty变成true
  31. console.log(c.value); // 重新调用计算方法

到此我们将Vue3.0核心的 Composition Api 就完毕了