文章目录

  • template 的变化
  • 新增语法
    • setUp
    • ref
    • reactive
    • toRefs
  • 生命周期的变化
  • watch 的变化
  • computed 的变化
  • 新增标签
    • Teleport
    • Suspense
  • 自定义 HOOKS
  • 结合 TypeScript 开发组件

template 的变化

在 Vue2 中,每个 template 节点只能有一个根节点:

<template>
  <div>123</div>
</template>复制代码

而在 Vue3 中,可以有多个根节点:

<template>
  <div>123</div>
  <div>456</div>
</template>复制代码

这种变化,让我们在开发过程中,减少了不必要html标签的书写。例如下面这个例子:

<template>
  <table>
    <tr>
      <columns />
    </tr>
  </table>
</template>复制代码

在这里,<columns />组件需要返回多个<td>元素。如果在<columns />组件中使用了父 div:

<template>
  <div>
    <td>Hello</td>
    <td>World</td>
  </div>
</template>复制代码

得到一个<Table />输出:

<template>
  <table>
    <tr>
      <div>
        <td>Hello</td>
        <td>World</td>
      </div>
    </tr>
  </table>
</template>复制代码

而这段生成的 HTML 是无效的。Vue3 中 Template 的变化解决了这个问题。

新增语法

setUp

setUp()可以代替了原来的data(),且只执行一次。

setUp()可以接收两个参数,props和context(没有用到的时候可以省略不写)。

<template>
  <div>
    <h1>num:{{ num }}</h1>
  </div>
  <button @click="add">加1</button>
</template>

<script lang="js">
export default {
  name: 'App',
  setup() {
    let num = 0;
    const add = () => {
      num++;
    };
    console.log(num);
    return {
      add,
      num,
    };
  },
};
</script>复制代码

上述例子是一个基本的setup的过程。但我们试着点击一下加1按钮,会发现num并没有发生改变。看一下控制台,发现输出的是一个number的值,但我们点击并没有发生改变。为了解决这个问题,我们将用到一个新的 API:Ref。

ref

我们改造下上面的代码:

<template>
  <div>
    <h1>msg:{{ msg }}</h1>
    <h1>num:{{ num }}</h1>
  </div>
  <button @click="add">加1</button>
</template>

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

export default {
  name: 'App',
  setup() {
    const msg = ref(0);
    let num = 0;
    const add = () => {
      msg.value++;
      num++;
    };
    console.log(msg);
    console.log(num);
    return {
      msg,
      add,
      num,
    };
  },
};
</script>复制代码

先看下ref的导入方式,可以看到这里是按需导入。我们再看下这里的控制台的输出,就会发现:msg打印是RefImpl,num打印是数值0。点击按钮发现,msg可以改变,而num不行。

RefImpl是什么呢?我把它理解为代理对象。就比如我们知道Vue2中data的数据是通过Object.defineProperty()来进行拦截。从而达到数据响应式的目的。而Vue3是利用了ES6中的proxy达到数据响应式的目的。

关于proxy的使用,我举一个简单的例子:

const target = {  message1: "hello",  message2: "everyone"};const handler = {  get: function(target, prop, receiver) {return "world";
  }
};const proxy = new Proxy(target, handler);console.log(proxy.message1); // worldconsole.log(proxy.message2); // world复制代码

ref是一个函数,它接受一个参数,返回的就是一个响应式对象。例子中,我们初始化的这个0作为参数包裹到这个对象中去,在未来操作这个值的时候,可以检测到改变并作出对应的相应。

reactive

创建一个对象的反应式状态,就要使用reactive方法。示例代码如下:

<template>
  <div>
    <h1>count:{{ obj.count }}</h1>
    <h1>double:{{ obj.double }}</h1>
  </div>
  <button @click="obj.increase">加1</button>
</template>

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

export default {
  name: 'App',
  setup() {
    const obj = reactive({
      count: 0,
      increase: () => {
        obj.count++;
      },
      double: computed(() => obj.count * 2), // 这里是computed在vue3中的用法
    });
    return {
      obj,
    };
  },
};
</script>复制代码

这时候我们点击按钮就可以改变状态了。但像{{obj.count}}这样写的时候有些繁琐,这里我们可以解构下obj:

<template>
  <div>
    <h1>count:{{ count }}</h1>
    <h1>double:{{ double }}</h1>
  </div>
  <button @click="increase">加1</button>
</template>

<script lang="js">
import { reactive, computed } from 'vue';

export default {
  name: 'App',
  setup() {
    const obj = reactive({
      count: 0,
      increase: () => {
        obj.count++;
      },
      double: computed(() => obj.count * 2),
    });
    return {
      ...obj,
    };
  },
};
</script>复制代码

但这时我们点击按钮,会发现状态改变不了,为什么呢?这是因为,解构会破坏代理,把他变成一个普通值。就跟上面的ref的例子一样,所以点击按钮并没有发生变化。这时候,就要请出另外一个新加的api了,toRefs:

toRefs

使用起来 比较简单,就返回的时候加上就可以:

<!-- html和上面一样的,省略了。 -->

<script lang="js">
import { reactive, computed, toRefs } from 'vue';

export default {
  name: 'App',
  setup() {
    const obj = reactive({
      count: 0,
      increase: () => {
        obj.count++;
      },
      double: computed(() => obj.count * 2),
    });
    return {
      ...toRefs(obj),
    };
  },
};
</script>复制代码

toRefs从组合函数返回反应对象时,此函数很有用,以便使用组件可以对返回的对象进行解构/扩展而不会失去反应性。

生命周期的变化

Vue2Vue3
beforeCreatebeforeCreate,use setup()
createdcreated,use setup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

示例:

<script lang="js">
import { onMounted } from 'vue';

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Componentis mounted!');
    });
  },
};
</script>复制代码

