41

记一次vue3.0技术分享会

记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约1小时

一. 版本

当前还处于 beta版本, 想要正式使用在项目里还需要一段的时间, 但是结构与api变化应该不大了.

这里列出的并不全, 但是够用了
1. alpha 内测版本
2. beta  公测版本
3. Gamma 正式发布的候选版本
4. Final 正式版
5. plus  增强版
6. full  完全版 
7. Trial 试用版(一般有时间或者功能限制)

二. 介绍

  1. 学一门新鲜的技术,就像练习王者荣耀新出的英雄一样, 探索他的好玩之处可以给开发者带来快乐, 使用新的好的技术会让工作更愉悦

  2. 这个版本的vue 类似"我的世界", 全部都是一个个方块组成, 不要小看方块, 方块多了甚至可以组成圆形(量变引起质变), 新鲜的玩法才能激发我们的兴趣

三. vue3.0的环境搭建

准备一套搭建好的环境防治到时候出现意外, 现场再按照步骤搭建一版, 每一步都不要落下能让大家更好的理解.
  1. npm install -g @vue/cli  cli升级到4版本

  2. 随便创建个项目, vue create next-test

  3. 选择配置最好把router与vuex一起选上, 方便后续升级

  4. vue add vue-next   cli提供的进化到下一版本的命令, 执行后自动将router, vuex 升级到alpha版.

四. vue3.0重要的优化

  1. 模板编译速度的提升, 对静态数据的跳过处理.
  2. 对数组的监控
  3. 对ts有了很好的支持
  4. 对2.x版本的完全兼容
  5. 可以有多个根节点 (也有bug, 比如外层开了display:flex 那么里面会收到影响, 也就是说布局更灵活但也更要小心, 总之请对自己与他人的代码负责)
  6. 支持Source map, 虽然没演示但是这点真的重要

五. vuex, router, vue 初始化写法的变化

vue:
import { createApp } from 'vue';
import App from './App.vue'
import router from './router'
import store from './store'

// 方法一. 创建实例变成了链式, 直接写下去感觉语义与结构有点模糊, 但是我们要理解vue这样做的良苦用心, 前端趋近于函数化.
// createApp(App).use(router).use(store).mount('#app')

// 方法二.
const app = createApp(App);
app.use(router);
app.use(store);
app.mount('#app');
vuex:
import { createStore } from 'vuex'
// 专门创建实例的一个方法
export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
});
router
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  }
]

