Vue3

新功能

原理

Vite

面试题

Vue3 比 Vue2 有什么优势

Vue3 生命周期

选项式 API 组合式 API
beforeCreate 不需要(直接写到 setup 函数中)
created 不需要(直接写到 setup 函数中)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy Vue 3:beforeUnmount onBeforeUnmount
destroyed Vue 3: unmounted onUnmounted
errorCaptured onErrorCaptured
activated onActivated
deactivated onDeactivated

composition VS options

composition API 优点:

如何选择:

选项式 APIOptions API

<template>
  <h1 @click="changeCount">{{ count }}</h1>
</template>
<script>
export default {
  name: 'App',
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    changeCount() {
      this.count++
    },
  },
}
</script>

组合式 APIComposition API

<template>
  <h1 @click="changeNum">{{ num }}</h1>
</template>
<script>
import { ref } from 'vue'
function useNum() {
  const num = ref(0)
  function changeNum() {
    num.value++
  }
  return { changeNum, num }
}
export default {
  name: 'App',
  setup() {
    const { changeNum, num } = useNum()
    return {
      changeNum,
      num,
    }
  },
}
</script>

如何理解 ref toRef 和 toRefs

ref

<template>
  <p>值类型响应式:{{ ageRef }} {{ state.name }}</p>
  <p ref="elemRef">templateRef</p>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
  name: 'Ref',
  setup() {
    const ageRef = ref(20)
    const nameRef = ref('cat')
    const elemRef = ref('elemRef')
    const state = reactive({
      name: nameRef,
    })
    setTimeout(() => {
      ageRef.value = 30 // .value 修改值
      nameRef.value = 'dog'
    }, 1500)
    onMounted(() => {
      console.log(elemRef.value)
    })
    return {
      ageRef,
      state,
      elemRef,
    }
  },
}
</script>

toRef

<template>
  <p>{{ ageRef }} {{ state.age }}</p>
</template>
<script>
import { toRef, reactive } from 'vue'
export default {
  name: 'ToRef',
  setup() {
    const state = reactive({
      age: 20,
      name: 'cat',
    })
    const ageRef = toRef(state, 'age')
    setTimeout(() => {
      state.age = 25
    }, 1000)
    setTimeout(() => {
      ageRef.value = 30
    }, 3000)
    return { state, ageRef }
  },
}
</script>

toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式

const state = {
  age: 20,
  name: 'cat',
}

toRefs

注意:直接解构 reactive 返回的 state ,页面能显示内容,但内容不是响应式的

<template>
  <p>{{ age }} {{ name }}</p>
</template>
<script>
import { toRefs, reactive } from 'vue'
export default {
  name: 'ToRef',
  setup() {
    const state = reactive({
      age: 20,
      name: 'cat',
    })
    // 将响应式对象,变为普通对象
    const stateAsRefs = toRefs(state)
    setTimeout(() => {
      state.age = 25
    }, 1000)
    // const { age: ageRef, name: nameRef } = stateAsRefs
    return { ...stateAsRefs }
  },
}
</script>

ref toRef 和 toRefs的最佳使用方式

为何需要 ref

<template>
  <p>{{ age }}</p>
  <p>{{ age1 }}</p>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
  name: 'WhyRef',
  setup() {
    const state = reactive({
      age: 20,
      name: 'dog',
    })
    // computed返回的是一个类似于ref的对象,也有.value
    const age1 = computed(() => {
      return state.age + 1
    })
    setTimeout(() => {
      state.age = 25
    }, 1000)
    return {
      ...state, // 这样不是响应式的
      age1,
    }
  },
}
</script>

为何需要 .value

简单理解 computed 运算逻辑

// 错误
function computed(getter) {
  let value
  watchEffect(() => { // 可以改为setTimeout进行模拟测试
    value = getter()
  })
  return value
}
// 正确
function computed(getter) {
  const ref = {
    value: null,
  }
  watchEffect(() => {
    ref.value = getter()
  })
  return ref
}

为何需要 toRef 和 toRefs

Vue3 升级了哪些重要功能

