前端知识学习

1、实例化

2.x使用构造函数new Vue(...)创建实例,3.x使用createApp函数创建实例;

2.x所有属性方法和设置都绑定到全局Vue对象上,3.x改为绑定到vue实例下,收紧了scope;

3.x移除了 Vue.config.productionTipVue.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')

2、创建页面

在/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

3、Composition API

vue2.x中,所有的数据都在data方法中定义返回,方法定义在methods下面,并通过this调用vue3.x中,所有的代码逻辑将在setup方法中实现,包括datawatchcomputedmethodshooks,并且不再有this

vue3.x setup方法在组件生命周期内只执行一次,不会重复执行

相比vue2.x中基于OPTIONS配置的方式,vue3.x基于组合式API的方式语义没有2.x清晰,2.x中datamethodscomputedwatch等都通过不同的scope区分开,看起来很清晰,3.x都放在setup方法中,对代码组织能力会有更高的要求。

vue2.x使用Composition API可以安装@vue/composition-api,使用基本跟Composition API一样,这里不再赘述

① 状态和事件绑定 reactive & ref

reactive 几乎等价于 2.x 中的 Vue.observable() API,只是为了避免与 RxJS 中的 observable 混淆而做了重命名

vue3.x的reactiveref取代了vue2.x中的data数据定义

从下面的代码中可以看到,reactive处理的是对象的双向绑定,而ref则可以处理js基础类型的双向绑定,其实ref的实现原理也只是对基础类型进行对象化封装,再添加一个__v_isRef标识属性用来区分。

具体可看ref.ts源码,如下

class RefImpl<T> {
  private _value: T // ref 原始值

  public readonly __v_isRef = true // ref 标识

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() { // getter 收集依赖
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) { // setter 触发响应
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

vue2.x与3.x响应式写法差别:

// 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 值暴露给渲染上下文,在渲染过程中,Vue 会直接使用其内部的值,也就是说在模板中你可以把 {{ num.value }} 直接写为 {{ num }} ,但是在js中还是需要通过 num.value取值和赋值。

使用 Reactive

使用 reactive 组合函数时必须始终保持对这个所返回对象的引用以保持响应性。这个对象不能被解构或展开,一旦被解构或者展开,返回的值将失去响应式。

toRefs API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref

② 只读数据 readonly

对于不允许写的对象,不管是普通object对象、reactive对象、ref对象,都可以通过readonly方法返回一个只读对象

直接修改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中的computed都支持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

3.x和2.x的watch一样,支持immediatedeep选项,但3.x不再支持'obj.key1.key2'的"点分隔"写法;

3.x中watch支持监听单个属性,也支持监听多个属性,相比2.x的watch更灵活;

3.x中watchEffect方法会返回一个方法,用于停止监听;

watchwatchEffect不同的地方在于,watchEffect注册后会立即调用,而watch默认不会,除非显示指定immediate=true,并且watchEffect可以停止监听

在 DOM 当中渲染内容会被视为一种“副作用”:程序会在外部修改其本身 (也就是这个 DOM) 的状态。我们可以使用 watchEffect API 应用基于响应式状态的副作用,并自动进行重应用。

// 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
    }
  }
}

4、生命周期钩子

2.x中生命周期钩子放在跟methods同级属性下

3.x中需要先导入钩子,然后在setup方法中注册钩子回调,并且钩子命名也跟React保持一样了

3.x移除了2.x中的beforeCreatecreated钩子,通过setup方法代替

与 React Hooks 相比

基于函数的组合式 API 提供了与 React Hooks 同等级别的逻辑组合能力,但是它们还是有很大不同:组合式 API 的 setup() 函数只会被调用一次,这意味着使用 Vue 组合式 API 的代码会是:

一般来说更符合惯用的 JavaScript 代码的直觉;

不需要顾虑调用顺序,也可以用在条件语句中;
不会在每次渲染时重复执行,以降低垃圾回收的压力;
不存在内联处理函数导致子组件永远更新的问题,也不需要 useCallback;
不存在忘记记录依赖的问题,也不需要“useEffect”和“useMemo”并传入依赖数组以捕获过时的变量。Vue 的自动依赖跟踪可以确保侦听器和计算值总是准确无误。
我们感谢 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')
    })
  }
}

2.x钩子对比3.x

5、Fragment

2.x中,vue template只允许有一个根节点

3.x中,vue template支持多个根节点,用过React的人应该知道<React.Fragment><></>

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

--

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

6、Teleport

teleport参照React中的portal,可以将元素渲染在父节点以外的其他地方,比如<body>下面的某个子元素

在vue3中, <teleport>是一个内置标签,我们通常将弹窗、tooltip等元素放在关闭的 </body>标签之前,如下:

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

如果按照以往的思路,需要将模态的UI代码放在底部,如下:

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

这样做是因为弹窗、tooltip需要显示在页面上层,需要正确处理父元素定位和z-index上下层级顺序,而最简单的解决方案是将这类DOM放在页面的最底部。这样的话这部分逻辑就脱离了整个项目的跟组件App的管理,就造成直接用JavaScript和CSS来修改UI,不规范并且失去响应式了。为了允许将一些UI片段段移动到页面中的其他位置,在Vue3中添加了一个新的<teleport>组件,并且<teleport>会在组件销毁时自动清空相应的dom,不用人工处理。

要使用 <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>

7、Suspense

<Suspense>是一个特殊的组件,它将呈现回退内容,而不是对于的组件,直到满足条件为止,这种情况通常是组件setup功能中发生的异步操作或者是异步组件中使用。例如这里有一个场景,父组件展示的内容包含异步的子组件,异步的子组件需要一定的时间才可以加载并展示,这时就需要一个组件处理一些占位逻辑或者加载异常逻辑,要用到 <Suspense>,例如:

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

或者在vue2.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>

上面代码中,假设 <Suspended-component>是一个异步组件,直到它完全加载并渲染前都会显示占位内容:Loading,这就是 <Suspense>的简单用法,该特性和Fragment以及 一样,灵感来自React