vue3 新功能

vue3 升级的重要功能

1.createApp

2.emits属性

3.Fragment

4.移除.sync 改为 v-model参数

5.异步组件的引用方式

6.移除filter

7.Teleport

8.Suspense

9.生命周期

Composition API 

10.composition API 对比 option API

11.reactive 和 ref 对比

12.toRef 和 toRefs

13.为什么需要ref?为何需要 .value?为何需要 toRef toRefs?

14.Composition API 实现逻辑复用

15.setup中如何获取组件实例

16.vue3 script setup

17.watch 和 watchEffect

原理

18.vue3 如何实现响应式

19.编译优化

20.源码体积优化

Vite

21.Vite 为什么启动非常快

22.ES6 Module 在浏览器中的应用

 

一、createApp

  vue3 是通过 createApp 函数来创建vue的应用实例的

  vue2 是通过 new Vue() 来创建vue实例的

复制代码
// vue2.x
import Vue from 'vue'
const app = new Vue({
   // 选项
})

//vue3
import {createApp} from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
复制代码

二、emits 属性

  1. emits 属性是vue3中新增的组件触发的自定义事件,代码示例:  

    父组件:

<HelloWorld @sayHello="sayHello">

   子组件

export default{
    name: 'HelloWorld',
    emits:['sayHello'], // 声明自定定义的事件
    setup(props,{ emit }){
          emit('sayHello','aaaa') //触发事件
    }   
}

  2、emits 不能在组件的 <script setup> 部分中使用,可以用defineEmits()返回一个相同的函数供我们使用。

  父组件:

<HelloWorld @sayHello="sayHello">

  子组件.  需要用defineEmits(['xxx','xxx']) 返回的函数来触发事件

<script setup>
      const emit = defineEmits(['sayHello']) // 多个触发事件,可以都放到数组中。返回的是一个函数,这个函数和$emits函数作用相同
      // 触发事件
      emit('sayHello','aaa')
</script>

 触发自定义事件的方法总结: 

 emits 属性:用于 vue3 中 Options API 中

   defineEmits: 只能用在 在Composition API 的setup 函数中

   $emit 方法:组件的模板表达式中可以直接使用$emit(),代码示例:

<!-- MyComponent -->
<button @click="$emit('someEvent')">Click Me</button>

三、Fragment

  • vu2.x 组件模板必须是单一根节点

 

<template>
    <div>
          <h2>{{title}}</h2>
          <div v-html="content"></div>
    </div>
</template>
  • vue3 组件模板可以有多个根节点 
<template>
   
      <h2>{{title}}</h2>
      <div v-html="content"></div>
    
</template>

四、 移除 .sync 改为v-model 参数

在自定义组件上对一个 prop 进行双向绑定的实现,vue3中改为 v-model:参数 

复制代码
<!--vue2.x -->
<MyComponent :title.sync="title" />
<!--等价于-->
<MyComponent :title="title" @update:title="title=$event"/>
<!--vue3--> 
<MyComponent v-model:title="title" v-model:age="age"/>
<!--等价于-->
<MyComponent :title="title" @update:title="title = $event" @update:age="age = $event"/>
复制代码

 组件中v-model在Vue3.4开始推荐使用defineModel()宏:

  • defineModel() 返回的值是一个ref
  • 它的.value 和父组件的 v-model 的值同步
  • 当子组件变更了,会触发父组件绑定的值一起更新
  • 多个v-model 绑定可以在defineModel('name')指定参数

  父组件:

复制代码
<script setup>
  import Child from './Child.vue'
  import {ref} from 'vue'
 
  const titleRef = ref('hello')
  const ageRef= ref(20)
</script>

<template>
    <h1>{{titleRef}} {{titleRef}}</h1>
    <Child 
        v-model:title="titleRef"
        v-model:age="ageRef"
    />
</template>
复制代码

子组件:

<script setup>
      const title = defineModel('title')
      const age = defineModel('age')
</script>
<template>
     <input v-model="title" type="text">
     <input type="number" v-model="age"/>
</template>

