一文读懂vue3新特性

1,403 阅读6分钟

vue3.0 的新变化

一、Proxy(核心原理)

  • 动机

  1. 由于 ES5 Object.defineProperty 的限制,Vue 不能检测数组和对象的一些特殊变化。
// vue 2.x
// 对于Object类型

const vm = new Vue({
  data:{
      a:1
  }
})

// vm.a 是响应式的

vm.b = 2
// vm.b 新增属性是非响应式的

// 对于Array类型

const vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})

vm.items[1] = 'x' // 不是响应性的 (通过索引改变一个普通值)
vm.items.length = 2 // 不是响应性的 (修改length)

vue3.x 不存在这些限制。proxy 是针对整个对象层面的代理拦截,而非 defineProperty 针对属性层面做的劫持。

Proxy 实现响应式 Demo

function reactive(target){
  if(!isObject(target)){
    return target
  }

  const handlers = {
    //属性读取触发get()方法
    get(target,key,receiver){
      const res = Reflect.get(target,key,receiver)
      return res
    },
    //属性设置触发set()方法
    set(target,key,value,receiver){
      trigger(target,key)
      const res = Reflect.set(target,key,value,receiver)
      return res
    },
    //数据删除触发deleteProperty()方法
    deleteProperty(target,key){
      const res = Reflect.deleteProperty(target,key)
      return res
    },
  }
  const observerd = new Proxy(target,handlers)
  return observerd
}
//对象
let obj = {
  name:'zyd',
  age:26
}
let obj_= reactive(obj)
// obj_.name = 'zyd1'
// obj_.style = '1'
//数组
let arr = new Array(5).fill().map((item,i)=>i)
let arr_ =  reactive(arr)
// arr_.push(5)
arr_[1] = 100
arr_[100] = 100
// arr_.length = 0
  1. Proxy 比 defineProperty 拥有更好的新标准的性能红利。

二、 Composition-API (核心 Api)