Vue3 官网

createApp

// vue2.x
const app = new Vue({ /* ... */ })
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
// vue3
const app = Vue.createApp({ /* ... */ })
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

emits 属性

<template>
  <son :msg="msg" @onSayHello="sayHello" />
</template>
<script>
import Son from './views/Son.vue'
export default {
  components: { Son },
  data() {
    return {
      msg: 'hello vue3',
    }
  },
  methods: {
    sayHello(info) {
      console.log('hello', info)
    },
  },
}
</script>
<template>
  <p>{{ msg }}</p>
</template>
<script>
export default {
  props: {
    msg: String,
  },
  emits: ['onSayHello'],
  setup(props, { emit }) {
    emit('onSayHello', 'vue3')
  },
}
</script>

多事件处理

<button @click="one($event), two($event)">Submit</button>

Fragment

<!-- vue2.x组件模板 -->
<template>
  <div>
    <p>{{ msg }}</p>
    <p>{{ content }}</p>
  </div>
</template>
<!-- vue3组件模板 -->
<template>
  <p>{{ msg }}</p>
  <p>{{ content }}</p>
</template>

移除 .sync

<!-- vue2.x -->
<MyComponent :title.sync="title" />
<!-- vue3.x -->
<MyComponent v-model:title="title" />

异步组件

// vue2.x
new Vue({
  components: {
    'my-component': () => import('./async-com.vue'),
  },
})
// vue3.x
import { createApp, defineAsyncComponent } from 'vue'
createApp({
  components: {
    AsyncComponent: defineAsyncComponent(() => import('./async-com.vue')),
  },
})

移除 filter

<!-- vue2.x -->
<div>{{ message | capitalize }}</div>
<div :id="rawId | formatId">/div>

Teleport

可以把一些组件放到外面去,Vue2 只能操作 DOM 来实现,Vue3 可以通过 Teleport 来实现

<button @click="modalOpen = true">Open full screen modal!</button>
<teleport to="body">
  <div v-if="modalOpen" class="modal">
    <div>
      telePort 弹窗(父元素是 body)
      <button @click="modalOpen = false">Close</button>
    </div>
  </div>
</teleport>

Suspense

场景:比如一个列表,一刷新就需要加载数据,在数据未加载之前会显示 loading,加载完之后在显示列表数据

Vue2 这种场景一般会写一个 data 控制显示隐藏,Element UI 库对此进行封装,Vue3 自己做了一个封装,封装成 Suspense

<Suspense>
  <template>
    <!-- 是一个异步组件 -->
    <Test1 />
  </template>
  <!-- #fallback 就是一个具名插槽。即Suspense组件内部,有两个slot,其中一个具名为fallback -->
  <template #fallback> Loading.. </template>
</Suspense>

Composition API

Composition API 实现逻辑复用

<template>
  <p>mouse position {{ x }} {{ y }}</p>
</template>
<script>
import useMousePosition from './useMousePosition'
export default {
  name: 'MousePosition',
  setup() {
    const { x, y } = useMousePosition()
    return {
      x,
      y,
    }
  },
}
</script>
import { ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}
export default useMousePosition

如果不使用 ref 使用 reactive,需要将整个 reactive 暴露出去。在父组件接收的时候不能直接解构,否则会失去响应式

Vue3 如何实现响应式

Object.defineProperty 的缺点:

Object.defineProperty(target, key, {
  get() {
    return value
  },
  set(newValue) {
    if (newValue !== value) {
      observer(newValue) // 值修改后进行监听
      // value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
      value = newValue
      updateView() // 触发更新视图
    }
  },
})

Proxy 实现响应式优点

性能是如何提升的

Proxy 实现不是一次性监听的,这里深度监听是在 get 中处理的,什么时候用到什么时候处理(惰性)。而 Object.defineProperty 实现是在一开始就进行处理,一次性全部处理完成

