Vue3.0带来了哪些变化?

2,485 阅读5分钟

前言:今天无意翻看到从去年至今,我已经在掘金阅读过1000+的文章,但是又回想起现在自己还这么菜,不由得狠狠地拍了一下大腿:我真TM菜。

距离 Vue3.0-beta 版发布已经有一月有余。今天终于能腾出点时间来看看新版Vue给我们带来了哪些令人惊艳特性。

虽然掘金上已经有很多关于Vue3.0的优质文章,但是我还是先从如何升级至vue3.0-beta写起。

2.x 升级至 3.0

目前如果Vue3.0需要在使用vue-cli创建项目后,执行vue add vue-next安装插件来实现。

注意:如果你想正常使用Vue3.0请确保你的vue-cli已经升级到最新版。截止目前vue-cli版本为:v4.3.1,预览版:v4.4.0。

写法有变化吗?

肯定有,3.0最为直观的变化就是在写法上有了巨大的变化,将 2.x 中与组件逻辑相关的选项以 API 函数的形式重新设计。将setup()函数作为组件选项的入口,详情可以查看 Vue-Composition-API ,这种写法看上去很像React的写法。那么这种变化是不是会增加Vue的学习成本呢?答案是否定的。下面将介绍2.x和3.0的部分组件选项写法对比。

声明变量

2.x 中声明变量:

<template>
    <div>
        <div> {{message}} </div>
        <h2> userinfo </h2>
        <div>name: {{userInfo.name}}</div>
        <div>name: {{userInfo.age}}</div>
        <div>name: {{userInfo.status}}</div>
        <div>name: {{userInfo.dream}}</div>
    </div>
</template>
<script>
export default {
    name: 'BlindDate',
    data() {
        return {
            message: 'hello girl!',
            userInfo: {
                name: 'lq9958',
                age: 18,
                status: 'single',
                dream: 'find a girlfriend'
            }
        }
    },
    ...,
}
</script>

3.0中声明变量:

//html
<template>
    <div> {{message}} </div>
    <h2> userinfo </h2>
    <div>name: {{userInfo.name}}</div>
    <div>name: {{userInfo.age}}</div>
    <div>name: {{userInfo.status}}</div>
    <div>name: {{userInfo.dream}}</div>
</template>
<script>
// 3.0中声明变量
import {ref reactive } from 'vue'

export default {
    name:'BlindDate',
    setup() {
        const message = ref('hello girl!')
        const userInfo = reactive({
            name: 'lq9958',
            age: 18,
            status: 'single',
            dream: 'find a girlfriend'
        })
        
        return {
            message,
            userInfo
        }
    },
    ...,
}
</script>

从上面可以看到,2.x中我们声明变量需要在data()函数中声明,但是在3.0中声明变量需要在setup()中声明(几乎所有2.x的组件选项都会写在这个函数中),同时还使用了两个函数refreactive(3.0中需要使用vue中任何api都需要这种按需导入的形式来使用相关api,这应该是为了让vue的各个模块独立出来,同时降低打包体积而考虑的),再看看template中的代码,是不是发现3.0中包含了多个根元素。是的,这也是3.0的新特性之一,允许tempalte中包含多个根元素,关于refreactive这两个函数的区别我会在稍后给出,接下来再看看其他的区别。

监听 watch

2.x 中的 wath

//html
<template>
  <div class="hello">
    <div>{{message}}</div>
    <div class="message">{{changeMessage}}</div>
    <button @click="handleMessage">Bless God</button>
  </div>
</template>

<script>
export default {
    data() {
         message: 'have no girlfriend',
         changedMessage: 'click the bottom btn will have a girlfriend!'
    },
    watch:{
        message: {
            handler: function(newVal, oldVal) {
                this.changedMessage = newVal
            }
        }
    },
    
    methods:{
    handleMessage: function(){
        message.value =  'lied to you'
        }
    },
    ...
    
}
</script>

3.0中的 watch

//html
...
<script>
import { ref,watch, watchEffect } from 'vue'

