Vue3.0深入剖析

1,962 阅读5分钟

Vue3.0的几大亮点

  1. 性能比vue2.x 快1.2~2倍
  2. 按需编译,体积比vue2.x更小
  3. composition Api (组和api类似React Hooks)
  4. 本身就是ts从写的更好的支持ts
  5. custom Renderer api 暴露了自定义渲染的api
  6. Teleport(Protal),Suspense 更先进的组件

Vue.3.0是如何变快的

1.优化了diff算法

  • Vue2.x中的虚拟dom是进行全量的对比
  • Vue3.x新增了动态标记位(PatchFlag),在与上次虚拟节点进行比较的时候,只对比带有patchflag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容

image.png

Vue2.x中首先 div会和div比较h1和h1比较,p和p比较,这就是全量比较,其中只有p标签改变了值,比较的时候是每个节点对比,其中h1的值是写死的根本不会变,按道理就不应该去比较,所以vue3.0做了优化

image.png

Vue3.0的比较只比较和追踪带有标记位的

vue-next-template-explorer.netlify.app/

代码示例:

<div>
     <h1>这是一个测试</h1>
     <h1>这是一个测试</h1>
     <h1>这是一个测试</h1>
     <p>{{msg}}</p>
</div>
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h1", null, "这是一个测试"),
    _createElementVNode("h1", null, "这是一个测试"),
    _createElementVNode("h1", null, "这是一个测试"),
    _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST

image.png

在创建虚拟Dom节点的时候,只有p标签插入了一个静态标记位1

下列为标记值所代表的含义:

export const enum PatchFlags {

  TEXT = 1, *//动态文字内容*

  CLASS = 1 << 1,  *// 2  动态 class*

  STYLE = 1 << 2,  *//4 动态样式*

  PROPS = 1 << 3, *//8 动态属性 props (不包含类名和样式)*

  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或者部分子节点带有key*

  UNKEYED_FRAGMENT = 1 << 8,  *//256 子节点没有key的fragment*

  NEED_PATCH = 1 << 9,   *//512 一个节点只会进行非props比较,比如`ref`*

  *// 动态的插槽*

  DYNAMIC_SLOTS = 1 << 10,

   *// SPECIAL FLAGS -------------------------------------------------------------*

   *// 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag*
 
   *// 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff*

  HOISTED = -1,

   *// 用来表示一个节点的diff应该结束*

  BAIL = -2,

}

2. hoistStatic (静态提升)

这个是没有添加静态提升的代码,无论是更新还是不需要更新的每次都要从新生成节点 列如:3个p标签 开启静态提升之前:

image.png 点击右侧的options选中 image.png 开启静态提升以后:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "这是一个测试", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("h1", null, "这是一个测试", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("h1", null, "这是一个测试", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _hoisted_2,
    _hoisted_3,
    _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST

image.png

选中静态提升的时候,h1的虚拟节点被从render方法中提升到全局变量中,这样就只创建一次,每次render的时候直接复用

3.事件侦听缓存

随便搞个栗子,开启事件侦听缓存之前:

<div>
     <button @click='clickEvent'></button>
</div>
 import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = ["onClick"]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", { onClick: _ctx.clickEvent }, null, 8 /* PROPS */, _hoisted_1)
  ]))
}

// Check the console for the AST

image.png

可以看到这个标记位的值是8,被当成了动态属性,这样就会被对比,这里每次都是同一个方法没必要去进行对比

开启事件侦听缓存之后:

image.png

image.png

标记位没有了,没有被追踪就没有产生对比了

Vue3.0的diff算法中只有有静态标记位的才会对比才会追踪。

Vue中ref和reactive的对比:

reactive

  •  reactive 是vue3.0提供的响应式数据的方法
  •  reactive 是通过es6中的proxy来实现的,接收的参数必须是对象(json/arr)传入其他的值不会被监听
  • 如果给reactive传递其他对象,默认情况下修改对象,视图不会更新,想要更新可以通过从新赋值的方式.如传入的是时间对象,值其实更新了,视图并灭有改变,想要改变就从新赋值

image.png

image.png

这样视图是没有更新的 **从新赋值以后视图更新

 setup(){
    const state=reactive({time:new Date()})
    const change=()=>{
      //时间从新赋值
      let newTime=new Date(state.time.getTime());

      newTime.setDate(state.time.getDate()+1);
      state.time=newTime;
       console.log(state.time)
    }
    return {
       ...toRefs(state),
       change

    }
  }

