Vue3.0全家桶最全入门指南 – vue3.0新特性 (2/4)

    |     2020年9月23日   |   vuejs, web前端技术   |     0 条评论   |    972

本篇是 Vue3 全家桶系列第 2 篇,对照 2.x 讲 vue3.0 新特性:createApp、页面与路由、Composition API(reactive / ref / readonly / computed / watch)、生命周期、Fragment、Teleport、Suspense。

系列目录:

一、实例化

2.x 用 new Vue(...);3.x 用 createApp。2.x 把 API 绑在全局 Vue 上,3.x 收到应用实例上,scope 更紧。同时移除了 Vue.config.productionTip 和 Vue.config.keyCodes。

// vue 2.x
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
Vue.prototype.customProperty = () => {}

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})
// vue 3.x
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

const app = createApp(App)

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.config.globalProperties.customProperty = () => {}

app.use(router).use(store).mount('#app')

二、创建页面

在 /src/views 新建 Test.vue:

<template>
  <div class="page-wrapper">
    <span>这是一个新页面</span>
  </div>
</template>

<script>
export default {
  name: 'Test',
  setup () {
    return {}
  }
}
</script>

在 /src/router/index.js 里挂路由:

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/test',
    name: 'Test',
    component: () => import(/* webpackChunkName: "test" */ '../views/Test.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

三、Composition API

2.x 数据在 data、方法在 methods,靠 this。3.x 逻辑都在 setup 里实现,包括 data、watch、computed、methods、hooks,不再有 this。setup 在组件生命周期里只跑一次。

OPTIONS 写法把 data / methods / computed / watch 分开放,看起来更清晰;组合式全堆在 setup,对代码组织要求更高。2.x 想用组合式可装 @vue/composition-api,用法基本一样。

① 状态:reactive 与 ref

reactive 几乎等价于 2.x 的 Vue.observable(),为避免和 RxJS 的 observable 混淆才改名。3.x 用它们取代 data:reactive 绑对象,ref 绑基础类型(内部是 { value } 再加 ref 标识)。

// vue2.x
export default {
  name: 'Test',
  data () {
    return {
      count: 0,
      num: 0
    }
  },
  methods: {
    addCount () {
      this.count++
    }
    addNum() {
      this.num++
    }
  }
}
// vue3.x
<template>
  <div class="page-wrapper">
    <div>
      <span>count 点击次数: </span>
      <span>{{ count }}</span>
      <button @click="addCount">点击增加</button>
    </div>
    <div>
      <span>num 点击次数: </span>
      <span>{{ num }}</span>
      <button @click="addNum">点击增加</button>
    </div>
  </div>
</template>

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

export default {
  name: 'Test',
  setup () {
    const state = reactive({
      count: 0
    })
    const num = ref(0)

    const addCount = function () {
      state.count++
    }
    const addNum = function () {
      num.value++
    }

    return {
      // 这样展开后state property会失去响应式,因为是取值返回,不是引用
      // ...state,
      ...toRefs(state),
      num,
      addCount,
      addNum
    }
  }
}
</script>

解开 Ref:把 ref 暴露给渲染上下文后,模板里写 {{{{ num }}}} 即可,不必 {{{{ num.value }}}};JS 里仍用 num.value。

使用 Reactive:必须保持对返回对象的引用,不能解构或展开,否则失响应。toRefs 把每个 property 转成对应 ref。

② 只读:readonly

普通 object、reactive、ref 都可 readonly 包一层。直接改只读对象,控制台告警,不抛错。源对象改了,只读视图仍会更新。

const state = reactive({
  count: 0
})
const readonlyState = readonly(state)
// 监听只读属性,state.count修改后依然会触发readonlyState.count更新
watch(() => readonlyState.count, (newVal, oldVal) => {
  console.log('readonly state is changed!')
  setTimeout(() => {
    // 修改只读属性会打印告警信息,但是不会报错
    readonlyState.count = 666
  }, 1000)
})

③ 计算属性 computed

2.x / 3.x 都支持 getter 和 setter,3.x 是组合函数:

// vue2.x
export default {
  ...
  computed: {
    totalCount() {
      return this.count + this.num
    },
    doubleCount: {
      get() {
        return this.count * 2
      },
      set(newVal) {
        this.count = newVal / 2
      }
    }
  }
}
// vue3.x
import { reactive, ref, toRefs, computed } from 'vue'

export default {
  name: 'Test',
  setup () {
    const state = reactive({
      count: 0,
      double: computed(() => {
        return state.count * 2
      })
    })
    const num = ref(0)

    const addCount = function () {
      state.count++
    }
    const addNum = function () {
      num.value++
    }

    // only getter
    const totalCount = computed(() => state.count + num.value)
    // getter & setter
    const doubleCount = computed({
      get () {
        return state.count * 2
      },
      set (newVal) {
        state.count = newVal / 2
      }
    })

    return {
      ...toRefs(state),
      num,
      totalCount,
      doubleCount,
      addCount,
      addNum
    }
  }
}

④ watch 与 watchEffect

  • 仍支持 immediate、deep,但不再支持 ‘obj.key1.key2’ 这种点分隔写法
  • 可听单个属性,也可一次听多个,比 2.x 灵活
  • watchEffect 返回停止函数
  • watchEffect 注册后立刻跑;watch 默认不跑,除非 immediate: true

往 DOM 里渲染算副作用。可用 watchEffect 按响应式状态自动重跑副作用。

// vue2.x
export default {
  ...
  data () {
    return {
      ...
      midObj: {
        innerObj: {
          size: 0
        }
      }
    }
  },
  computed: {
    totalCount() {
      return this.count + this.num
    }
  },
  watch: {
    totalCount(newVal, oldVal) {
      console.log(`count + num = ${newVal}`)
    },
    'midObj.innerObj.size': {
      // deep: true,
      immediate: true,
      handler(newVal, oldVal) {
        console.log(`this.midObj.innerObj.size = ${newVal}`)
      }
    }
  }
}
// vue3.x
import { reactive, ref, toRefs, computed, watch } from 'vue'

export default {
  name: 'Test',
  setup () {
    const state = reactive({
      count: 0,
      double: computed(() => {
        return state.count * 2
      }),
      midObj: {
        innerObj: {
          size: 0
        }
      }
    })
    const num = ref(0)

    const addCount = function () {
      state.count++
    }
    const addNum = function () {
      num.value++
    }

    // 监听单个属性
    watch(() => totalCount.value, (newVal, oldVal) => {
      console.log(`count + num = ${newVal}`)
    })
    // 监听单个属性, immediate
    watch(() => totalCount.value, (newVal, oldVal) => {
      console.log(`count + num = ${newVal}, immediate=true`)
    }, {
      immediate: true
    })
    // 监听单个属性, deep
    watch(() => state.midObj, (newVal, oldVal) => {
      console.log(`state.midObj = ${JSON.stringify(newVal)}, deep=true`)
    }, {
      deep: true
    })
    setTimeout(() => {
      state.midObj.innerObj.size = 1
    }, 2000)
    // 监听多个属性
    watch([num, () => totalCount.value], ([numVal, totalVal], [oldNumVal, OldTotalVal]) => {
      console.log(`num is ${numVal}, count + num = ${totalVal}`)
    })
    // 副作用,会立即执行
    let callTimes = 0
    const stopEffect = watchEffect(() => {
      console.log('watchEffect is called!')
      const div = document.createElement('div')
      div.textContent = `totalCount is ${totalCount.value}`
      document.body.appendChild(div)
      // 调用 5 次后,取消effect监听
      callTimes++
      if (callTimes >= 5) stopEffect()
    })

    return {
      ...toRefs(state),
      num,
      totalCount,
      addCount,
      addNum
    }
  }
}

四、生命周期钩子

2.x 钩子和 methods 同级;3.x 先 import,再在 setup 里注册,命名向 React 靠齐。去掉 beforeCreate / created,由 setup 代替。

和 React Hooks 比:组合式 setup() 只调用一次——更像普通 JS;不依赖调用顺序,可用在条件里;不会每次渲染都跑,GC 压力更小;没有内联函数导致子组件永远更新的问题,也不需要 useCallback;不必手填依赖数组,自动依赖跟踪保证侦听器和计算值准确。React Hooks 是本提案的主要灵感,Vue 响应式模型正好绕开那些设计问题。

// vue2.x
export default {
  data () {
    return {}
  },
  methods: {
    ...
  },
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeDestroy() {},
  destroyed() {}
}
// vue3.x
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

export default {
  setup() {
    onBeforeMount(() => {
      console.log('component is onBeforeMount')
    })
    onMounted(() => {
      console.log('component is onMounted')
    })
    onBeforeUpdate(() => {
      console.log('component is onBeforeUpdate')
    })
    onUpdated(() => {
      console.log('component is onUpdated')
    })
    onBeforeUnmount(() => {
      console.log('component is onBeforeUnmount')
    })
    onUnmounted(() => {
      console.log('component is onUnmounted')
    })
  }
}

五、Fragment

2.x 模板只能一个根节点;3.x 支持多根,类似 React 的 Fragment / <>。

// vue2.x
<template>
  <div>
    <span>hello</span>
    <span>world</span>
  </div>
</template>
// vue3.x
<template>
  <span>hello</span>
  <span>world</span>
</template>

六、Teleport

teleport 参照 React Portal,把元素渲染到父节点以外,例如 body 下某个节点。弹窗、tooltip 常放在 </body> 前,好处理定位和 z-index。

<body>
  <div id="app">
    <!--main page content here-->
  </div>
  <!--modal here-->
</body>

若把模态 UI 写在底部,会脱离 App 根组件,容易变成手改 JS/CSS、丢掉响应式。Teleport 可把片段挪走,组件销毁时自动清 DOM。

<body>
  <div id="app">
    <h3>Tooltips with Vue 3 Teleport</h3>
  </div>
  <div>
    <my-modal></my-modal>
  </div>
</body>

先在页面放目标节点,再把模态 teleport 过去:

<body>
  <div id="app">
    <h3>Tooltips with Vue 3 Teleport</h3>
  </div>
  <div id="endofbody"></div>
</body>
<template>
  <button @click="openModal">
    Click to open modal! (With teleport!)
  </button>
  <teleport to="#endofbody">
    <div v-if="isModalOpen" class="modal">
      ...
    </div>
  </teleport>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const isModalOpen = ref(false)
    const openModal = function () {
      isModalOpen.value = true
    }
    return {
      isModalOpen,
      openModal
    }
  }
}
</script>