defineModel() 底层机制:

  • 一个名为modelValue 的prop(v-model不加参数时,默认的参数是modelValue,定义了参数使用定义的名称),本地ref的值与其同步
  • 一个名为update:modelValue(v-model不加参数时的事件,自定了参数适应定义的名称) 的事件,当本地ref的值发生变更式触发
    复制代码
    <!--父组件-->
    <script setup>
    import { ref } from 'vue'
    import UserName from './UserName.vue'
    
    const first = ref('John')
    const last = ref('Doe')
    </script>
    
    <template>
      <h1>{{ first }} {{ last }}</h1>
      <UserName
        v-model="first"  
        v-model:last-name="last"
      />
    </template>
    
    
    <!--子组件-->
    <script setup>
    defineProps({
      modelValue: String, //v-model不加参数时,会增加默认参数modelValue
      lastName: String. // v-model:last-name 加参数就用自定义的参数名
    })
    
    defineEmits(['update:modelValue', 'update:lastName'])
    </script>
    
    <template>
      <input
        type="text"
        :value="modelValue"
        @input="$emit('update:modelValue', $event.target.value)"
      />
      <input
        type="text"
        :value="lastName"
        @input="$emit('update:lastName', $event.target.value)"
      />
    </template>
    复制代码

五、异步组件的引用方式

  • vu2.x 通过 () => import('') 
export default {
     //...
     components: {
           'my-component': () => import('./component/my-async-component')
     }  
}

 

  • vue3中通过defineAsyncComponent(()=>import(''))  
<script setup>
import {defineAsyncCompnent } from 'vue'

const MyComponent = defineAsyncComponent(() => import('./component/my-async-component'))

</script>

六、移除filter

  vue3 中 将filter移除,使用计算属性代替

七、Teleport

  • <Teleport> 是一个内置组件,将组建中某个模板传送到该组件的DOM结构外层的位置 
  • 场景:全屏的模态窗
  • 代码:
    <Teleport to="body">
           <div class="modal">
               <p>内容</p>
               <button @click="open=false">关闭</button>
           </div>
    </Teleport>    

    这样,我们就把模板插入到 body标签下了

八、Suspense

  • <Suspense> 内置组件,用来处理异步组件的加载,提供了一个优雅的加载状态处理机制。
  • <Suspense>组件有两个插槽: #default 和 #fallback
  • 例子:
    复制代码
    <template>
      <Suspense>
        <template #default>
          <AsyncComponent /> <!--  是一个异步组件 -->
        </template>
        <template #fallback>
          <div>Loading...</div>
        </template>
      </Suspense>
    </template>
    复制代码

九、生命周期

  • vue3 选项式API(options API)形式的生命周期钩子
    • beforeCreate
    • created
    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • beforeUnmount
    • unmounted  
  • vue3 Composition AP(组合API) 形式的生命周期钩子
    • setup() ====> setup 相当于之前的 beforeCreate 和 created。setup执行的时间比beforeCreate 还要早 
    • onBeforeMount
    • onMounted
    • onBeforeUpdate
    • onUpdated
    • onBeforeUnmount
    • onUnmounted

     

十、reactive 和 ref 对比

  • 在组合式API中,创建响应式数据用 ref 或者 reactive
  • ref: 基本类型、对象类型的响应式数据都可以创建。ref 创建对象类型的响应式数据,底层的实现是:将非原始值通过reactive()转换为响应式代理。
  • reactive:只能创建对象类型的响应式数据。上面提到的ref创建的对象类型时,底层也是调用了这个方法实现的
  • ref 创建的变量必须使用.value(可以使用volar 插件自动添加.value);
    复制代码
    <template>
         <div>{ageRef}}</div>
        <div>{{state.name}}- {{state.age}}</div>
       <button @click="change">修改数据</button>
    </template>
    <script setup>
     import {ref} from 'vue'
     const ageRef = ref(20)
     const state = ref({
         name:'zhangsan',
        age: 18
     }) 
    function change(){
        ageRef.value = 25 
        state.value = {name:'lisi',age: 18} //可以直接赋值新对象,reactive则不可以
    }
    </script>
    复制代码

    reactive创建的响应式数据的缺陷:

    • 重新分配一个新对象,会失去响应式(可以使用Object.assign代替)
      复制代码
      <script setup>
             const obj = reactive({
                     name:'zhangsan',
                     age: 14
             })
             function change(){
                  // 此时 obj 失去了响应式,视图不会更新
                // obj = reactive({name:'lisi',age:18}) //直接赋值新的对象会失去响应式
                 // 用Object.assign()来替换
                 Object.assign(obj,{name:'lisi',age: 18})
              }
      </script>
      复制代码
    • 对解构操作不友好:解构出来的变量会失去响应

      const state = reactive({count: 0})
      
      // 解构时,count已经与 state.count 断开链接
      let {count} = state
      
      count++ // 此时不会影响原始 state

     使用规则:

    • 若是基本类型,必须用ref
    • 若是对象类型,层级不深,ref和 reactive 都行
    • 若是一个对象类型,层级比较深,推荐使用reactive    