export default {
    ...,
    setup() {
        let message = ref('have no girlfriend')
        let changeMessage = ref('click the bottom btn will have a girlfriend!')
        
        // simple effect
        watchEffect(()=> {
          changeMessage.value = message.value  
        })
        
        watch(message,(newVal,oldVal) => {
          changeMessage.value = newVal
        })
        
        // method
        const handleMessage = function(){
          message.value =  'lied to you'
        }
        return {
          message,
          handleMessage,
          changeMessage
        }
    }
}
</script>

在3.0中使用watch同样需要先导入 watch函数,这里可以看到我在导入watch的同时还导入了一个watchEffect,这个watchEffect是干什么的呢?翻看源码后发现其实两者是一样的,只是后者的 callbacknull

watch 源码
位置:vue/dist/vue.global.js

  function watch(source, cb, options) {
      if ( !isFunction(cb)) {
          warn(`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
              `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
              `supports \`watch(source, cb, options?) signature.`);
      }
      return doWatch(source, cb, options);
  }

watchEffect 源码
位置:vue/dist/vue.global.js

  function watchEffect(effect, options) {
      return doWatch(effect, null, options);
  }

是的,并没有什么复杂的处理逻辑,仅仅是判断了callback是否为函数,当你使用watch时,第二个参数不为函数时,vue 就会发出警告,推荐你使用watchEffect,因为watchEffect允许你不指定监听源,你代码中涉及到的属性,只要发生变化都会立即执行watchEffect,详情请看:watch和watchEffect的区别。 相比于2.x我们如果需要监听多个数据源时,会写一大串的属性和handler,3.0只在 watchEffect函数中写执行逻辑即可。这里我们可以看到我导出了一个method。是的,在3.0中我们不在需要在methods属性中书写我们的methods(纳尼?你在说啥) 所有的methods都将定义在setup()中,这个我在上文已经提到过。

计算属性 computed

2.x的计算属性

<script>
export default {
    data() {
        return {
            firstName: '黑',
            lastName: '嘿黑'
        }
    },
    computed:{
        fullName: function() {
            return this.firstName + this.lastName
        }
    },
    ...
}
</script>

3.0中的计算属性

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

export default {
    setup() {
        let firstName = ref('jack')
        let lastName = ref('jones')
        
        let fullName = computed(() => {
            return firstName + lastName
        })
        
        return {
            firstName,
            lastName,
            fullName
        }
    }
}
</script>

computed 目前的变化目前不是很大。

生命周期

在2.x中我们知道除去 keep-alive的两个生命周期处理函数activateddeactivated以及一个捕获异常的errorCaptured(说实话平常我都没用过这个函数,哎,再狠狠地拍一下大腿:我真TM菜)。2.x和3.0的生命周期函数使用方式都是一致,这里我只举例 onMounted这一个函数。

2.0 中的 mounted

<script>
export default {
    ...,
    mounted() {
        // do some thing
    }
}
</script>

3.0中的 onMounted

<script>
import { onMounted } from 'vue'
export default {
    setup() {
        onMounted(() => {
            // do some thing
        })
    }
}
</script>

生命周期函数并没有什么实质性的变化,只是名字改变了而已,下面是2.x中的生命周期函数在3.0中的对应表:

2.x3.0
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updateonUpdate
activatedonActivated
deactivatedonDeactivated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
errorCapturedonErrorCaptured

如何获取DOM元素

2.x中获取DOM元素

//hmtl
<template>
    <div ref="root">
        test message
    </div>
</template>

<script>
export default {
    ...,
    mounted() {
       let root = this.$refs.root
    },
    ...
}
</script>

在3.0中,我们已经用不着 this了,那我们如歌获取DOM元素呢?

//hmtl
<template>
    <div ref="root">
        test message
    </div>
</template>

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

export default {
    ...,
    setup() {
        let root = ref(null)
        onMounted(() => {
            let rootEle = root.value
        })
        
        return {
            root
        }
        
    }
    ...
}
</script>

有没有很惊喜?获取元素居然是这样的,在3.0中,vue 如果检测到声明的变量和模板中的ref值相等的话就会在初始化完成之后将元素赋值给这个变量,详情请看 Vue3.0中获取DOM元素

上面代码中有一行是这样的let rootEle = root.value,为什么获取到的元素不是上面的root 变量,而是要去使用.value来获取,这就要提到Vue3.0响应式设计。

Vue3.0使用的ES6中的新API ProxyReflect 来完成响应式设计。