image.png

Ref

  • 1.Ref底层的本质还是 reactive 系统会自动根据我们给ref传入的值将它转化为
  • Ref(xx)------->reactive({value:xxx})
  • 2. 在模板中获取ref 的值不用通过value,在js中需要,那为什么reactive就不用.value去修改值呢,它是怎么判断的呢,vue3.0的底层会把ref包装的数据加入一个私有属性__v_isRef,我们是没法访问私有属性的,想判断是ref还是reactive包装的数据类型,vue3.0提供了isRef()和isReactive()

image.png

  • 3.之所会有ref是因为reactive只监听对象或者数组,想监听某个变量比较繁琐,所以就有了ref可以单独监听某个变量
  • 4.Ref和reactive创建的数据都是递归监听的,它们会将每一层都包装成proxy对象,在修改大体量的数据的时候就会比较消耗性能,所以Vue3.0就衍生了shallowReactive和shallowRef以及triggerRef

shallowReactive、shallowRef、triggerRef的使用

shallowReactive和shallowRef属于非递归监听,shallowReactive只监听了第一层的数据,且只将第一层包装成了proxy对象

shallowReactive

<template>
  <div class="home">
     <div>{{a}}</div>
     <div>{{gf.b}}</div>
     <div>{{gf.f.c}}</div>
     <div>{{gf.f.s.d}}</div>
    <button @click='change'>改变</button>
    <div></div>
  </div>
</template>
<script>
import {reactive,toRefs,ref,shallowReactive} from 'vue'
export default {
  name: 'Home',
  setup(){
    const state=shallowReactive({
      a:"a",
      gf:{
        b:'b',
          f:{
            c:'c',
            s:{
              d:'d'
            }
          }
        }
    })
    const change=()=>{
       state.a=1;
       state.gf.b=2;
       state.gf.f.c=3;
       state.gf.f.s.d=4;
       console.log(state)
       console.log(state.gf)
       console.log(state.gf.f)
       console.log(state.gf.f.s)
    }
    return {
       ...toRefs(state),
       change

    }
  }
}
</script>

image.png

之所以子集的值也发生改变是因为第一层的值发生改变了会被监听,视图会被更新,如果注释掉 state.a=1;视图将不再更新,因为监听不到

shallowRef监听的是.value的变化并不是第一层的变化

<template>
  <div class="home">
     <div>{{state.a}}</div>
     <div>{{state.gf.b}}</div>
     <div>{{state.gf.f.c}}</div>
     <div>{{state.gf.f.s.d}}</div>
    <button @click='change'>改变</button>
  </div>
</template>
<script>
import {reactive,toRefs,ref,shallowReactive,shallowRef} from 'vue'
export default {
  name: 'Home',
  setup(){
    // const state=shallowReactive({
      const state= shallowRef({
        a:"a",
        gf:{
          b:'b',
            f:{
              c:'c',
              s:{
                d:'d'
              }
            }
          }
      })
    const change=()=>{
      //  state.a=1;
      //  state.gf.b=2;
      //  state.gf.f.c=3;
      //  state.gf.f.s.d=4;
      state.value.a=1;
       state.value.gf.b=2;
       state.value.gf.f.c=3;
       state.value.gf.f.s.d=4;
       console.log(state)
      console.log(state.value)
       console.log(state.value.gf)
       console.log(state.value.gf.f)
       console.log(state.value.gf.f.s)
    }
    return {
       state,
       change

    }
  }
}
</script>

image.png

修改的值并没有发生变化,当修改value值的时候,值发生了变化

image.png

image.png

如果我只想修改第4层级的值,这样修改整个对象是不是有点扯,vue3.0就提供了triggerRef()函数,这个类似强制视图更新的意思,不加这个函数视图不会更新,有点像2.0中那个 $emit()这个函数,修改一个数组或者嵌套多级的对象,2.0中视图不会更新,我们就是用$emit()去更新的视图

image.png

image.png

手动实现一个reactive

