[TOC]

vue3.0初尝试

一.vue3.0的安装

//这里我使用了yarn,用npm同理
//升级vue-cli v4.5或以上
yarn global add @vue/cli@next
//安装
yarn global add create-vite-app
 create-vite-app vue-ws<项目名>
//安装包
yarn
//运行
yarn dev

报错

三.具体使用 - 图1

解决方法

打开以下文件删除c前面的部分!

三.具体使用 - 图2

二.vue2.0与3.0的不同

vue是mvvm框架,那么vue的响应式是如何实现的呢

vue2的实现方式是通过Object.defineProperty来重新定义getter,setter方法实现的。但是这么做只能做到浅层响应,如果数据存在两层或多层就无法做到数据响应式了。如果想要做到两层或多层响应,可以使用递归的方式,但是递归会导致性能受到影响,对于数组可以采用函数劫持的方式实现的

let effective
function effect(fun) {
    effective = fun
}

function reactive(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }

    Object.keys(data).forEach(function (key) {
        let value = data[key]
        Object.defineProperty(data, key, {
            emumerable: false,
            configurable: true,
            get: () => {
                return value
            },
            set: newVal => {
                if (newVal !== value) {
                    effective()
                    value = newVal
                }
            }
        })
    })
    return data
}

module.exports = {
    effect, reactive
}

新版的Vue3使用ES6的Proxy方式来解决这个问题。首先Proxy是支持数组的也就是数组是不需要做特别的代码的。对于深层监听也不不必要使用递归的方式解决。当get的判断值是对象类型时,将对象做响应式处理返回就可以了,并且这个操作不是发生在数据初始化的时候,而是设置值得时候,因此对于性能而言,得到很大的提升。

function reactive(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }
    const observed = new Proxy(data, {
        get(target, key, receiver) {
            // Reflect有返回值不报错
            let result = Reflect.get(target, key, receiver)

            // 多层代理
            return typeof result !== 'object' ? result : reactive(result) 
        },
        set(target, key, value, receiver) {
            effective()
            // proxy + reflect
            const ret = Reflect.set(target, key, value, receiver)
            return ret
        },

        deleteProperty(target,key){
            const ret = Reflect.deleteProperty(target,key)
            return ret
        }

    })
    return observed
}

三.vue3.0性能

vue3.0的性能优化主要在于以下几点

  • Performance
  • Tree-shaking support
  • Composition API
  • Fragment,Teleport,Suppense
  • Better Typescript support
  • Custom Render API

在vue2.0的基础上3.0保存了虚拟dom,原先的vue2.0中diff算法,当数据改变需要一个节点一个节点的比较,3.0比较的是只发生改变的dom节点,动态更新数据时永远只会关注动态更新的东西

四.Composition API

4.1 setup

setup其实就是组件的另外一个选项:

  • 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;
  • 比如methods、computed、watch、data、生命周期等等;