Vue3.0提供了两种创建响应式数据的方式,也就是文章开篇提到的 refreactive,这两者有什么区别?开发时应该使用哪一种?最直接的方式就是查看源码,虽然看不懂多少。

以下内容纯属个人见解,如有错误的地方,希望各位大佬指正,谢谢。

ref 源码
位置:@vue/reactivity/dist/reactivity.global.js

// createRef 函数 为 ref 函数的核心代码
function createRef(rawValue, shallow = false) {
    if (isRef(rawValue)) {
        return rawValue;
    }
    // 这里可以看到,如果 createdRef 的第二个参数不传入 vue 默认是会进行深度代理的
    let value = shallow ? rawValue : convert(rawValue);

    // vue 在这里对普通 value 进行了一次包裹,返回一个包含 __v_isRef 和 value 属性的对象
    const r = {
        __v_isRef: true,
        get value() {
            track(r, "get" /* GET */, 'value');
            return value;
        },
        set value(newVal) {
            if (shared.hasChanged(toRaw(newVal), rawValue)) {
                rawValue = newVal;
                value = shallow ? newVal : convert(newVal);
                trigger(r, "set" /* SET */, 'value',  { newValue: newVal } );
            }
        }
    };
    return r;
}

也就是说在不传入第二个参数时,默认会走 convert分支,那我们看看 convert里面都有些啥。


  const convert = (val) => isObject(val) ? reactive(val) : val;

当传入的值是一个对象时,则走reactive 分支,否则返回原数据,这里的 reactive正是Vue3.0创建响应式数据的第二种方式,这里先不看,我们直接返回源数据,再来看看源码:

    const r = {
        __v_isRef: true,
        get value() {
            track(r, "get" /* GET */, 'value');
            return value;
        },
        set value(newVal) {
            if (shared.hasChanged(toRaw(newVal), rawValue)) {
                rawValue = newVal;
                value = shallow ? newVal : convert(newVal);
                trigger(r, "set" /* SET */, 'value',  { newValue: newVal } );
            }
        }
    };
    return r;

看到这里诸位应该明白了,当我们传入的值不是一个对象时,Vue会对其进行一次包装,返回一个包含 __v_isRefvalue 属性的对象, 这里 value的值正是传入的值,这也是为什么我们在使用ref方式创建一个变量时,如果对其修改需要使用.value的方式来修改。

那我们再来看看reactive

reactive 源码
位置:@vue/reactivity/dist/reactivity.global.js

  function reactive(target) {
      // if trying to observe a readonly proxy, return the readonly version.
      if (target && target.__v_isReadonly) {
          return target;
      }
      return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
  }
  
  
  function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
      if (!isObject(target)) {
          {
              console.warn(`value cannot be made reactive: ${String(target)}`);
          }
          return target;
      }
      // target is already a Proxy, return it.
      // exception: calling readonly() on a reactive object
      if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
          return target;
      }
      // target already has corresponding Proxy
      if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
          return isReadonly ? target.__v_readonly : target.__v_reactive;
      }
      // only a whitelist of value types can be observed.
      if (!canObserve(target)) {
          return target;
      }
      const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers);
      def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed);
      return observed;
  }

当进入reactive分支后,Vue首先会判断该数值是否为一个对象,当不是一个对象时,Vue会发出警告并直接返回这个数值。当然,这个数值也就不是响应式的了。好了,对于Vue内部是如何去对 observed 对象做代理操作的今天暂时不深究,今天的目标暂时把 ref reactive 区别搞清楚:ref在内部对不同类型的数值做了处理,当我们在了解ref内部流程后应该清楚,在创建一个基本数据类型的响应式数据还是应当使用ref,当需要创建的响应式数据不是基本数据类型时,应当是使用reactive

关于Vue的今天暂时就写到这里吧,Vue3.0到来不止是其本身的改变,她的周边配套设施同样也会随其更改,下期打算写一些关于Vue-router的东西。第一次在掘金上发布文章,平时看到各位大佬文章中要背景有背景,要颜色有颜色。哎,最后再狠狠地拍下大腿:我真TM菜。

文章中使用的3.0版本为v3.0.0-beta.14,目前3.0仓库仍然在频繁的更新,具体使用情况还是要等到官方文档出来才能知晓。

转载或摘抄须注明出处。