// 测试数据
const data = {
  name: 'bird',
  age: 20,
  info: {
    city: 'beijing',
  },
}
function reactive(target = {}) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则返回
    return target
  }
  // 代理配置
  const proxyConf = {
    get(target, key, receiver) {
      // 只处本身(非原型)属性
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key) // 监听
      }
      const result = Reflect.get(target, key, receiver)
      // 惰性深度监听。什么时候用什么时候监听
      return reactive(result) // 返回结果
    },
    set(target, key, val, receiver) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true
      }
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('已有的key', key)
      } else {
        console.log('新增的key', key)
      }
      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      return result // 是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      return result // 是否删除成功
    },
  }
  // 生成代理对象
  const observed = new Proxy(target, proxyConf)
  return observed
}
const proxyData = reactive(data)

Reflect 的作用:

'a' in obj -> Reflect.has(obj, 'a')

delete obj.b -> Reflect.deleteProperty(obj, 'b')

总结

v-model 参数用法

Vue2 的 .sync 修饰符

<text-document v-bind:title.sync="doc.title" />
<!-- 语法糖 -->
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
/>

Vue3 的 v-model

<ChildComponent :title.sync="pageTitle" />
<!-- 替换为 -->
<ChildComponent v-model:title="pageTitle" />

v-model 相当于传递了 modelValue prop 并接受抛出的 update:modelValue 事件

<template>
  <p>{{ name }} {{ age }}</p>
  <user-info v-model:name="name" v-model:age="age" />
</template>
<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './UserInfo.vue'
export default {
  components: { UserInfo },
  setup() {
    const state = reactive({
      name: 'bird',
      age: '20',
    })
    return toRefs(state)
  },
}
</script>
<template>
  <input :value="name" @input="$emit('update:name', $event.target.value)" />
  <input :value="age" @input="$emit('update:age', $event.target.value)" />
</template>
<script>
export default {
  props: {
    name: String,
    age: String,
  },
}
</script>

watch 和 watchEffect 的区别

<template>
  <p>{{ numberRef }}</p>
  <p>{{ name }} {{ age }}</p>
</template>
<script>
import { reactive, toRefs, ref, watch, watchEffect } from 'vue'
export default {
  setup() {
    const numberRef = ref(10)
    const state = reactive({
      name: 'bird',
      age: 20,
    })
    watchEffect(() => {
      // 初始化时,一定会执行一次(收集需要监听的数据)
      console.log('watchEffect', state.age)
    })
    watchEffect(() => {
      console.log('watchEffect', numberRef)
    })
    // watch监听ref属性
    watch(numberRef, (newNum, oldNum) => {
      console.log('ref watch', newNum, oldNum)
    })
    // watch监听state属性
    watch(
      // 1.确定监听哪个属性
      () => state.age,
      // 2.回调函数
      (newState, oldState) => {
        console.log('state watch', newState, oldState)
      },
      // 3.配置项
      {
        immediate: true, // 初始化之前就监听
        // deep: true // 深度监听
      }
    )
    setTimeout(() => {
      numberRef.value = 100
    }, 1000)
    setTimeout(() => {
      state.age = 25
    }, 1500)
    return { numberRef, ...toRefs(state) }
  },
}
</script>

setup 中如何获取组件实例

<template>
  <p>getInstance</p>
</template>
<script>
import { getCurrentInstance, onMounted } from 'vue'
export default {
  data() {
    return {
      x: 1,
      y: 2,
    }
  },
  setup() { // created beforeCreate 组件还没有正式初始化
    console.log('setup this', this) // undefined
    onMounted(() => {
      console.log('onMounted this', this) // undefined
      console.log('x', instance.data.x) // 1
    })
    const instance = getCurrentInstance()
    console.log('instance', instance) // 组件实例
    console.log('x', instance.data.x) // undefined
  },
  mounted() {
    console.log('mounted this', this) // Proxy 实例
    console.log('y', this.y) // 2
  },
}
</script>

Vue3 为何比 Vue2 快

PatchFlag(标记)

Vu3 在线编译

Vue2 在线编译

Vue2 和 Vu3 diff 算法比较

Vue3 一些面试题

<div>
  <div>1</div>
  <div>2</div>
  <div>{{ name }}</div>