4.1.1 setup的参数

  • 第一个参数:props,主要用于父子通信传值 ```vue
``` - 第二个参数:context,它是一个SetupContext,它里面包含三个属性: - attrs:所有的非prop的attribute; - slots:父组件传递过来的插槽; - emit:当我们组件内部需要发出事件时会用到emit,在函数内部无法访问到this,因此不可以通过 `this.$emit`发出事件; ```vue //父组件 ``` ```vue //子组件


**结果截图:**

![](https://cdn.nlark.com/yuque/0/2021/png/2158281/1636602854782-769990f2-4677-4adb-85ec-2a8e410b587d.png#alt=image.png)

<a name="2f0e9991"></a>
### 4.1.2 setup返回值

- setup的返回值可以在模板template中被使用;
- 可以通过setup的返回值来替代data选项;

```vue
<template>
  <div>
    <h2>{{ name }}</h2>
    <h2>当前数字为: {{ counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import { ref } from "vue";
export default {
  props: {
    message: String,
  },
  setup(props, context) {
    const name = "ws";
    let counter = ref(100);

    const increment = () => {
      counter.value++;
    };
    const decrement = () => {
      counter.value--;
    };

    return {
      name,
      counter,
      increment,
      decrement,
    };
  },
};
</script>

4.2 reactive API

reactive() 函数接收一个普通对象,返回一个响应式的数据对象, 想要使用创建的响应式数据也很简单,创建出来之后,在setup中return出去,直接在template中调用即可

<template>
  <div>
    <div class="home">
      姓名:{{ msg }} 年龄:{{ age }}
      <button @click="add">点我年龄加1</button>
    </div>
  </div>
</template>

<script lang="ts">
import { reactive,toRefs } from "vue";

export default {
  name: "HelloWorld",
  setup() {
    const msg = "王苏";
    //设置动态数据,相当于vue2.0中在data中存放数据
    let state = reactive({
      age: 18,
    });
    function add():void {
      state.age += 1;
    }
    //返回的数据会是响应式数据
    return { msg,...toRefs(state),add };
  },
};
</script>

4.2.1 isReactive

可以检查对象是否是由 reactive创建的响应式代理:

import { reactive, isReactive } from 'vue'
export default {
  setup() {
    const num = reactive({
    age:20
    })
    console.log(isReactive(num)) // -> true
  }
}

4.2.2 readonly

修改响应式对象的只读性,它返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不需要对其进行修改)

在开发中常见的readonly方法会传入三个类型的参数:

  • 类型一:普通对象;

  • 类型二:reactive返回的对象;

  • 类型三:ref的对象;

    <script>
    export default {
      setup() {
        // readonly通常会传入三个类型的数据
        // 1.传入一个普通对象
        const info = {
          name: "why",
          age: 18
        }
        const state1 = readonly(info)
    
        console.log(state1);
    
        // 2.传入reactive对象
        const state = reactive({
          name: "why",
          age: 18
        })
        const state2 = readonly(state);
    
        // 3.传入ref对象
        const nameRef = ref("why");
        const state3 = readonly(nameRef);
    
        return {
          state2,
          changeName
        }
      }
    }
    </script>
    

4.3 ref API

reactive API对传入的类型是有限制的,它要求我们必须传入的是数组和对象,当我们传入一个基本数据类型或函数,浏览器会爆出一个‘value cannot be made retive:xxx’警告:

  • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
  • 它内部的值是在ref的 value 的属性中被维护的;
<template>
  <div>
    <h2>{{ name}}</h2>
    <button @click="changeName">切换名字</button>
  </div>
</template>

<script>
import { ref } from "vue";

export default {
  setup() {
    const name = ref("ws");
    const changeName = () => {
      //通过value可以拿到name
     name.value ==="ws"?name.value='qy':name.value='ws'
    };
    return {
      name,
      changeName
    };
  },
};
</script>

4.4 torefs

如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改解构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的:

<script>
  import { ref, reactive } from 'vue';

  export default {
    setup() {
      const state = reactive({
        name: "ws",
        age: 20
      });

      //const { name, age } = state;
        //这里使用toRefs包裹可以解构出响应式对象
        const { name, age } = toRefs(state);

      const changeName = () => state.name = "ws";

      return {
        name,
        age,
        changeName
      }
    }
  }
</script>

五.组件

5.1 teleport

传送门组件,主要用于模态框,以一种简洁的方式可以指定他里面内容的父元素

<template>
 <div>
   <button @click="modelOpen = true">弹出model</button>

   <teleport to="body">
     <div v-if="modelOpen" class="model">
       <div>
          <div class="content">
             这是一个弹出框,父元素是body
          </div>
         <button @click="modelOpen = false">关闭</button>
       </div>
     </div>
   </teleport>
 </div>
</template>

<script>
export default {
 name: "App",
 components: {},
 data() {
   return {
     modelOpen: true,
   };
 },
};
</script>
<style lang="" scoped>
.model {
 position: absolute;
 top: 0;
 left: 0;
 right: 0;
 bottom: 0;
 display: flex;
 flex-direction: column;
 align-items: center;
 justify-content: center;
 background-color: rgba(0, 0, 0, 0.5);
}
.model>div {
 display: flex;
 flex-direction: column;
 justify-content: center;
 align-items: center;
 background-color: #fff;
 width: 300px;
 height: 300px;
 padding: 5px;
border-radius: 10px;
}
.content{
 width: 250px;
 height: 100px;
}
</style>

5.2 Fragments

Fragments使得在组件中不需要在使用唯一跟组件,vue3.0支持多根组件

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

5.3 Emits

如果组件中存在自定义事件,官方建议加入Emits,如果自定义事件(子传父)与原生事件命名一致就会导致事件触发两次,因此需要加入emits项

//子组件
<template>
  <div @click="$emit('click')">点击</div>
</template>

<script>
export default {
  emits: ['click'],
  name: "",
  data() {
    return {};
  },
  components: {},
};
</script>

<style></style>
//父组件
<template>
  <div>

<Emits @click='onClick'/>
  </div>
</template>
<script>
import Emits from "./components/Emits.vue"
export default {
  name: "App",
  components: {Emits},
  methods: {
    onClick(){
    //没有emits打印两次,有emits打印一次
      console.log('fffff')
    }
  },
};
</script>

5.4 model

在vue2.0中v-model与.sync修饰符有重复,因此在vue3.0中删除了sync,统一为v-model的参数形式

//父组件
<template>
  <div>
  <Model v-model='counter'></Model>
  </div>
</template>

<script>
import Model from "./components/Model.vue"
export default {
  name: "App",
  components: {Model},
  data() {
    return {
      counter:1
    };
  },
};
</script>
//子组件
<template>
  <div @click="$emit('update:modelValue', modelValue + 1)">
    counter:{{ modelValue }}
  </div>
</template>

<script>
export default {
  name: "Model",
  props: {
    modelValue: {
      type: Number,
      default: 0,
    },
  },

  },
};
</script>