watch 的变化

watch 在vue3中的用法与在vue2中的用法类似。

computed 的变化

computed 在vue3中的用法与在vue2中的用法类似。但在vue3中,可以写在reactive内部,也可写在外部。

watch和computed的用法如下:

<script lang="js">
import { reactive, computed, watch, toRefs } from 'vue';

export default {
  setup() {
    const data = reactive({
      count: 0,
      increase: () => {
        data.count++;
      },
      double: computed(() => data.count * 2),
    });
    const conComputed = computed(() => data.count * 2);
    const number = ref(0);
    watch(data, () => {
      console.log(data);
      document.title = 'updated ' + data.count;
    });
    watch(number, () => {
      console.log(number);
    });
    return {
      number,
      conComputed,
      ...toRefs(data),
    };
  },
};
</script>复制代码

在vue3中,watch和computed都要先引入才能使用。

新增标签

Teleport

平时我们的遮罩层都存在于某个多级标签下面,这样其实是不合理的。Teleport的出现可以让我们写的组件移动到指定标签下面。to是要移动到哪个标签下,它支持选择器。

示例代码如下:

<template>
  <teleport to="#modal">
    <div id="center">
      <h1>this is a modal</h1>
    </div>
  </teleport>
</template>

<script lang="js">
export default {
  name: 'modal',
};
</script>

<style scoped>
#center {
  width: 200px;
  height: 200px;
  background: red;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style>复制代码
<template>
  <div id="modal">
    <div>
      <div>
        <modal v-if="show"></modal>
      </div>
    </div>
  </div>
  <button @click="show = !show">show</button>
</template>

<script lang="js">
import { ref } from 'vue';
import Modal from './components/modal.vue';

export default {
  name: 'App',
  components: {
    Modal,
  },
  setup() {
    const show = ref(false);
    return {
      show,
    };
  },
};
</script>复制代码

点击按钮之前:

Vue3 学习笔记_Vue3

点击按钮之后:

Vue3 学习笔记_Vue3_02

观察点击按钮前后的DOM结构,我们会发现,即使modal组件在div#modal下被嵌套了多层div后才使用,当显示时,它依然移动到div#modal下面。

Suspense

Suspense 在异步请求的场景下是很实用的。示例如下:

<template>
  <h1>I have some async work to do before I can render</h1>
</template>

<script>
export default {
  name: 'MyAsyncComponent',
  async setup() {
    await someAsyncWork();
  }
 }
 </script>复制代码
<template>
  <slot v-if="error" name="error"></slot>
  <Suspense v-else>
    <template #default>
      <slot name="default"></slot>
    </template>
    <template #fallback>
      <slot name="fallback"></slot>
    </template>
  </Suspense>
</template>

<script>
import { ref, onErrorCaptured } from 'vue'

export default {
  name: 'SuspenseWithError',
  setup() {
    const error = ref(null);
    onErrorCaptured((e) => {
      error.value = e;
      return true;
    });
    return { error };
  }
}
</script>复制代码
<template>
  <SuspenseWithError>
    <template #default>
      <MyAsyncComponent />
    </template>
    <template #fallback>
      <span>Loading... Please wait.</span>
    </template>
    <template #error>
      <h1>I failed to load</h1>
    </template>
  </SuspenseWithError>
</template>

<script>
import MyAsyncComponent from '@/components/MyAsyncComponent.vue';
import SuspenseWithError from '@/components/SuspenseWithError.vue';

export default {
  name: 'App',
  components: { MyAsyncComponent, SuspenseWithError },
}
</script>复制代码

自定义 HOOKS

示例如下:

<script lang="ts">import { onMounted, onUnmounted, ref } from 'vue';const useMousePosition = () => {  const x = ref(0);  const y = ref(0);  const updateMouse = (e: MouseEvent) => {
    x.value = e.pageX;
    y.value = e.pageY;
  };

  onMounted(() => {document.addEventListener('mousemove', updateMouse);
  });

  onUnmounted(() => {document.removeEventListener('mousemove', updateMouse);
  });  return { x, y };
};export default useMousePosition;
</script>复制代码
<script lang="js">
import useMousePosition from '@/hooks/useMousePosition';

export default {
  setup() {
    const { x, y } = useMousePosition();
    return {
      x,
      y,
    };
  },
};
</script>复制代码

结合 TypeScript 开发组件

在vue3中,创建组件需要用defineComponent包裹。

示例如下:

<script lang="ts">import { defineComponent, PropType, computed } from 'vue';// 把接口类型导出。在使用的过程中导入接口,对接口进行定义export interface ColumnProps {  id: number;
  title: string;
  avatar?: string;
  des: string;
}export default defineComponent({  name: 'ColumnList',  props: {list: {      type: Array as PropType<ColumnProps[]>,
      required: true,
    },
  },  setup(props) {//  这里使用到了propsconst ColumnList = computed(() => {      return props.list.map((item) => {if (!item.avatar) {
          item.avatar = require('@/assets/logo.png'); // 默认图片}return item;
      });
    });return {
      ColumnList,
    };
  },
});
</script>复制代码
<template>
  <div id="container">
    <column-list :list="list"></column-list>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import ColumnList, { ColumnProps } from '@/components/column-list.vue';

const testDate: ColumnProps[] = [
  {
    id: 1,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200',
  },
  {
    id: 2,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200',
  },
  {
    id: 3,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200',
  },
  {
    id: 4,
    title: 'test1',
    des: 'test1',
    avatar: 'https://picsum.photos/id/239/200/200',
  },
];
export default defineComponent({
  name: 'App',
  components: {
    ColumnList,
  },
  setup() {
    return {
      list: testDate,
    };
  },
});
</script>复制代码