字面理解:组合 Api(vue 希望通过功能函数的组合去实现逻辑拆分与复用)

  • 动机

  1. 横切点关注问题 代码散落 Options 与 Class Api,代码组织不够聚合,无法按功能去进行代码组织,导致代码散落在 data、生命周期、watch、computed 里。

  2. 逻辑拆分与复用 功能拆分 vue2.x 代码复用的主要方式是提取可复用组件;纯的计算方法,可以提取为公共方法;但有些不需要模版的公共逻辑(并且与状态、事件、生命周期等紧密关联),就很难抽取,之前的 mixin、高阶组件等方案也都有他们各自带来的弊端。
    vue3.x 全新的 composition-API,可以完美解决这些问题,思路与 react-hook 类似,用负责单一功能函数进行组合。

  • 用法

    setup

    • setup 是一个新的组件选项,(它充当在组件内部使用 Composition API 的入口点。)

        // book.vue
        export default {
            props: {
              title: String
            },
            setup(props,context:{attrs, slots, emit}) {
              console.log(props.title)
            }
        }
      
    • 调用时机

      在 beforeCreate 之前,全局只调用一次。

    • 使用

        <template>
          <div>{{ count }} {{ object.foo }}</div>
        </template>
      
        <script>
          import { ref, reactive } from 'vue'
      
          export default {
            setup() {
              const count = ref(0)
              const object = reactive({ foo: 'bar' })
      
              // 必须return,才可以在模版上使用
              return {
                count,
                object
              }
            }
          }
        </script>
      
    • setup 函数里 this 并不是期望的组件实例,为 undefined,所以不要在 setup 访问 this,尽管这也毫无意义。

    reactive

    作用:用于实现对象数据类型的响应式侦测

    用法: 传入一个普通对象,返回值是一个经过 vue 代理过的响应式对象

      const Counter = {
        setup(){
          //reactive实现响应式,适用于一组值
          const state = reactive({
            count:0
          })
          const add =()=>{
            state.count ++
          }
          //返回一个代理后的对象,用来实现响应式
          console.log(state)
          return {
            state,
            add
          }
        },
        template:`<h1>{{state.count}}</h1><button @click="add">+</button>`,
      };
    

    ref

    作用:用于实现基础数据类型值(例:String、Number)的响应式侦测

    用法:传入一个值,返回一个 vue 内部包装过的{value:xxx}对象,并且改变 value 属性可以触发响应式更新,用于模版渲染时,不用.value 这样去访问,vue 内部会自动拆包。

    为什么这样设计?
    因为在 JavaScript 中,原始类型(例如 Number 或 String)是通过值传递的,而非引用。 按值还是按引用传递的差别

    //基础类型
    let a = 1
    let b = a// a变量与b变量已没有关系,实现不了响应式
    //对象类型
    let a = {value:1}
    let b = a// a变量与b变量都在引用同一个对象,响应式就不会中断
    //ref就是vue内部帮你实现的将普通值转化为一个包装对象的工具。
    let a = ref(1)
    console.log(a) //{value:1} 值转为对象
    
      const Counter = {
        setup(){
          //ref实现响应式,适合单个值场景
          const count = ref(0)
          const add =()=>{
            count.value ++
          }
          console.log(count)
          return {
            count,
            add
          }
        },
        template:`<h1>{{count}}</h1><button @click="add">+</button>`,
      };
    

    ref 常见的.value 问题
    问题有多严重?:前端人因为 Vue3 的 Ref-sugar 提案打起来了!
    .value 到底什么时候需要
    (1) 如果自己的代码取值,需要
    (2) watch 等的 vue 自身提供的 api 上,不需要(vue 自动帮你做了拆包)
    (3) 模版取值不需要
    影响:造成开发体验上的割裂 避免:并将所有的 ref 统一命名比如:xxxRef 一定程度可以避免,或者使用 ref:语法糖。

    toRef、toRefs

    作用:reactive 返回的代理对象在组合函数的传递过程中,必须保持对返回对象的引用,以保证其响应式,该对象不能被 ES6 解构或属性拆解。
    toRef 方法

        const pos = reactive({
          x:0,
          y:0
        })
        //将响应式对象某一个属性转化为ref
        const xRef = toRef(pos,'x')
        const yRef = toRef(pos,'y')
    

    toRefs 方法

        const pos = reactive({
            x:0,
            y:0
          })
          //将整个响应式对象的全部属性转化为ref,装在一个普通对象中
          const posRefsObj = useRefs(pos)
          //等价于
          const posRefsObj = {
              x:toRef(pos,'x')
              y:toRef(pos,'y')
          }
    

    computed

    作用: 与 vue2.x 一致,根据函数体内依赖的值的变化,计算出一个新值。 用法: 传入一个计算函数,返回一个包装后的{value:xxx}响应式引用对象。

      const Counter = {
            setup(){
                const state = reactive({
                  count:0
                })
                //计算count是否是偶数,字体切换红绿颜色
                let isOdd = computed(()=>state.count%2 === 0)
                const add =()=>{
                  state.count ++
                }
                return {
                  state,
                  isOdd,
                  add
                }
              },
              template:`<h1 :style="{'color':isOdd?'red':'green'}">{{state.count}}</h1><button @click="add">+</button>`,
          };
    

    watch

    作用: 与 vue2.x 一致,主动监测响应式数据,数据改变后,执行用户传入的回调。

      const Counter = {
        setup(){
          //reactive实现响应式,适用于一组值
          const state = reactive({
            count:0
          })
          //计算count是否是奇数,字体切换红绿颜色
          let isOdd = computed(()=>state.count%2 === 0)
    
          watch(isOdd,(newValue)=>{
            alert(newValue?'偶数':"奇数")
          })
          const add =()=>{
            state.count ++
          }
          return {
            state,
            isOdd,
            add
          }
        },
        template:`<h1 :style="{'color':isOdd?'red':'green'}">{{state.count}}</h1><button @click="add">+</button>`,
      };
    

    生命周期

    选件API compositonAPI
    beforeCreate 没有必要*
    created 没有必要*
    beforeMount onBeforeMount
    mounted onMounted
    beforeUpdate onBeforeUpdate
    updated onUpdated
    beforeUnmount onBeforeUnmount
    unmounted onUnmounted

与 react-hook 对比

  1. 心智负担不同
    两者都会有一定的心智负担
    react-hook 的问题是总担心频繁触发更新,useEffect 可以说是控制代码不被频繁执行最后的逃生舱,但是依赖项数组如果设置不正确,会导致副作用执行时机不正确,还可能会导致取到闭包旧值的 bug; useCallBack 经常需要使用,用来避免触发不必要的子组件渲染。 vue-compositonApi的问题刚好相反,是经常担心触发不了更新,比如解构导致的响应式丢失,vue 引入 ref 解决这个问题,但引起了总是忘记写.value 的新问题。
  2. 对生命周期的看法不一样
    react-hook 有意弱化生命周期的概念,转而倡导渲染副作用的概念,由数据变化引起渲染副作用的执行。或者另一个角度说 react 用 useEffect 实现了生命周期的聚合与封装。
//hook
 useEffect(() => {
  alert('组件挂载')
   return ()=>{
     //组件卸载,清除副作用
     alert('组件卸载')
   }
 },[]);

 //compositonApi
 onMounted(()=>{
   alert('组件挂载')
 })
 onUnmounted(()=>{
   alert('组件卸载')
 })

vue-compositonApi 还是熟悉的 vue2.x 生命周期,没有增加新的理解成本

compositionAPI 实践 Demo(并与 hook 的对比)常见吸顶及上拉加载 demo

三、 其他改动

  • teleport (传送)
  • 组件不再限制只能存在一个根元素
  • data 选项,都统一改成函数形式(之前根组件是对象,子组件是函数)
  • $children 已废弃,只能通过 ref 获取组件及 dom 了。
  • 等等。更多的请参照官方文档
作者简介

郑瑜栋:在团队拥有一个奇怪的外号“=”哥。