六.扩展

6.1 自定义渲染器(custom renderer)

在编译之前以什么方式将虚拟dom转化为真实dom,由我们自己定义,渲染到不同平台上,可以实现一些跨平台的操作,像react中的taro,vue中uniapp的跨端操作都是建立在自定义渲染器的基础上的。

//CavansApp.vue
<template>
  <piechart
    @click="handleClick"
    :data="state.data"
    :x="200"
    :y="200"
  ></piechart>
</template>

<script>
import { reactive, ref } from "vue";
export default {
  setup(props) {
    const state = reactive({
      data: [
        { name: "老师", count: 200, color: "red" },
        { name: "医生", count: 100, color: "skyblue" },
        { name: "护士", count: 300, color: "green" },
        { name: "律师", count: 50, color: "brown" },
        { name: "警察", count: 50, color: "orange" },
      ],
    });

    const handleClick = () => {
      state.data.push({
        name: "其他",
        count: 30,
        color: "gray",
      });
    };
    return{
        state,
        handleClick
    }
  },
};
</script>

<style></style>
//main.js
import { createApp, createRenderer } from "vue";
import App from "./App.vue";
import CanvasApp from "./components/CavansApp.vue";
import "./index.css";

createApp(App).mount("#app");

let ctx, canvas;
//自定义渲染器
const nodeOptions = {
  //处理元素的创建逻辑
  createElement(tag) {
    //创建虚拟节点
    return { tag };
  },
  //处理元素的插入逻辑
  insert(child, parent, anchor) {
    //1.如果子元素不是真实dom,只需要把数据保存在虚拟对象上
    //保存关系
    child.parent = parent;
    if (!parent.child) {
      parent.child = [child];
    } else {
      parent.child.push(child);
    }
    //2.如果这里传入的是真实的dom,这里是canvas,需要绘制
    //在当前节点上,当前操作的子元素的父元素是个文本节点
    if (parent.nodeType == 1) {
      draw(child);

      //事件处理
      if (child.onClick) {
        canvas.addEventListener("click", function () {
          child.onClick();
          //数据发生变化是一个异步的过程
          setTimeout(() => {
            draw(child);
          }, 0);
        });
      }
    }
  },
  //属性更新,执行时间比较早
  patchProp(el, key, preValue, nextValue) {
    el[key] = nextValue;
  },
};
const renderer = createRenderer(nodeOptions);

const app = renderer.createApp(CanvasApp);

//绘制画布操作
const draw = (el,noClear) => {
 if (!noClear) {
   ctx.clearRect(0, 0, canvas.width, canvas.height)
}
 if (el.tag == 'piechart') {
   let { data, r, x, y } = el;
   let total = data.reduce((memo, current) => memo + current.count, 0);
   let start = 0,
       end = 0;
   data.forEach(item => {
     end += item.count / total * 360;
     drawPieChart(start, end, item.color, x, y, r);
     drawPieChartText(item.name, (start + end) / 2, x, y, r);
     start = end;
  });
}
 el.childs && el.childs.forEach(child => draw(child,true));
}