reactived的底层就是用递归将对象的每一层都包装成Proxy对象

 function reactive(obj){
    if(typeof obj==='object'){
        if(obj instanceof Array){//如果是一个数组 
            obj.forEach((item,index)=>{//取出数组的每一个元素包装
                if(typeof item ==='object'){//判断每一个元素是否是一个对象,
                    obj[index]=reactive(item)//如果是一个对象也需要包装成Proxy
                }
            })
        }else{//如果是一个对象----
            for(let key in obj){//取出对象属性的值
                let item=obj[key];
                if(typeof item ==='object'){//判断对象的取值是否又是一个对象
                    obj[key]=reactive(item)//如果是一个对象也需要包装成Proxy
                }
            }
        }
        return new Proxy(obj,{
            get(obj,key){
                return obj[key]
            },
            set(obj,key,value){
                obj[key]=value;
                console.log('更新ui')
                return true;
            }
       })
   }else{
       console.warn(`${obj} 不是 object`)
   }
}
let obj={
    a:"a",
    gf:{
        b:'b',
        f:{
            c:'c',
            s:{
                d:'d'
            }
        }
    }
}
let state=reactive(obj);
state.a=1;
state.gf.b=2;
state.gf.f.c=3;
state.gf.f.s=4;

image.png

我们要注意在set中一定要return true,我们在set的时候可能会很多次要告知当前这这一次set值是成功的,比如push一个数组,除了在数组的末尾改变一个值以外,还会将数组的length进行改变,这里set就会执行2次。

Pinia在vue2.x中和vue3.0中具体的使用,以及注意点

比Vuex更轻量更友好,所以拥抱吧,真香 官网还没出汉语版的 pinia.esm.dev/cookbook/op…

yarn add pinia
# 或者使用npm
npm install pinia

在main.js中

// 使用pinia
import { createPinia } from 'pinia'
app.use(createPinia())

建立store.js文件

import { defineStore } from 'pinia'

export const useMainStore = defineStore({
  // store
  // 它用于 devtools 并允许恢复状态
  id: 'main',
  // 一个返回新状态的函数
  state: () => ({
     ishow:true,
     list:[]
  }),
  // getters
  getters: {
    getIsshow(state) {
      return this.ishow
    },
  },
  // actions
  actions: {
    getdata() {
        fetch('http://jsonplaceholder.typicode.com/posts')
        .then(response => response.json())
        .then(json =>this.list=json)
    },
  },
})

actions将vuex中mutations合并到了actions,写起来更直观,组件中使用:

import {useMainStore} from '../store/pinia'
import { storeToRefs } from 'pinia'
setup(){
    const store=useMainStore();
      const { list, ishow } = storeToRefs(store);
      //这里如果用解构就必须用storeToRefs包裹,否则数据不会是响应式
      const state=reactive({
        ishow,
        //ishow:computed(()=>store.ishow)
        list
        //list:computed(()=>store.list)
      })
     return {
         ...toRefs(state)
     }
    //actions的使用
    const change=()=>{
        store.getdata()
    }
}

如果你不使用组合API,而使用computed、methods,Pinnia也提供了和Vuex一样的,mapState(),mapActions() 在main.js/ts中 记得安装 1:yarn add pinia@0.5.2 用@next版本会报错,vue2.x官网也提示用v1版本 2:yarn add @vue/composition-api

pinia.js //仓库

import { defineStore } from 'pinia'

export const useMainStore = defineStore({
  // store
  // 它用于 devtools 并允许恢复状态
  id: 'main',
  // 一个返回新状态的函数
  state: () => ({
     ishow:false,
     test:true,
     listData:[]
  }),
  // getters
  getters: {
    getIsshow(state) {
      return this.ishow
    },
  },
  // actions
  actions: {
    getData() {
        fetch('http://jsonplaceholder.typicode.com/posts')
        .then(response => response.json())
        .then(json =>this.listData=json)
    },
    changValue(){
        //等价于 this.ishow=true;   this.test=1113123;
        //$patch可以一次修改多个值
        this.$patch({
            ishow:true,
            test: 1113123,
      })
    }
  },
})
main.js/ts

import { createPinia,PiniaPlugin } from 'pinia'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
Vue.use(PiniaPlugin)
const pinia = createPinia()
new Vue({
  router,
  pinia,
  render: h => h(App)
}).$mount('#app')

组件中使用:

<div @clcik='getData'>
    {{ishow}}
</div>

import { mapState } from 'pinia'
import { mapState,mapActions } from 'pinia'
 computed: {
    // 在组件中可以是用this.counter获取
    // 和使用store.counter获取一样
    ...mapState(useMainStore, ['ishow','test'])
    }),
  },
  methods:{
      //...mapActions(useMainStore, ['getData','changValue'])
      //或者给函数起个别名
      ...mapActions(useMainStore,{newGetData:'getData'})
  }


歇会。。。持续更新