快速进阶Vue30

13次阅读

共计 10605 个字符,预计需要花费 27 分钟才能阅读完成。

在 2019.10.5 日发布了 Vue3.0 预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.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

1. 剖析 Vue Composition API

可以去看官方地址

  • Vue 3 使用 ts 实现了类型推断,新版 api 全部采用普通函数,在编写代码时可以享受完整的类型推断(避免使用装饰器)
  • 解决了多组件间逻辑重用问题(解决:高阶组件、mixin、作用域插槽)
  • Composition API 使用简单

先尝鲜 Vue3.0 看看效果

<script src="vue.global.js"></script>
<div id="container"></div>
<script>
    function usePosition(){ // 实时获取鼠标位置
        let state = Vue.reactive({x:0,y:0});
        function update(e) {
            state.x= e.pageX
            state.y = e.pageY
        }
        Vue.onMounted(() => {window.addEventListener('mousemove', update)
        })
        Vue.onUnmounted(() => {window.removeEventListener('mousemove', update)
        })
        return Vue.toRefs(state);
    }
    const App = {setup(){ // Composition API 使用的入口
            const state  = Vue.reactive({name:'youxuan'}); // 定义响应数据
            const {x,y} = usePosition(); // 使用公共逻辑
            Vue.onMounted(()=>{console.log('当组挂载完成')
            });
            Vue.onUpdated(()=>{console.log('数据发生更新')
            });
            Vue.onUnmounted(()=>{console.log('组件将要卸载')
            })
            function changeName(){state.name = 'webyouxuan';}
            return { // 返回上下文, 可以在模板中使用
                state,
                changeName,
                x,
                y
            }
        },
        template:`<button @click="changeName">{{state.name}} 鼠标 x: {{x}} 鼠标: {{y}}</button>`
    }
    Vue.createApp().mount(App,container);
</script>

到这里你会发现 响应式 才是 Vue 的灵魂

2. 源码目录剖析

packages 目录中包含着 Vue3.0 所有功能

├── packages
│   ├── compiler-core # 所有平台的编译器
│   ├── compiler-dom # 针对浏览器而写的编译器
│   ├── reactivity # 数据响应式系统
│   ├── runtime-core # 虚拟 DOM 渲染器,Vue 组件和 Vue 的各种 API
│   ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。│   ├── runtime-test # 专门为测试写的 runtime
│   ├── server-renderer # 用于 SSR
│   ├── shared # 帮助方法
│   ├── template-explorer
│   └── vue # 构建 vue runtime + compiler

compiler
compiler-core主要功能是暴露编译相关的 API 以及 baseCompile 方法
compiler-dom基于 compiler-core 封装针对浏览器的compiler (对浏览器标签进行处理)

runtime
runtime-core 虚拟 DOM 渲染器、Vue 组件和 Vue 的各种 API
runtime-testDOM 结构格式化成对象,方便测试
runtime-dom 基于 runtime-core 编写的浏览器的 runtime (增加了节点的增删改查,样式处理等),返回rendercreateApp 方法

reactivity
单独的数据响应式系统,核心方法reactiveeffectrefcomputed

vue
整合 compiler + runtime

到此我们解析了 Vue3.0 结构目录,整体来看整个项目还是非常清晰的

再来尝尝鲜:
我们可以根据官方的测试用例来看下如何使用Vue3.0

const app = {template:`<div>{{count}}</div>`,
    data(){return {count:100}
    },
}
let proxy = Vue.createApp().mount(app,container);
setTimeout(()=>{proxy.count = 200;},2000)

接下来我们来对比 Vue 2 和 Vue 3 中的响应式原理区别

3.Vue2.0 响应式原理机制 – defineProperty

这个原理老生常谈了,就是 拦截对象 ,给对象的属性增加setget 方法,因为核心是 defineProperty 所以还需要对数组的方法进行拦截

3.1 对对象进行拦截