const d2a = (n) => {
 return n * Math.PI / 180;
}
const drawPieChart = (start, end, color, cx, cy, r) => {
 let x = cx + Math.cos(d2a(start)) * r;
 let y = cy + Math.sin(d2a(start)) * r;
 ctx.beginPath();
 ctx.moveTo(cx, cy);
 ctx.lineTo(x, y);
 ctx.arc(cx, cy, r, d2a(start), d2a(end), false);
 ctx.fillStyle = color;
 ctx.fill();
 ctx.stroke();
 ctx.closePath();
}
const drawPieChartText = (val, position, cx, cy, r) => {
 ctx.beginPath();
 let x = cx + Math.cos(d2a(position)) * r/1.25 - 20;
 let y = cy + Math.sin(d2a(position)) * r/1.25;
 ctx.fillStyle = '#000';
 ctx.font = '20px 微软雅黑';
 ctx.fillText(val,x,y);
 ctx.closePath();
}

//扩展mount:创建画布元素
function createCanvasApp(App) {
  const App = renderer.createApp(App);
  const mount = app.mount;
  app.mount = () => {
    //创建画布,插入画布
    canvas = document.createElement("canvas");
    ctx = canvas.getContext("2d");
    //设置画布基础属性
    canvas.width = 600;
    canvas.height = 600;
    document.querySelector(selector).appendChild(canvas);

    //执行默认的mount
    mount(canvas)
  };
}

//执行创建
createCanvasApp(CanvasApp).mount("#demo")

6.2 全局API(instance api)

全局提供了一个component可以在全局还挂载组件

//main.js
import { createApp, h } from "vue";
import App from "./App.vue";
// import CanvasApp from "./components/CavansApp.vue";
import "./index.css";
//这里定义一个全局的ws组件,在任意地方都可以使用
createApp(App).component('ws',{
    render(){
        return h('h1','我是王苏')
    }
}).mount("#app");
//任意组件
<template>
  <div>
    <ws></ws>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {

  },
  methods: {

  },
};
</script>

6.3 树摇优化

在vue2.0中global API 是作为静态函数挂载在构造函数上,像vue.nextTick(),webpack中有tree-shaking,但是vue没有。因此就会造成代码沉余,因此vue3.0使用摇树优化

受影响的api:

  • Vue.nextTick

  • Vue.observable

  • Vue.version

  • Vue.set

  • Vue.delete

  • Vue.compile

    import {nextTick}from 'vue'
    nextTick(()=>{
    //这里进行一些操作,获取最新的dom
    })
    

6.4 渲染函数API

与vue的2.0相比,3.0的渲染函数变得更为简单

  • 不需要传入h函数,通过vue自身去导入
  • 不会造成属性嵌套,让props结构更加平整
  • 作用域插槽删掉了,以前通过this.scopedSlots访问,现在通过slot的方式去调用它的内部函数来返回组件插槽内容

6.5 使用jsx

实际上 vue-cli 建立的 vue3 项目中已经内置了 jsx 的这个编译插件,如果你不需要自定义配置,是不需要安装就可以即开即用 jsx 的。

安装 jsx-next

    yarn add -D @vue/babel-plugin-jsx

新建babel.config.js

module.exports = {
  // 这是原来的预设,cli 搭建项目就有的
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  // 需要配置的插件
  plugins: ['@vue/babel-plugin-jsx']
}

使用

import { defineComponent, ref } from 'vue'

const Home = defineComponent({
  name: 'Home',
  setup (props) {
    const input = ref(null)

    const click = (e) => {
      console.log(e)
      console.log(input.value)
    }
    return {
      click,
      input
    }
  },
  render () {
    return (
      <>
        <div>使用jsx</div>
        <button onClick={this.click}>点击</button>
        <input v-model={this.input} placeholder="请输入内容"/>
      </>
    )
  }

})

export default Home

6.6 异步组件

vue3.0中的函数式组件必须定义为纯函数,异步组件定义时有如下变化:

  • 必须明确使用defineAsyncComponent包裹

  • component选项重名为loader

  • Loader函数不在接受resolve and reject且必须返回一个Promise ```vue

```

七.资料

7.1 相关学习文章

7.2 ui组件库