在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 = []; // 存放响应式effectfunction 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变成trueconsole.log(c.value); // 重新调用计算方法
到此我们将Vue3.0核心的 Composition Api 就讲解完毕了! 不管是面试还是后期的应用也再也不需要担心啦!~