function observer(target){
    // 如果不是对象数据类型直接返回即可
    if(typeof target !== 'object'){return target}
    // 重新定义 key
    for(let key in target){defineReactive(target,key,target[key])
    }
}
function update(){console.log('update view')
}
function defineReactive(obj,key,value){observer(value); // 有可能对象类型是多层,递归劫持
    Object.defineProperty(obj,key,{get(){
            // 在 get 方法中收集依赖
            return value
        },
        set(newVal){if(newVal !== value){observer(value);
                update(); // 在 set 方法中触发更新}
        }
    })
}
let obj = {name:'youxuan'}
observer(obj);
obj.name = 'webyouxuan';

3.2 数组方法劫持

let oldProtoMehtods = Array.prototype;
let proto = Object.create(oldProtoMehtods);
['push','pop','shift','unshift'].forEach(method=>{
    Object.defineProperty(proto,method,{get(){update();
            oldProtoMehtods[method].call(this,...arguments)
        }
    })
})
function observer(target){if(typeof target !== 'object'){return target}
    // 如果不是对象数据类型直接返回即可
    if(Array.isArray(target)){Object.setPrototypeOf(target,proto);
        // 给数组中的每一项进行 observr
        for(let i = 0 ; i < target.length;i++){observer(target[i])
        }
        return
    };
    // 重新定义 key
    for(let key in target){defineReactive(target,key,target[key])
    }
}

测试

let obj = {hobby:[{name:'youxuan'},'喝']}
observer(obj)
obj.hobby[0].name = 'webyouxuan'; // 更改数组中的对象也会触发试图更新
console.log(obj)

这里依赖收集的过程就不详细描述了,我们把焦点放在 Vue3.0

  • Object.defineProperty 缺点

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

4.Vue3.0 数据响应机制 – Proxy

在学习 Vue3.0 之前,你必须要先熟练掌握 ES6 中的 Proxy、Reflect 及 ES6 中为我们提供的 Map、Set 两种数据结构

先应用再说原理:

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

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

4.1 reactive 方法实现

通过 proxy 自定义获取、增加、删除等行为

function reactive(target){
    // 创建响应式对象
    return createReactiveObject(target);
}
function isObject(target){return typeof target === 'object' && target!== null;}
function createReactiveObject(target){
    // 判断 target 是不是对象, 不是对象不必继续
    if(!isObject(target)){return target;}
    const handlers = {get(target,key,receiver){ // 取值
            console.log('获取')
            let res = Reflect.get(target,key,receiver);
            return res;
        },
        set(target,key,value,receiver){ // 更改、新增属性
            console.log('设置')
            let result = Reflect.set(target,key,value,receiver);
            return result;
        },
        deleteProperty(target,key){ // 删除属性
            console.log('删除')
            const result = Reflect.deleteProperty(target,key);
            return result;
        }
    }
    // 开始代理
    observed = new Proxy(target,handlers);
    return observed;
}
let p = reactive({name:'youxuan'});
console.log(p.name); // 获取
p.name = 'webyouxuan'; // 设置
delete p.name; // 删除

我们继续考虑多层对象如何实现代理

let p = reactive({name: "youxuan", age: { num: 10} });
p.age.num = 11

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

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

这里我们将 p.age 取到的对象再次进行代理,这样在去更改值即可触发 set 方法

我们继续考虑数组问题
我们可以发现 Proxy 默认可以支持数组,包括数组的长度变化以及索引值的变化

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

但是这样会触发两次 set 方法,第一次更新的是数组中的第 4 项,第二次更新的是数组的length

我们来屏蔽掉多次触发,更新操作

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

解决重复使用 reactive 情况

// 情况 1. 多次代理同一个对象
let arr = [1,2,3,4];
let p = reactive(arr);
reactive(arr);

// 情况 2. 将代理后的结果继续代理
let p = reactive([1,2,3,4]);
reactive(p);

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

const toProxy = new WeakMap(); // 存放被代理过的对象
const toRaw = new WeakMap(); // 存放已经代理过的对象
function reactive(target) {
  // 创建响应式对象
  return createReactiveObject(target);
}
function isObject(target) {return typeof target === "object" && target !== null;}
function hasOwn(target,key){return target.hasOwnProperty(key);
}
function createReactiveObject(target) {if (!isObject(target)) {return target;}
  let observed = toProxy.get(target);
  if(observed){ // 判断是否被代理过
    return observed;
  }
  if(toRaw.has(target)){ // 判断是否要重复代理
    return target;
  }
  const handlers = {get(target, key, receiver) {
      // 取值
      console.log("获取");
      let res = Reflect.get(target, key, receiver);
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {let oldValue = target[key];
      let hadKey = hasOwn(target,key);
      let result = Reflect.set(target, key, value, receiver);
      if(!hadKey){console.log('更新 添加')
      }else if(oldValue !== value){console.log('更新 修改')
      }
      return result;
    },
    deleteProperty(target, key) {console.log("删除");
      const result = Reflect.deleteProperty(target, key);
      return result;
    }
  };
  // 开始代理
  observed = new Proxy(target, handlers);
  toProxy.set(target,observed);
  toRaw.set(observed,target); // 做映射表
  return observed;
}

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

get(target, key, receiver) {let res = Reflect.get(target, key, receiver);
+   track(target,'get',key); // 依赖收集
    return isObject(res) 
    ?reactive(res):res;
},
set(target, key, value, receiver) {let oldValue = target[key];
    let hadKey = hasOwn(target,key);
    let result = Reflect.set(target, key, value, receiver);
    if(!hadKey){+     trigger(target,'add',key); // 触发添加
    }else if(oldValue !== value){+     trigger(target,'set',key); // 触发修改
    }
    return result;
}

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

4.2 effect 实现

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

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

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

function effect(fn) {const effect = createReactiveEffect(fn); // 创建响应式的 effect
  effect(); // 先执行一次
  return effect;
}
const activeReactiveEffectStack = []; // 存放响应式 effect
function createReactiveEffect(fn) {const effect = function() {
    // 响应式的 effect
    return run(effect, fn);
  };
  return effect;
}
function run(effect, fn) {
    try {activeReactiveEffectStack.push(effect);
      return fn(); // 先让 fn 执行, 执行时会触发 get 方法,可以将 effect 存入对应的 key 属性} finally {activeReactiveEffectStack.pop(effect);
    }
}

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

const targetMap = new WeakMap();
function track(target,type,key){
    // 查看是否有 effect
    const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
    if(effect){let depsMap = targetMap.get(target);
        if(!depsMap){ // 不存在 map
            targetMap.set(target,depsMap = new Map());
        }
        let dep = depsMap.get(target);
        if(!dep){ // 不存在 set
            depsMap.set(key,(dep = new Set()));
        }
        if(!dep.has(effect)){dep.add(effect); // 将 effect 添加到依赖中
        }
    }
}

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

function trigger(target,type,key){const depsMap = targetMap.get(target);
    if(!depsMap){return}
    let effects = depsMap.get(key);
    if(effects){
        effects.forEach(effect=>{effect();
        })
    }
}

我们发现如下问题

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

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

function trigger(target, type, key) {const depsMap = targetMap.get(target);
  if (!depsMap) {return;}
  let effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => {effect();
    });
  }
  // 处理如果当前类型是增加属性,如果用到数组的 length 的 effect 应该也会被执行
  if (type === "add") {let effects = depsMap.get("length");
    if (effects) {
      effects.forEach(effect => {effect();
      });
    }
  }
}