const router = createRouter({
// 专门创建history的函数
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

六. 变量的定义

1: ref

import { ref } from "vue";
export default {
  // 1: 这个版本基本逻辑都在setup里面完成了, 有人说把他当成2.x的data.
  setup() {
    // 2: 定义一个追踪的变量,也就是双向绑定.
    const n = ref(1); // 生成的n是一个对象, 这样方便vue去监控它
    function addN() {
      n.value++; // 注意这里要使用.value的形式, 因为n是对象↑, value才是他的值
    }
    return {
      n,   // 返回出去页面的template才可以使用它, {{n}} 不需要.value
      addN
    }
  }
 }

2: reactive

import { reactive, toRefs } from "vue";
export default {
  setup() {
    // 注意事项: reactive的对象不可以结构返回或导入, 会导致失去响应式
    const obj = reactive({
      name: "金毛",
      age: 4
    });
    function addObj() {
      obj.age++;
    }
    return {
      ...obj, // 这样写不好, 里面会失去响应式
      obj, // 这样写那么外面就要都基于obj来调取, 类型{{obj.age}}
      ...toRefs(obj) // 必须是reactive生成的对象, 普通对象不可以, 他把每一项都拿出来包了一下, 我们可以这样用了 {{age}}, 放心咱们多深的obj也可以响应
    }
  }
 }

7. 之前的ref何去何从

这个老兄被别人抢了关键词, 也只能自己改改写法了
  <div>
    <div ref="content">第一步, 在dom上面定义, 他会有一个回调</div>
  </div>
  <ul>
    <li>v-for 出来的ref</li>
    <li>可以写为表达式的形式, 可以推导出vue是如何实现的</li>
    <li>vue2.x的时候v-for不用这么麻烦, 直接写上去会被组装成数组</li>
    <li :ref="el => { items[index] = el }" v-for="(item,index) in 6" :key="item">{{item}}</li>
  </ul>
import { ref, onMounted, onBeforeUpdate } from "vue";
export default {
  setup() {
    // 2: 定义一个变量接收dom, 名字无所谓, 但是与dom统一的话会有很好的语义化
    const content = ref(null);
    const items = ref([]);

    // 4: 在生命周期下, 这个值已经完成了变化, 所以当然第一时间就拿到
    onMounted(() => {
      console.log(content.value);
      console.log("li标签组", items.value);
    });

    // 5: 确保在每次变更之前重置引用
    onBeforeUpdate(() => {
      items.value = [];
    });

    // 3: 返出去的名称要与dom的ref相同, 这样就可以接收到dom的回调
    return {
      content,
      items
    };
  }
};

8. 生命周期

<template>
  <div>
    <button @click="add">点击增加, 触发updata</button>
    <p>{{obj.count}}</p>
  </div>
  <p>
    2.x与 3.0的对照
    beforeCreate -> 使用 setup()
    created -> 使用 setup()
    beforeMount -> onBeforeMount
    mounted -> onMounted
    beforeUpdate -> onBeforeUpdate
    updated -> onUpdated
    beforeDestroy -> onBeforeUnmount
    destroyed -> onUnmounted
    errorCaptured -> onErrorCaptured
  </p>
</template>

<script>

//这些生命周期注册方法只能用在 setup 钩子中
import { onMounted, onUpdated, onBeforeUnmount, reactive } from "vue";
export default {
  // 1: setup显得冗长, 可以自己动手做一些插件来优化
  // 2: 本身更加明确意图
  // 3: 需要树立工程师的正确代码意识
  // 4: 能力不足可能会写出更差的代码
  // 作者说: 提升上界的收益远远大于降低下界的损失。值得深思, 前端需要提高门槛
  // 5: 调用时机: 创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用
  // 6: 这些生命周期钩子注册函数只能在 setup() 期间同步使用, 因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例), 不在当前组件下调用这些函数会抛出一个错误。
  // 7: 原则上生命周期里面不会放具体的逻辑,哪怕只有一句赋值一个三元都不可放, 这也正好符合当前的工程模式

  // 讨论: 有多少种方式, 可以判断出某个函数 当前处于哪个函数?
  //       比如有多层嵌套的组件是否有影响
  setup() {
    onMounted(() => {
      console.log("is mounted!");
    });
    onUpdated(() => {
      console.log("is onUpdated!");
    });
    onBeforeUnmount(() => {
      console.log("is onBeforeUnmount!");
    });
    const obj = reactive({
      count: 1
    });
    function add() {
      obj.count++;
    }
    return {
      obj,
      add
    };
  }
};
</script>

9. 路由

<template>
  <div>
    {{id}}
  </div>
</template>

<script>
import { getCurrentInstance, ref } from 'vue';
export default {
  setup(){
    const { ctx } = getCurrentInstance()
    // 1. 这样也是为了去掉this
    // 2. 方便类型推导
    console.log(ctx.$router); // push等方法
    console.log(ctx.$router.currentRoute.value); // 路由实例
    // 这个其实没有必要变成ref因为这个值没必要动态
    // 但是他太长了, 这个真的不能忍
    const id = ref(ctx.$router.currentRoute.value.query.id)

    // 4: 页面拦截器
    ctx.$router.beforeEach((to, from,next)=>{
      console.log('路由的生命周期')
      next()
    })
    return {
      id
    }
  }
}

</script>

10. vuex

import { createStore } from 'vuex'

// 难道前端趋势只有函数这一种吗
export default createStore({
  state: {
    name:'牛逼, 你拿到我了',
    age: 24,
    a:'白',
    b:'黑'
  },
  mutations: {
    updateName(state, n){
      state.name += n
    }
  },
  actions: {
    deferName(store) {
     setTimeout(()=>{
       // 必须只有commit可以修改值, 这个设定我比较反对, 可以讨论
       // vuex本身结构就很拖沓, 定义域使用个人都不喜欢
      store.state.name = '牛逼, 你改回来了'
     },1000)
    }
  },
  getters: {
    fullName(state){ return `${state.a} - + -${state.b}` }
  },
  modules: {
  }
});
<template>
  <div>
    <p>{{name}}</p>
    <button @click="updateName('+')">点击改变名字</button>
    <br />
    <button @click="deferName('+')">改回来</button>

    <p>{{fullName}}</p>
  </div>
</template>

<script>
import { useStore } from "vuex";
import { computed } from "vue";
export default {
  setup() {
    const store = useStore();
    // 1: 单个引入
    const name = computed(() => store.state.name);
    // 2: 引入整个state
    const state = computed(() => store.state);
    console.log("vuex的实例", state.value); // 别忘了.value

    // 3: 方法其实就直接从本体上取下来了
    const updateName = newName => store.commit("updateName", newName);

    // 4: action一个意思
    const deferName = () => store.dispatch("deferName");

    // 5: getter 没变化
    const fullName = computed(() => store.getters.fullName);
    return {
      name,
      fullName,
      deferName,
      updateName,
    };
  }
};
</script>

11. composition(这个可能是最重要的改革了)

前端算是面向函数编程, 各种规范也都趋近于函数化
composition使得前端工程师的编程规范, 更接近于原生js, 三十年河东三十年河西, 几年前前端需要模板来进行'规则化', 现在前端又想要更多的自由.
开发工程而不是插件的话, 还是不要使用mixin了, 这东西无法追溯来源, 搞得语义太差了, 我们要对它说'no'.
举例子的变量命名有点low, 抱歉~~
<template>
  <div>
    <button @click="addN1">上面的增加</button>---> {{n1}}
  </div>
   <div>
    <button @click="addN2">下面的增加</button>---> {{n2}}
    <button @click="addN210">每次n2+10</button>
  </div>
  <div>
    <p>组件里面也可以如此引用, 这就可以代替mixin一部分功能了</p>
    <button @click="addN3">n3的增加</button>---> {{n3.value}}
  </div>
  <div>
    <com></com>
  </div>
</template>

<script>
import { ref} from 'vue';
import n3Change from './mixin';
import com from '../components/composition.vue';

export default {
  components:{
    com
  },
  setup(){
     // 1: setup只是一个整合函数
     // 2: 甚至整个函数里面可能会没有具体的逻辑
     // 3: 以此推断, ref等方式定义的变量, 会自动识别在哪个setup内部, 从而达到逻辑的复用
     // 4: 由此方法可以很好的代替mixin了
     // 5: 当然, 这里也可以截获数据,来做一些事情
     const {n2, addN2} = n2Change();
     function addN210(){
       n2.value += 10
     }
     return {
       ...n1Change(),
       ...n3Change(),
       n2,
       addN2,
       addN210
     }
  }
}
// 甚至已经可以写在任何地方了, 响应式自由度大大提高
function n1Change(){
   const n1 = ref(1);
   let addN1 = ()=>{
     n1.value++
   }
   return {
     n1,
     addN1
   }
}

function n2Change(){
   const n2 = ref(1);
   let addN2 = ()=>{
     n2.value++
   }
   return {
     n2,
     addN2
   }
}
</script>

写在任何地方, 然后导入就成了mixin


import { reactive } from 'vue';


export default ()=> {
  const n3 = reactive({
    name: 'mixin',
    value: 1
  })
  const addN3=()=>{
    n3.value++
  }
  return {
    n3,
    addN3
  }
}

12. 插件的新思路

// 开发插件并不一定要挂载到vue的原型上
// 导致vue原型臃肿, 命名冲突等等(比如两个ui都叫 message)
// 原理就是 provide 和 inject, 依赖注入.

import {provide, inject} from 'vue';

// 这里使用symbol就不会造成变量名的冲突了, 这个命名权交给用户才是真正合理的架构设计
const StoreSymbol = Symbol()

export function provideString(store){
  provide(StoreSymbol, store)
}

export function useString() {

  const store = inject(StoreSymbol)

  return store
}

app.vue页面统一的初始化一下

export default {
  setup(){
    // 一些初始化'配置/操作'可以在这里进行
    // 需要放在对应的根节点, 因为依赖provide 和 inject
     provideString({
       a:'可能我是axios',
       b:'可能我是一个message弹框'
     })
  }
}

在需要使用的组件里面接收

<template>
  <div>
    插件的演示
  </div>
</template>

<script>
import { useString } from '../插件';

export default {
  setup(){
    const store = useString();
    // 不光是拿到值, 可以由app定义什么可以被拿到
    console.log('拿到值了',store)
  }
}

</script>

13. 新观察者

<template>
  <div>
    <button @click="addn1">n1增加--{{n1}}</button>
    <button @click="addn2">n2增加--{{n2}}</button>
    <button @click="addn3">n3增加--{{n3}}</button>
  </div>
</template>

<script>
import { watch, ref } from "vue";
export default {
  setup() {
    const n1 = ref(1);
    const n2 = ref(1);
    const n3 = ref(1);
    // 1: 监听一个
    // 第一个参数是函数返回值, 当然也可以 直接写n1 
    // 如果监听的是一个对象里面的某个属性, 那就需要这种函数的写法了, 比2.x的字符串写法高明很多

    watch(
      () => n1.value,
      (val, oldVal) => {
        console.log("新值", val);
        console.log("老值", oldVal);
      }
    );
    // 2: 监听多个
    // 数组的形式定义多个, 这就出现问题了吧, 如果我观察的对象就是个数组, 并且每一项都是一个返回值的函数, 岂不是会被他误认为是多监控的结构, 苦恼
    watch(
      [() => n2.value, ()=>n3.value],
      ([val, val3],[val2, val4]) => {
        // val 是 n2的新值   val2是 n2的老值
        // val3 是 n3的新值  val4是 n3的老值
        console.log("新值 与 老值 是这种对应关系", val, val2);
        console.log("新值 与 老值 是这种对应关系", val3, val4);
      }
    );

    function addn1() {
      n1.value++;
    }
    function addn2() {
      n2.value++;
    }
     function addn3() {
      n3.value++;
    }
    return {
      addn1,
      addn2,
      addn3,
      n1,
      n2,
      n3
    };
  }
};
</script>

13. 新计算属性

别看watchEffect带个'watch',但是他的功能可以归为计算属性里面
<template>
  <div>
    <button @click="addCount">点击计算</button>
    <button @click="setCount(1)">点击出发set</button>
    <p>count--{{count}}</p>
    <p>count2--{{count2}}</p>
    <p>count3--{{count3}}</p>
  </div>
</template>

<script>
// 弄得类似react了
import { computed, ref, watchEffect } from "vue";
export default {
  setup() {
    const count = ref(1);
    // 1. 默认的定义方式
    const count2 = computed(() => count.value * 2);
    console.log(count2.value); // 也要value因为可能是简单类型
    // 2. getter与setter当然可以定义
    const count3 = computed({
      get: () => count.value * 3,
      set: val => {
        // 这里重置了count
        count.value = val;
      }
    });
    // 3. watchEffect 更像是计算函数
    // 立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数
    // 侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
    // Vue 的响应式系统会缓存副作用函数,并异步地刷新它, 比如同时改变了count与conut4此时watchEffect只是执行一次
    // 初始化运行是在组件 mounted 之前执行的。因此,如果你希望在编写副作用函数时访问 DOM(或模板 ref),请在 onMounted 钩子中进行
    // 并不是返回值, 而是监听里面所有的值, 任何有变化都会重新执行, 他应该可以玩出点东西。
    const count4 = ref(1);
    const stop = watchEffect(() => {
      if (count4.value) {
        console.log("作为判断条件也可以根据count4的变化而重新执行");
      }
      console.log(count.value);
    });
    setTimeout(() => {
      stop(); // 停止监听
    }, 10000);
    function addCount() {
      count.value++;
      setTimeout(() => {
        count4.value++;
      }, 1000);
    }
    // 触发setter
    function setCount() {
      count3.value = 2;
    }
    return {
      count,
      count2,
      addCount,
      count3,
      setCount
    };
  }
};
</script>

14. customRef防抖

当然这里说的'防抖'不是重点, 重点是这种代码的思维
<template>
  <div>
    <input type="text" v-model="text" />
  </div>
</template>

<script>
import { customRef, onUnmounted } from "vue";
export default {
  setup() {
    let timeout = null; // 并不需要响应式
    const text = useDebouncedRef("hello", (time) => {
      // 毕竟是延时的不用担心获取不到这个值
      console.log("延时执行回调", text.value);
      console.log('时间实例', time)
      timeout = time;
    });
    // 好习惯也是成为合格工程师的必要条件
    onUnmounted(()=>{
        clearTimeout(timeout);
    })
    return {
      text
    };
  }
};

// 并不用纯粹的js编写, 可以利用customRef来监控这个值的一举一动
// 写法一般, 但是思路又多了一条, 感谢
function useDebouncedRef(value, callback, delay = 200) {
  let timeout;
  // 两个参数分别是用于追踪的 track 与用于触发响应的 trigger
  // 这两个参数对 值的追踪 在当前并没有用,比如watchEffect的出发机制
  // 不调用这两个值没问题, 但是如果写成插件的话还是要调用的, 因为别人没准在追踪这个值,

  // 注意:  这个函数不可以有太大的delay, 如果超过500的话就需要考虑在组件销毁时候的清除定时器, 反而逻辑加深了, 此时我们可以每次把演示器的实例拿到
  return customRef((track,trigger) => {
    return {
      get() {
        track()
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        // callback接受的太晚了, 可以在这里用另一个函数或对象接收
        timeout = setTimeout(() => {
          value = newValue;
          trigger()
          callback(timeout);
        }, delay);
      }
    };
  });
}
</script>

15. 组件与注入

父级

<template>
  <div>
    组件:
    <zj :type="type" @ok="wancheng"></zj>
  </div>
</template>

<script>
import zj from "../components/子组件.vue";
import { ref } from 'vue';
import { provide } from 'vue'

export default {
  components: { 
    zj
  },
  setup() {
    provide('name','向下传值'); // 基础值
    provide('name2', ref('向下传值')); // 监控值
    const type = ref('大多数');

    function wancheng(msg){
      console.log('子组件-->',msg)
      setTimeout(()=>{
        type.value = 'xxxxxxx'
      },2000)
    }
    return {
      type,
      wancheng
    }
  }
};
</script>

子组件

<template>
  <div>props的属性不用setup去return --- {{type}}</div>
</template>

<script>
import { inject, ref } from 'vue'
// 为了让 TypeScript 正确的推导类型,我们必须使用 createComponent 来定义组件:
export default {
  props: {
    type: String
  },
  // 1: props也是不可以解构的, 会失去响应式
  // 2: context是上下文, 我们可以获取到slots emit 等方法
  // 3: props, context 分开也是为了ts更明确的类型推导
  // setup({type}){
  setup(props, context) {
    // 1: props
    console.log("props", props.type);
    console.log("上下文", context);
    context.emit('ok','传递完成')

    // 2: 注入
    console.log('inject',inject('name'));
    console.log('inject',inject('xxxx','我是默认值'))
    inject('name1', ref('默认值')) // 接收方也可以这样
  }
};
</script>

16. 总结

每次看到新技术都会感觉挺好玩的, 一成不变的生活会太无趣了, 在某些方面讲vue失去了一些本来的优势, 但是人家可以兼容vue2.x那就没的说了, 作为分享会的稿子的话时间差不多一个小时, 最好每个点都现场手敲, 光让大家看已经写好的代码会走神的, 我在学习视频的时候最不喜欢的就是老师说"这个我就不演示了".
这次就这么多, 希望和你一起进步.


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者