十一、composition API 和 options API对比

十二、toRef 和 toRefs

  • toRef
    • 将reactive 定义的响应式对象的一个属性,创建成一个对应的ref
    • 创建的fef,具有响应式
    • 两者保持引用关系 
    • 示例:
      复制代码
      <tamplte>
              <div>{{ageRef}} ---{{state.age}}</div>
      </template>
      <script setup>
          import {reactive, toRef} from 'vue'
           const state = reactive({
                name:'zhangsan',
                age: 14
           })
          const ageRef = toRef(state,'age')
      
         setTimeout(()=>{
               ageRef.value = 18 //此时state.age 也会更新
         },1000)
      
      </script>
      复制代码
  • toRefs 
    • 将reactive 创建的响应式对象,转换成普通对象
    • 对象的每个prop 都是对应的ref,具有响应式
    • 两者保持引用关系 
      • 示例: 
        复制代码
        <template>
               <div>名字:{{ name }} - 年龄: {{age}}</div>
        </template>
        <script setup>
           import {reactive,toRefs} from 'vue'
        
            const state = reactive({
                name: 'zhangsan',
                age: 14
            })
           // toRefs() 返回的是一个普通对象,每个属性都是ref
        // 可以解构,不会失去响应式
           const {age,name}
        = toRefs(state) setTimeout(()=>{ age.value = 20 name.value = 'lisi' state.age = 22 },1000) </script>
        复制代码
  • 组合式函数中返回响应式对象时,toRefs 相当有用 
    复制代码
    function useFeatureX(){
           const state = reactive({
                foo: 1,
                bar: 2
           })
    
    
          // 在返回时都转为 ref  
          return roRefs(state)
    }
    
    
    // 可以结构2而不失去响应性
    const { foo, bar } = useFeatureX
    复制代码

     

十三、为什么需要ref?为何需要 .value?为何需要 toRef toRefs?

  1. 定义响应式数据可以用reactive,值类型的我们可以在包一层对象即可,那么为什么vue要再设计出一个ref呢?

      • 返回值类型会丢失响应式

 

复制代码
<template>
    <div>{{age}}</div><!--一直是20-->
    <div>{{books}}</div><!--1s后books变了-->
</template>
<script setup>
import {reactive} from 'vue'