七、Suspense

Suspense 先渲染回退内容,直到条件满足——常见于 setup 里的异步或异步组件。父组件要等异步子组件时,可用它做占位或失败处理。

// vue2.x
<template>
  <div>
    <div v-if="!loading">
      ...
    </div>
    <div v-if="loading">Loading...</div>
  </div>
</template>

2.x 也可用 vue-async-manager:

<template>
  <div>
    <Suspense>
      <div>
        ...
      </div>
      <div slot="fallback">Loading...</div>
    </Suspense>
  </div>
</template>
// vue3.x
<Suspense>
  <template >
    <Suspended-component />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

异步组件完全加载渲染前会显示 Loading。Fragment、Teleport、Suspense 都受 React 启发。

能力 2.x 3.x
创建应用 new Vue() createApp().mount()
全局 API 挂在 Vue 上 挂在 app 实例
状态 data() reactive / ref + setup
钩子 选项式 beforeCreate/created… setup + onMounted…
多根 不行 Fragment
传送 DOM 自己搬 Teleport
异步占位 手写 v-if / 插件 Suspense

一句话总结:Vue3 用 createApp 收紧全局 API,用 setup + Composition API 组织状态和副作用,再用 Fragment、Teleport、Suspense 补上多根、传送门和异步占位。

转载请注明来源:Vue3.0全家桶最全入门指南 – vue3.0新特性 (2/4)
本文链接地址:https://ai.zhousir.top/?p=3082
回复 取消