</div>
<script>
export function render() {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('div', null, '1'),
      _createVNode('div', null, '2'),
      _createVNode('div', null, _toDisplayString(_ctx.name), 1 /* TEXT */),
    ])
  )
}
</script>

hoistStatic(静态提升)

Vue2 无论元素是否参与更新,每次都会重新创建然后再渲染。Vue3 对于不参与更新的元素,做静态提升,只会被创建一次,在渲染时直接复用即可

<div>
  <div>1</div>
  <div>2</div>
  <div>{{ name }}</div>
</div>
<script>
const _hoisted_1 = /*#__PURE__*/ _createVNode('div', null, '1', -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/ _createVNode('div', null, '2', -1 /* HOISTED */)
export function render() {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _hoisted_1,
      _hoisted_2,
      _createVNode('div', null, _toDisplayString(_ctx.name), 1 /* TEXT */),
    ])
  )
}
</script>

cacheHandler(缓存事件)

Vue2 绑定事件每次触发都要重新生成全新 Function 去更新。Vue3 提供事件缓存对象,当开启 cacheHandler 会自动生成一个内联函数,同时生成一个静态节点,当事件再次触发时,只需从缓存中调用即可

<div>
  <div @click="todo">something</div>
</div>
<script>
export function render() {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode(
        'div',
        {
          onClick: _cache[1] || (_cache[1] = (...args) => _ctx.todo(...args)),
        },
        'something'
      ),
    ])
  )
}
</script>

SSR 渲染

tree shaking(按需编译)

Vite

Vite 为何启动快

CommonJs

「万字进阶」深入浅出 Commonjs 和 Es Module

CommonJs 实现原理

;(function (exports, require, module, __filename, __dirname) {
  const sayName = require('./hello.js')
  module.exports = function say() {
    return {
      name: sayName(),
      author: '我不是外星人',
    }
  }
})
function wrapper(script) {
  return '(function (exports, require, module, __filename, __dirname) {' + script + '\n})'
}

require 加载原理

// id 为路径标识符
function require(id) {
  /* 查找  Module 上有没有已经加载的 js  对象*/
  const cachedModule = Module._cache[id]
  /* 如果已经加载了那么直接取走缓存的 exports 对象  */
  if (cachedModule) {
    return cachedModule.exports
  }
  /* 创建当前模块的 module  */
  const module = { exports: {}, loaded: false }
  /* 将 module 缓存到  Module 的缓存属性中,路径标识符作为 id */
  Module._cache[id] = module
  /* 加载文件 */
  runInThisContext(wrapper('module.exports = "123"'))(
    module.exports,
    require,
    module,
    __filename,
    __dirname
  )
  /* 加载完成 */
  module.loaded = true
  /* 返回值 */
  return module.exports
}

require 大致流程如下:

  1. require 会接收一个参数——文件标识符,然后分析定位文件,接下来会从 Module 上查找有没有缓存,如果有缓存,那么直接返回缓存的内容
  2. 如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,加载完文件后,将 loaded 设置为 true
  3. exportsmodule.exports 持有相同的引用,所以对 exports 赋值会导致 exports 操作的不再是 module.exports 的引用

ES Module

ES6 之后,JS 才有了真正意义上的模块化规范。ES Module 的优势:

基本使用

<script type="module">
  import add from './add.js'
  console.log(add(10, 20)) // 30
</script>
<script>
// add.js
export default function add(a, b) {
  return a + b
}
</script>
<script type="module">
  import { add } from './math.js'
  console.log(add(10, 20)) // 30
</script>
<script>
// math.js
export function add(a, b) {
  return a + b
}
</script>

外链使用

<script type="module">
  import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs'
  console.log(createStore) // Function
</script>

动态引入

<button id="btn1">load1</button>
<script type="module">
  document.getElementById('btn1').addEventListener('click', async () => {
    const res = await import('./math.js')
    console.log(res.add(10, 20)) // 30
  })
</script>
<script>
// math.js
export function add(a, b) {
  return a + b
}
</script>

Composition API 和 React Hooks 对比