const state = reactive({age:20,name:'zhangsan',books:['css','html','javascript'])
let age = state.age // state.age 是值类型,所以age 失去了响应式
let  books = state.books //state.books 是对象类型,books 仍具有响应式

setTimeout(()=>{
   state.age = 21 
   state.books.push('vue')
} ,1000)
<script>
复制代码

 

      • 而在开发中 computed有可能返回值类型,我们开发中计算属性用的非常多,如果返回的值类型不具有响应式,那么开发起来很痛苦啊          
复制代码
<template>
     <div>{{age}}</div><!---1s后变成21->
</template>
<script setup>
  import {reactive, computed } from 'vue'
 
 const state = reactive({age:20,name:'lisi'})

 const age = computed(()=>{
      return state.age
 })
console.log(age) // ref类型的响应式数据

 setTimeout(()=>{
      state.age = 21
 },1000)
</script>
复制代码

 从上面代码可以看出,vue底层在实现computed属性时,将返回的值类型变成了ref。      

      • 所以不只是定义数据的时候需要ref,vue中其他地方也需要ref。    

 2.ref定义的对象,为什么需要.value ?

      • ref是一个对象,value 存储值
      • 通过.value 属性的get和 set实现响应式 
      • 用于模板、reactive时,不需要.value,其他情况都需要     

 3.为和需要toRef和toRefs

      • 初衷:不丢失响应式的情况下,把对象数据分解
      • 前提: 针对的是响应式数据对象(reactive封装的),非普通对象
      • 注意:toRef 和 toRefs是延续响应式,不是创造响应式,创造响应式是ref 和 reactive      

十四、Composition API 如何实现逻辑复用?

   vue的组合式API中,将通过组合式函数 来实现逻辑复用

    组合式函数:利用Vue的组合式API来封装和复用有状态逻辑的函数

    具体实现:

      • 抽离逻辑代码放到一个函数中
      • 函数命名为useXxxxx格式
      • 在setup 中饮用useXxxx 函数的形式提取到外部文件中 

      示例:  鼠标踪器功能

         src/composable/useMouse.ts   

复制代码
import { ref, onMounted, onUnmounted } from 'vue'

// 组合式函数命名以 “use” 开头
export function useMouse(){
  const x = ref(0)
  const y = ref(0)

  function update(event:Event){
    //使用 as 关键字进行类型断言
    const mouseEvent = event as MouseEvent
    x.value = mouseEvent.pageX;
    y.value = mouseEvent.pageY
  }

  onMounted(()=>{
    window.addEventListener('mousemove',update)
  })
  onUnmounted(()=>{
    window.removeEventListener('mouseove',update)
  })

  return {
    x,
    y
  }
}
复制代码

    组件中引用组合函数:

<template>
  <div>鼠标位置: {{x}},{{y}}</div>
</template>
<script setup lang="ts">
    import {useMouse} from '../composable/useMouse.ts'
   const {x,y} = useMouse()
<script> 

十五、setup中如何获取组件实例

  • setup 和 其他Composition API 中没有this
  • 可通过getCurrentInstance() 获取当前实力
  • 若使用Options API 可照常使用this
    <script setup>
         import {getCurrentInstance, onMounted} from 'vue'
         
        onMounted(()=>{
              console.log(this) //undefined
              console.log(getCurrentInstance()) //获取组件实例
        })
    </script>

十六、vue3 script setup

  • <script setup> 中顶级变量、自定义组建,可以直接用于模板 ,不需要有默认导出和return
  • 使用该功能的vue 版本要 >= 3.2.0
  • 可以和其他<script> 同时使用
  • <script setup> 建议放到 <template>前面
    复制代码
    <script>
           function add(a,b){
                    return a+b
           }
    </script>
    
    <script setup>
       import { ref, reactive, toRefs} from 'vue'
       import Child from './Child'
    
       const countRef = ref(100)
      
       function addCount(){
             countRef.value++
       }
    
       const state = reactive({name:'zhangsan',age:20})
       const {name} = toRefs(state)
    
       console.log(add(10,20)) // 30
    </script>
    <template>
             <p @click="addCount">{{countRef}}</p>
            <p>{{name}}</p>
    </template>
    复制代码
    •  用defineProps 和 defineEmits 来定义参数 和定义事件
      复制代码
      //Child.vue
      
      <script setup>
      import {defineProps,defineEmits} from 'vue'
      
      //定义属性--数组形式
      //defineProps(['name','age'])
      
      // 定义属性--对象形式,可以定义类型
      defineProps({
            name: String,
            age: Number
      })
      //定义事件
      const emit = defineEmits(['change',delete])
      
      function deleteHandler(){
            emit('delete','bbb') // 触发事件
      }
      </script>
      <template>
             <p>{{name}} - {{age}}</p>
         <!---模板中可以使用$emit触发事件--> <button @click="$emit('change',123)">change</button> <button @click="deleteHandler">delete</button> </template> //Parent.vue <script setup> import Child from './Child.vue' inport {reactive, toRefs } from 'vue' const state = reactive({name:'zhangsan',age:12}) const {name,age} = toRefs(state) function onChange(info){ console.log(info) } function onDelte(info){ console.log(info) } </script> <template> <Child :name="name" :age="age" @change="onChange" @delete="onDlete" /> </template>
      复制代码
  • defineExpose :暴露数据给父组件 
    复制代码
    //Child.vue
    <script setup>
    import {ref, defineExpose} from 'vue'
    const countRef = ref(100)
    function add(a,b){
          return a+b
    }
    defineExpos({
         countRef,
         add
    })
    </script>
    <template>
          <p>子组件:{{a}}</p>
    </template>
    
    //Parent.vue
    <script setup>
    import Child from './Child'
    import {ref, onMounted} from 'vue'
    
    const childRef = ref(null)
    onMounted(()=>{
         // 拿到Child 组件的一些数据
        console.log(childRef.value) // 可以获取到组件暴露的所有数据
        console.log(childRef.value.a)
        console.log(child.value.add.add)
    })
    </script>
    <template>
         <Child ref="childRef">
    </template>
    复制代码

十七、watch 和 watchEffect

  1. watch

    watch(data,()=>{},{})可以传三个参数

    • 第一个参数:数据源 
      • 数据源是一个ref
        复制代码
        <script setup>
         import {ref,watch} from 'vue'
        
         const count = ref(0)
        
         setTimeout(()=>{
             count.value = 1
         },1000)
        
        //直接监听一个ref
        watch(count,(newvalue,oldvalue)=>{
            console.log(newvalue,oldvalue)
        })
        
        </script>
        复制代码
      • 数据源是一个响应式对象reactive时,会隐士创建一个深度监听器(即多层嵌套的对象修改都可以监听到)
        复制代码
        <script setup>
        import {reactive, watch} from 'vue'
        //reactive 响应式对象
        const obj = reactive({
         name:'zhangsan',
         age:20,
         info:{
           city: '北京' 
        }
        })
        
        setTimeout(()=>{
             obj.age = 22;
            obj.info.city = 'changchun' 
        },1000)
        
        //会隐士创建一个深度监听
        watch(obj,(value)=>{
            console.log(value)
        })
        </script>
        复制代码
      • 数据源是一个getter函数,用来监听对象的某个属性,如果监听的属性仍是个对象类型,需要手动开启深度监听(即deep:true)
        复制代码
        <script setup>
        import {reactive, watch} from 'vue'
        const obj = reactive({
          age:20,
          info:{
              city: '北京'
          }
        })
        
        setTimgout(()=>{
           obj.age = 21;
          obj.info.city = '长春'
        },1000)
        
        //监听obj.age属性时,需要返回一个getter函数形式,否则不会被监听
        watch(
             ()=>obj.age,
             (value,oldvalue) =>{
                   console.log(value,oldvalue)
             }  
        ) 
        //监听obj.info属性时,需要返回一个getter函数形式,并且需要开启深度监听,否则不会监听到info内部属性的变化,除非替换整个info对象
        watch(
             ()=>obj.info,
             (value) =>{
                   console.log(value)
             },
             {
                  deep:true //开启深度监听
             }
        ) 
        
        //如果监听的是obj.info.city 则不需要深度监听,因为obj.info.city是基本类型 
        watch(
            () => obj.info.city, ///需要写成getter函数形式
            (value,oldvalue)=>{
                console.log(value,oldvalue,'5555')
            }
        
        )
        </script>
        复制代码
    • 第二个参数:监听触发的回调函数 (newvalue,oldvalue)=> {}
      • 如果监听的是基本类型的,会返回newvalue和 oldvalue
      • 若果监听的是对象类型的,返回的newvalue 和oldvalue 是相同的,都是新值  
    • 第三个参数:配置对象,可选的
      • immediate:true  立即执行
      • deep:true 深度监听
      • once:true 仅触发一次         

  2.watchEffect

    watchEffect(()=>{})

    • 接受一个函数作为参数
    • 回调函数会在组件渲染时立即触发一次,即使是空函数(为了收集监听的数据)
    • 自动追踪所能访问到的响应式属性(必须是在回调函数中用到的属性改变了,才会触发回调函数)
    • 无法获取新值和就值
      复制代码
      <script setup>
       import {ref,reactive} from 'vue'
      
       const countRef = ref(0)
       const obj = reactive({age:20,info:{city:'beijing'}})
      
      
       watchEffect(()=>{
           //初始化时一定会执行一次,为了收集监听的数据
          console.log(countRef,'countRef') //countRef 会被收集进行监听
          console.log(obj.info.city,'city') // city属性会被收集,一旦city改变就会触发回调函数
       })
      setTimout(()
      =>{ countRef.value = 2 // 这个改变会触发watchEeffect ,因为watchEffect 中收集的该属性的监听(watchEffect 回调函数中用到) },1000) setTimout(()=>{ obj.age = 21 // 这个改变不会触发watchEeffect ,因为watchEffect 中没有用到改属性 },1000) setTimout(()=>{ obj.info.city ='长春' // 这个改变会触发watchEeffect },1000) </script>
      复制代码

  3.watch 与 watchEffect 区别?

      • 两者都可以监听data 属性变化
      • watch 需要明确监听那个属性;而watchEffect 会根据其中的属性自动监听变化
      • watch默认情况是惰性执行,只有数据发生变化才会执行。除非设置immmediate:true;而 watchEffect 初始化时立即执行一次 

  4.watch 和 watchEffect 适用场景:

    • watchEffect 的适用场景
      •  适用于需要立即执行响应数据变化的场景
      • 监听多个数据源,并且不想明确指定每个数据源时,使用watchEffect 更为方便
      • 不需要获取新值和旧值,或者逻辑上不需要区分新旧值
    • watch的适用场景  
      • 需要数据变化时获取新旧值,以便进行对比或执行依赖于新旧值的操作时
      • 不需要立即执行,而是在数据变化时才需要响应的场景
      • 适用需要精确控制监听哪些数据变化的场景         

十八、vue3 如何实现响应式

  vue2.x中数据响应式是通过Object.defineProperty()来实现了

  Object.defineProperty 的缺点:

      • 深度监听需要一次行递归
      • 无法监听新增属性/删除属性(所以有了 Vue.set、Vue.delete)
      • 无法监听数组API 方法,需要特殊处理(vue2中重写了数组原型上的方法)      

     vue3采用Proxy 代替 vue2.x Object.defineProperty() 来实现的。

      • Proxy 是ES6引入的一个新特性,Proxy是一个构造函数,用于实现对对象的代理操作,避免直接操作原数据对象
      • Proxy 是对整个对象进行监听和拦截(Object.definProperty() 只能监听单个属性
        复制代码
        const objProxy = new Proxy(obj,{
             get(target,key,receiver){
                   console.log('拦截对象属性的读取')
             },
             set(target,key,value,receiver){
                 console.log('拦截对象属性的设置')
             },
             has(target,key){
                  console.log('判断对象是否含有该属性,返回一个布尔值')
             },
             ownKeys(target){
                console.log('拦截Object.keys(target)、for...in等循环,返回一个数组')
             },
             deleteProperty(target,key){
                   console.log('删除某个属性的操作,返回一个布尔值')
             },
             defineProperty(target,key,desc){
             },
             getPrototypeOf(target){
                  console.log('获取对象的原型')
             },
             setPropertyOf(target,proto){
             }
        })                
        复制代码
      • Reflect:在Proxy内部,调用对象的默认行为时,建议使用Reflect。
      • Reflect 是ES6为了操作对象而提供的新Api,类似于Object的操作方法。
          • Object 对象的一些明显语言内部方法(如Object.defineProperty)放到Reflect对象上。未来的对象的新方法都会往Relfect对象上部署,用于解决Object 方法过于臃肿的问题
          • 让Object 操作变成函数形式,这样更符合语言的规范 
            复制代码
            const obj = {
                  name: 'zhangsan'
            }
            //旧的写法
            'name' in obj  // true
            delete obj['name'] //删除name 属性
            
            //新的写法
            Reflect.has(obj,'name') // true
            Reflect.deleteProperty(obj,'name')
            复制代码
          • Reflect 对象的方法 与 Proxy 对象的方法一一对应,这样Proxy 对象的拦截操作用调用相应的Reflect方法来完成,保证了原生行为能够正常执行
            复制代码
            let obj = {name:'zhangsan',age:20}
            new Proxy(obj,{
                 get(target,name){
                       return Reflect.get(target,name)
                 },
                 set(target,name,val){
                       return Reflect.set(target,name,val)
                },
                deleteProperty(target,name){
                       return Reflect.deleteProperty(target,name)
                },
                has(target,name){
                       return Reflect.has(target,name)
               }
            })
            复制代码

  vue3 用Proxy 响应式的实现:

    1. 件单的Proxy代理实现响应式

复制代码
function reactive(target = {}){
        if(typeof target !== 'object'){
            return target
        }
        const proxyConf = {
            get(target,key,receiver){
                //只处理本身(非原型的)属性
                const ownKeys = Reflect.ownKeys(target)
          console.log(key)
if(ownKeys.includes(key)){ console.log('get',key) } const result = Reflect.get(target,key,receiver) return result }, set(target,key,val,receiver){
          console.log(key)

//重复数据,不处理 if(val === target[key]){ return true } const result = Reflect.set(target,key,val,receiver) return result // 设置是否成功 }, deleteProperty(target,key){ const result = Reflect.deleteProperty(target,key) return result //是否删除成功 } } // 生成代理对象 const observed = new Proxy(target,proxyConf) return observed }
复制代码

 

  测试一下简单数据操作,都能劫持

复制代码
const state = reactive({
    name:'zhangsan',
  info:{
city:'beijing'
}
}) //获取 console.log(state.name) // get name //设置已存在的属性 state.name = 'lisi' // 设置不存在的属性 state.age = 20 // 删除属性 delete state.age //true
复制代码

      如果是嵌套对象的情况,就会有点问题,当你拦截一个深层对象的属性访问时,实际上拦截的是最外层的属性访问

复制代码
const state = reactive({
     name:'zhangsan',
    info:{
         city:'北京' 
   }
})

console.log(state.name ) // get name.      zhangsan
console.log(state.info). // get info           {city:'北京'}
console.log(state.info.city) // get info       北京
复制代码

  上面state.info.city 的时候,拦截的的是info,

  解决方案:在get中判断如果是对象类型,则再进行一层代理

复制代码
function reactive(target = {}){
        if(typeof target !== 'object'){ // 如果不是对象类型,直接返回
            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)
            }
        }
        // 生成代理对象
        const observed = new Proxy(target,proxyConf)

        return observed
    }
复制代码

  vue2 和 vue3 响应式实现的区别:

      • vue2 通过 Object.defineProperty() 实现响应式的,vue3 是通过reactive 是通过Proxy 实现的 
      • Object.defineProperty 是监听对象的属性,需要遍历对象,对每个属性进行监听;Proxy 是对整个对象进行监听,不需要再遍历属性了
      • Object.defineProperty 对新增的属性和删除的属性不能见听到;而Proxy 可以拦截到
      • 嵌套对象,Object.defineProperty需要递归实现;而proxy 是在get 读取的时候递归
      • Object.defineProperty 不支持数组,数组的方法需要重写;Proxy可以拦截到对数组的操作

十九、编译优化

vue3 在编译阶段做了进一步优化。主要有:

  • diff算法优化
    • 编译模板时,为动态节点添加一个标记
      <div>HelWorldlo </div>
      <div>{{message}}</div>
      <div :class="name"></div>

      上面的代码会变编译成:

      复制代码
      import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
      
      export function render(_ctx, _cache, $props, $setup, $data, $options) {
        return (_openBlock(), _createElementBlock(_Fragment, null, [
          _cache[0] || (_cache[0] = _createElementVNode("div", null, "HelWorldlo ", -1 /* HOISTED */)),
          _createElementVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
          _createElementVNode("div", {
            class: _normalizeClass(_ctx.name)
          }, null, 2 /* CLASS */)
        ], 64 /* STABLE_FRAGMENT */))
      }
      
      // Check the console for the AST
      复制代码
      _createElementVNode()最后面的数组1、2 就是静态标记 path flag (-1:表示静态的,永不会用做diff,1:代表动态文本节点,2:代表动态的calss).
    • 标记分为不同类型,静态类型枚举如下:
      复制代码
      export const enum PatchFlags {
        TEXT = 1,// 动态的文本节点
        CLASS = 1 << 1,  // 2 动态的 class
        STYLE = 1 << 2,  // 4 动态的 style
        PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
        FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
        HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
        STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
        KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
        UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
        NEED_PATCH = 1 << 9,   // 512
        DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
        HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
        BAIL = -2 // 一个特殊的标志,指代差异算法
      }
      复制代码
    • diff 算法时,根据类型标记(patch flag)数字来检查来确定不同类型的动态节点,来做相应的操作。

    

         由上图可知,vue3中,静态的文本将不会进行比较,只比较打了静态标记的动态文本

  • 静态提升 hoistStatic
    • vue3 在编译过程中会对静态内容(模板中永远不会改变的内容)进行提升
    • 将创建静态内容的vnode函数,提升到这个模板的渲染函数之外,缓存起来,在重新渲染时重用这个节点,例如:
      <div>
        <div>foo</div> <!-- 需提升 -->
        <div>bar</div> <!-- 需提升 -->
        <div>{{ dynamic }}</div>
      </div>

      上面代码foo 和 bar 这两个 div 是完全静态的,没有必要在重新渲染时再次创建,所以编译时对完全静态的元素进行了缓存

      复制代码
      import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
      
      export function render(_ctx, _cache, $props, $setup, $data, $options) {
        return (_openBlock(), _createElementBlock(_Fragment, null, [
          _cache[0] || (_cache[0] = _createElementVNode("div", null, "foo ", -1 /* HOISTED */)),
          _cache[1] || (_cache[1] = _createElementVNode("div", null, "HelWorldlo ", -1 /* HOISTED */)),
          _createElementVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
        ], 64 /* STABLE_FRAGMENT */))
      }
      
      // Check the console for the AST
      复制代码
      _cache[0] ||_cache[0] = _createElementVNode("div", null, "foo ", -1 /* HOISTED */) 将静态文本的创建给缓存起来,如果有直接取缓存的内容
  • 事件监听缓存 cachHandler
    • 绑定事件行为会被视为动态绑定,每次都会重新定义
    • vue3 编译时将事件定义赋值给一个变量缓存起来,下次重新染时直接复用

     没开启事件监听缓存

export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
                                             // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
  ]))
})

               开启事件侦听器缓存

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
    }, "点我")
  ]))
}
  • 服务端渲染(SSR)方面也进行了优化
    • 静态节点直接输出,绕过了vdom
    • 动态节点,还是需要动态渲染
      复制代码
      <div>
          <div>
              <span>你好</span>
          </div>
          ...  // 很多个静态属性
          <div>
              <span>{{ message }}</span>
          </div>
      </div>
      复制代码

      上面代码中编译后:

      复制代码
      import { mergeProps as _mergeProps } from "vue"
      import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"
      
      export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
        const _cssVars = { style: { color: _ctx.color }}
        _push(`<div${
          _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
        }><div><span>你好</span></div> ... // 很多个静态属性 <div><span>${
          _ssrInterpolate(_ctx.message)
        }</span></div></div>`)
      }
      
      // Check the console for the AST
      复制代码

      从上面可以看出,静态节点直接输出了。这样减少了服务端的计算量,提升了SSR的速度

        