4.3 ref 实现

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

function convert(val) {return isObject(val) ? reactive(val) : val;
}
function ref(raw) {raw = convert(raw);
  const v = {
    _isRef:true, // 标识是 ref 类型
    get value() {track(v, "get", "");
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(v,'set','');
    }
  };
  return v;
}

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

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

这样做的话岂不是每次都要多来一个.value,这样太难用了

get 方法中判断如果获取的是 ref 的值,就将此值的 value 直接返回即可

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

4.4 computed 实现

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

先来看用法:

let a = reactive({name:'youxuan'});
let c = computed(()=>{console.log('执行次数')
  return a.name +'webyouxuan';
})
// 不取不执行,取 n 次只执行一次
console.log(c.value);
console.log(c.value);
function computed(getter){
  let dirty = true;
  const runner = effect(getter,{ // 标识这个 effect 是懒执行
    lazy:true, // 懒执行
    scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行 effect
      dirty = true;
    }
  });
  let value;
  return {
    _isRef:true,
    get value(){if(dirty){value = runner(); // 执行 runner 会继续收集依赖
        dirty = false;
      }
      return value;
    }
  }
}

修改 effect 方法

function effect(fn,options) {let effect = createReactiveEffect(fn,options);
  if(!options.lazy){ // 如果是 lazy 则不立即执行
    effect();}
  return effect;
}
function createReactiveEffect(fn,options) {const effect = function() {return run(effect, fn);
  };
  effect.scheduler = options.scheduler;
  return effect;
}

trigger 时判断

deps.forEach(effect => {if(effect.scheduler){ // 如果有 scheduler 说明不需要执行 effect
    effect.scheduler(); // 将 dirty 设置为 true, 下次获取值时重新执行 runner 方法}else{effect(); // 否则就是 effect 正常执行即可
  }
});
let a = reactive({name:'youxuan'});
let c = computed(()=>{console.log('执行次数')
  return a.name +'webyouxuan';
})
// 不取不执行,取 n 次只执行一次
console.log(c.value);
a.name = 'zf10'; // 更改值 不会触发重新计算, 但是会将 dirty 变成 true

console.log(c.value); // 重新调用计算方法

到此我们将 Vue3.0 核心的 Composition Api 就讲解完毕了!不管是面试还是后期的应用也再也不需要担心啦!~

正文完
 0