二十、源码体积优化

  • 移除一些不常用的API(如: filter、inline-template)
  • Tree shaking 优化
    • vue2 中,vue源代码是使用CommonJS 格式编写的,所以不支持tree shaking。这意味着,即使你用了vue一部分功能,最总打包仍会包含整个vue库代码
    • vue3中,源代码被重写为使用 ES Module 格式,支持Tree Shaking。
    • Tree shanking 在编译阶段利用 ES6 Moudule (import 、 export)判断哪些模块已经加载,哪些模块和变量未被使用或引用,进而删除对应的代码
    • 如:ref、reactive、computed等等 仅仅在用到的时候才引入,没有用到不会被引入。
    • 这样就会减少最终的打包体积
    • <script setup>
      import {ref,computed} from 'vue'
      
      const countRef = ref(0)
      
      const ReadCount = computed(()=> countRef.value++)
      </script>

      上面代码在打包时,会将ref、computed 功能函数打包进去,其他没有用到的则去掉

二十一、Vite 为什么启动非常快

  什么是Vite

      • 前端打包工具,Vue 作者发起的项目
      • 借助Vue的影响力,和webpack 竞争
      • 优势:开发环境下无需打包,启动快。(注意:生产环境还是需要打包的)        

  Vite 为什么启动非常快

      • 开发环境使用 ES6 Module,无需打包。所以非常快
      • 生产环境使用rollup,并不会快很多      

二十二、 ES6 Module 在浏览器中的应用

  •  一个模块就是一个独立的文件,模块的作用域只在模块内,外面无法访问
  •  只有通过export、import 申明的才可以外部访问,有很好的保护作用
  •   在HTML中使用模块,需要<script type="module">来告诉浏览器这里需要当作模块来对待(包括内联代码和外部引用)
    <script type="module" src="./foo.js">
    <script type="module">
       // ....
    </script>
  •   export 导出
    • 单个导出
      export const a = 1
      export function b() {}
    • 集体导出
      复制代码
      const a = 1
      function add(a,b){
         return a-b
      }
      
      function subtract (a,b){
        return a - b
      }
      
      
      export {
        a,
         add,
        subtract
      }
      复制代码
    • 默认导出 export default:  
      • 导入时不需要花括号
      • 只暴露一个主要的接口,比如一个构造函数 或 一个对象
      • default 只能用一次
        复制代码
        calss User{
            constructor(name,age){
               this.name = name
               this.age = age
            }
            greet (){
            }
        }
        
        export default User
        复制代码
    • 导出时可以重命名:使用as关键字给导出的内容重新命名
      const a = 1
      
      function add(me){}
      
      export { 
            a as newName,
            add
      }
       
  •   import 导入
    • 导入具体的导出
      import {a,add} from './module'
    • 导入所有导出:使用 * as ,将整个模块的所有导出, 导入为一个对象
      import * as myModule from './module'
      console.log(myModule.add)
      console.log(myModule.a)

      将 moudle 中的所有导出(a、add、subtract),导入到module 这个对象中

    • 导入 默认导出(export default )时:不需要使用大括号 
      复制代码
      //a.js. 默认导出
      const str = "export default的内容";
      export default str
      
      
      //在另一个文件中的导入方式:
      import StrFile from 'a'; 
      
      //导入的时候没有花括号
      //本质上,a.js文件的export default输出一个叫做default的变量,然后系统允许你引入的时候为其重命名
      复制代码
    • 导入可以重命名:使用 as 关键字 给倒入的变量或函数 重命名
      import {
           newA as a,
          add,
          min as subtract
      } from './module'

       

 

 

 

 

 

 

posted @   yangkangkang  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示