快速入手 Vue3 总结

323 阅读14分钟

Vue3 快速入手


新特性:

  • Performance:性能比 Vue2.x 快 1.2~2倍;
  • Tree shaking support:按需编译,体积比 Vue2.x 更小;
  • Composition API:组合API(类似 React Hooks);
  • Better typeScript support:更好的 TS 支持;
  • Custom Render API:暴露了自定义渲染 API;
  • Fragment、Teleport(Protal)、Suspense:更先进的组件

Vue 3.0 是如何变快的:Performance

diff算法 的优化:

- Vue2.x中的vdom是进行全量的对比;- Vue3.x新增了静态标记(PatchFlag):在与上次虚拟节点进行对比的时候,只对比带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容;- 编译模板时,动态节点会被标记,并且将标记分为不同的类型;在 diff算法 时,可以区分静态节点,以及不同类型的动态节点

释义:在创建 vdom 的时候,会根据 DOM 中的内容会不会发生变化(即: DOM 中的内容是否为动态),添加静态标记!

案例:

<div>
  <p>AHu</p>
  <p>AHu</p>
  <p>AHu</p>
  <p>{{msg}}</p>
</div>

转变为:

export function render(_ctx, _cache, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "AHu"),
    _createVNode("p", null, "AHu"),
    _createVNode("p", null, "AHu"),
    _createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1 /* TEXT */)
  ]))
}

hoistStatic 静态提升:

定义:

- 将 静态节点 的定义,提升到父作用域,缓存起来;(空间换时间)- 多个相邻的 静态节点,会被合并;(编译优化)- 典型的拿空间换时间的优化策略
- Vue2.x中无论元素是否参与更新,每次都会重建,然后再渲染;- Vue3.x中对于不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停地复用

关键字:静态提升

案例:

<div>
  <p>AHu</p>
  <p>AHu</p>
  <p>AHu</p>
  <p>{{msg}}</p>
</div>

静态提升之前:

export function render(_ctx, _cache, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "AHu"),
    _createVNode("p", null, "AHu"),
    _createVNode("p", null, "AHu"),
    _createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1)
  ]))
}

静态提升之后:

const _hoisted_1 = _createVNode("p", null, "AHu", -1)
const _hoisted_2 = _createVNode("p", null, "AHu", -1)
const _hoisted_3 = _createVNode("p", null, "AHu", -1)
​
export function render(_ctx, _cache, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1, // 直接复用
    _hoisted_2, // 直接复用
    _hoisted_3, // 直接复用
    _createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1)
  ]))
}

cacheHandlers 事件侦听器缓存:

- 默认情况下,onClick 会被视为 动态绑定,因此每次都会去追踪它的变化!如果是同一个函数,就没有必要追踪变化,直接缓存起来复用即可

案例:

<div>
  <button @click="onClick">按钮</button>
</div>

事件侦听之前:

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

事件侦听之后:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args))) // 有 _cache[0] 就用 _cache[0],没有就 重新定义一个
    }, "按钮")
  ]))
}

注意:转换之后的代码中,静态标记 patch flag 已消失,则不需要再追踪对比


SSR渲染:

- 静态节点直接输出,绕过了 vdom:
  当有大量静态的内容时候,这些内容会被当作纯字符串推进一个 buffer 里面,即使存在动态的绑定,会通过模板插值嵌入进去。这样一来,会比通过 vdom 来渲染的快上许多;
  当静态内容大到一定量级的时候,会用_createStaticVNode 方法在客户端去生成一个 static node,这些 静态node,会被直接 innerHtml,就不需要创建对象,然后根据对象渲染

案例:

<div>
  <span>AHu</span>
  <span>AHu</span>
  <span>AHu</span>
  <span :id="1">{{msg}}</span>
  <button @click="btnHandler">按钮</button>
</div> 
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttr as _ssrRenderAttr, 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))
  }><span>AHu</span><span>AHu</span><span>AHu</span><span${
    _ssrRenderAttr("id", 1)
  }>${
    _ssrInterpolate(_ctx.msg)
  }</span><button>按钮</button></div>`)
}

Tree shaking support:

- 编译时,根据不同的情况,引入不同的 API


Composition API:

关于 Vite 和 ES Module:

Vite:

Vite 是 Vue 作者开发的一款意图取代 webpack 的工具;
原理:利用 ES6 的 import 会发送请求去加载文件的特性,拦截这些请求,做一些预编译,省去 webpack 冗长的打包时间

安装 Vite:

npm install -g create-vite-app

利用 Vite 创建 Vue3 项目“

create-vite-app projectName

安装依赖,运行项目:

cd projectName
npm install
npm run dev

为什么 Vite 启动快:

- 开发环境使用 ES6 Module,无需打包 —— 非常快;- 生产环境使用 rollup,并不会快很多

ES Module 在浏览器中的应用:

<script type="module">
  // ...
</script>

基本使用:

<script type="module">
  import { add, multi } from './src/math.js'
  console.log('add res', add(10, 20))
  console.log('multi res', multi(10, 20))
</script>

外链形式使用:

<script type="module" src="./src/index.js"></script>

远程引用:

<script type="module">
  import { createStore } from 'https://unpkg.com/...'
  console.log('createStore', createStore)
</script>

动态引入:

<body>
  <p>动态引入</p>
  <button id="btn1">load_1</button>
  <button id="btn2">load_2</button>
</body><script type="module">
  document.getElementById('btn1').addEventListener('click', async () => {
    const add = await import('./src/add.js')
    console.log('add', add) // 注意:方法在 default 中
    const res = add.default(1, 2)
    console.log('add res', res)
  })
  document.getElementById('btn1').addEventListener('click', async () => {
    const { add, multi } = await import('./src/math.js')
    console.log(add, multi)
    console.log('add res', add(10, 20))
    console.log('multi res', multi(10, 20))
  })
</script>

Composition API:

Vue2.x 中,如果需要新增功能:

1、需要在 data 属性中 新增数据;
2、需要在 methods / watch / computed 中 新增业务逻辑

弊端:数据 和 业务逻辑 分散,导致代码不便于管理和维护

Vue3.x 中,提供了 setup() (Composition API 的入口函数):

<template>
  <div>
    <p>{{count}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template>
<script>
  import { ref } from 'vue'
​
  export default {
    name: 'App',
    // setup() 是 Composition API 的入口函数
    setuo() {
      // let count = 0
      // 定义了一个名称叫做 count 的变量,这个变量的初始值为 0
      // 这个变量发生改变之后,Vue 会自动更新
      let count = ref(0) // 是一个对象
      // 在 Composition API 中,如果想定义方法,不用定义到  methods 中,直接定义即可
      function myFn() {
        // console.log(count.value)
        count.value += 1
      }
      // 注意:在 Composition API 中定义的变量/方法,要想在外界使用,必须通过 return {xxx, xxx} 暴露出去
      return { count, myFn }
    }
  }
</script>

注意:

  • ref函数 只能监听 简单类型 的变化,不能监听 复杂类型(数组/对象) 的变化
  • 复杂类型 的变化需要通过 reactive() 进行监听

案例:

<template>
  <div>
    <ul>
      <li v-for="(stu, index) in state.stus" :key="stu.id" @click="remStu(index)">{{stu.name}} - {{stu.age}}</li>
    </ul>
  </div>
</template><script>
  import { reactive } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let state = reactive({
        stus: [
          { id: 1, name: 'zs', age: 10 },
          { id: 2, name: 'ls', age: 20 },
          { id: 3, name: 'ww', age: 30 }
        ]
      })
      function remStu(index) {
        // 注意:此处不再是 this,而是 state
        state.stus = state.stus.filter((stu, idx) => idx !== index)
      }
      
      return { state, remStu }
    }
  }
</script>

利用 Composition API 进行优化:

将 各个功能的 数据 和 业务逻辑 独立开来

<template>
  <div>
    <form>
      <input type="text" v-model="state2.stu.id">
      <input type="text" v-model="state2.stu.name">
      <input type="text" v-model="state2.stu.age">
      <input type="submit" @click="addStu">
    </form>
    <ul>
      <li v-for="(stu, index) in state.stus" :key="stu.id" @click="remStu(index)">{{stu.name}} - {{stu.age}}</li>
    </ul>
  </div>
</template><script>
  import { reactive } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      // 移除学生的数据
      let { state, remStu } = useRemoveStudent()
      // 添加学生的数据
      // 注意:外界传值
      let { state2, addStu } = useAddStudent(state)
      
      return { state, remStu, state2, addStu }
    }
  }
  
  // 移除学生的业务逻辑
  function useRemoveStudent() {
      let state = reactive({
        stus: [
          { id: 1, name: 'zs', age: 10 },
          { id: 2, name: 'ls', age: 20 },
          { id: 3, name: 'ww', age: 30 }
        ]
      })
      function remStu(index) {
        // 注意:此处不再是 this,而是 state
        state.stus = state.stus.filter((stu, idx) => idx !== index)
      }
      
      return { state, remStu }
    }
  
  // 添加学生的业务逻辑
  function useAddStudent(state) {
    let state2 = reactive({
      stu: {
        id: '',
        name: '',
        age: ''
      }
    })
    function addStu(e) {
      e.preventDefault();
      const stu = Object.assign({}, state2.stu)
      state.stus.push(stu)
      state2.stu.id = ''
      state2.stu.name = ''
      state2.stu.age = ''
    }
    
    return { state2, addStu }
  }
</script>

还可以将 业务逻辑 独立成为一个单独的 JS文件:

例如:

// Step1: 注意 依赖
import { reactive } from 'vue'// 添加学生的业务逻辑
  function useAddStudent(state) {
    let state2 = reactive({
      stu: {
        id: '',
        name: '',
        age: ''
      }
    })
    function addStu(e) {
      e.preventDefault();
      const stu = Object.assign({}, state2.stu)
      state.stus.push(stu)
      state2.stu.id = ''
      state2.stu.name = ''
      state2.stu.age = ''
    }
    
    return { state2, addStu }
  }
​
// Step2: 重要 暴露出去
export default useAddStudent
​
// Step3: 然后在 数据 的文件中,引入该文件!!!

Composition API 和 Option API 可以混合使用:

案例:

<template>
  <div>
    <p>{{name}}</p>
    <button @click="myFn1">按钮</button>
    <p>{{age}}</p>
    <button @click="myFn2">按钮</button>
  </div>
</template><script>
  import { ref } from 'vue'
  export default {
    name: 'App',
    data: function() {
      return {
        name: 'AHu'
      }
    },
    methods: {
      myFn1() {
        alert('abc')
      }
    },
    setup() {
      let age = ref(24)
      function myFn2() {
        alert('welcome')
      }
      return { age, myFn2 }
    }
  }
</script>

Composition API (组合 API / 注入 API)的本质:

Vue 在运行时,将 Composition API 当中暴露出去的数据 注入 到 Option API 中

setup() 的执行时机 和 注意点:

setup() 的执行时机:

- 在生命周期中的 beforeCreate 和 Created 之间:
  beforeCreate:组件刚被创建,data 和 methods 还未初始化完成
  setup
  Created:组件刚被创建,data 和 methods 初始化完成
  
  - 生命周期中,setup 代替了 beforeCreate 和 Created

setup() 的注意点:

- 由于在执行 setup() 时,还没有执行 Created 生命周期方法,因此在 setup函数中,无法使用 data 和 methods;
​
- 由于不能在 setup() 中使用 data 和 methods,因此 Vue 为了避免错误地使用,它直接将 setup() 中的 this 修改成了 undefined;
​
- setup() 只能是 同步 的,不能是 异步 的;
​
- setup 和 其他的 Composition API 中没有 this
  在 Options API 中,可以照常使用 this
  在 Composition API 中,可通过 getCurrentInstance() 获取当前实例
<template>
  <p>get instance</p>
</template><script>
  import { onMounted, getCurrentInstance } from 'vue'
  
  export default {
    name: 'GetInstance',
    data() {
      return {
        x: 1,
        y: 2
      }
    },
    setup() {
      console.log('this1', this) // undefined
      
      onMounted(() => {
        console.log('this in onMounted', this) // undefined
        console.log('instance', instance) // x 1
      })
      
      const instance = getCurrentInstance()
      console.log('instance', instance)
      // 注意 setup 在 生命周期 中的位置
      console.log('x', instance.data.x) // undefined
    }
  }
</script>

setup() 的本质:

将传入的数据包装成一个 Proxy 对象

reactive 的 定义 和 注意点:

reactive 的定义:

reactive 是 Vue3.x 中提供的实现响应式数据的方法:
在 Vue2.x 中,响应式数据是通过 Object.defineProperty 来实现的;而在 Vue3.x 中,响应式数据是通过 ES6 的 proxy 来实现的

reactive 的注意点:

1、reactive 的参数必须是 对象(json/arr);
2、如果给 reactive 传递了其他对象,在默认情况下,界面不会自动更新;如果想更新,可以通过重新赋值的方式

案例:

<template>
  <div>
    <p>{{state.time}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { reactive } from 'vue'
  
  export default{
    name: 'App',
    setup() {
      let state = reactive({
        time: new Date()
      })
      function myFn() {
        // 直接修改以前的,界面不会更新
        // state.time.setDate(state.time.getDate() + 1)
        
        // 重新赋值
        const newTime = new Date(state.time.getTime())
        newTime.setDate(state.time.getDate() + 1)
        state.time = newTime
        console.log(state.time)
      }
      return { state, myFn }
    }
  }
</script>

ref 的 定义 和 本质 和 注意点:

ref 的定义:

ref 和 reactive 一样,也是用来实现响应式数据的方法:
但是 reactive 必须传递一个 对象,因此在企业开发中只想让某个变量实现响应式的时候会非常麻烦;
因此,Vue3.x 提供了 ref 方法,实现对 简单值 的 监听

ref 的本质:

ref 的本质其实还是 reactive:
当我们给 ref() 传值之后,ref() 底层逻辑会自动将 ref() 转换成 reactive(),即:ref(24) -> reactive({value: 24})

因此,在为 ref() 绑定的标签赋值时,需要赋值在 value 属性上:

<template>
  <div>
    <p>{{age}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { ref } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let age = ref(24)
      function myFn() {
        age.value = 18
        console.log(age)
      }
      return { age, myFn }
    }
  }
</script>

ref 的注意点:

如果是通过 ref() 创建的数据,那么在 template 中使用的时候不需要再通过 .value 来获取!因为 Vue 会自动给我们添加 .value

ref 和 reactive 的区别:

如果在 template 里使用的是 ref 类型的数据,那么 Vue 会自动帮我们添加 .value;如果使用的是 reactive 类型的数据,Vue 不会自动添加 .value

Vue在解析数据之前,会自动判断这个数据是否为 ref 类型:

通过当前数据的 __v_ref 来判断:
如果有这个私有的属性,并且取值为 true,那么就代表是一个 ref 类型的数据

自主判断:

关键 API:isRefisReactive

<template>
  <div>
    <p>{{age}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { isRef, isReactive } from 'vue'
  
  // import { ref } from 'vue'
  import { reactive } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      // let age = ref(24)
      let age = reactive({value; 24})
      function myFn() {
        console.log(isRef(age)) // false
        console.log(isReactive(age)) // true
      }
      return { age, myFn }
    }
  }
</script>


递归监听 与 非递归监听:

递归监听:

递归监听 的定义:

默认情况下,无论是通过 ref 还是 reactive 都是递归监听
<template>
  <div>
    <p>{{state.value.a}}</p>
    <p>{{state.value.gf.b}}</p>
    <p>{{state.value.gf.f.c}}</p>
    <p>{{state.value.gf.f.s.d}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { ref } from 'vue'
  
  let state = ref({
    a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
  })
  
  function myFn() {
    state.value.a = '1'
    state.value.gf.b = '2'
    state.value.gf.f.c = '3'
    state.value.gf.f.s.d = '4'
  }
  
  return { state, myFn }
</script>

递归监听 存在的问题:

如果数据量比较大,会非常消耗性能:
将数据进行递归,取出每一层级的值,然后将其包装成为 Proxy 对象

非递归监听:shallowRef 和 shallowReactive

定义:

只能监听 第一层级 的数据,无法深度监听更深层级的数据:
只要 第一层级 的数据发生了改变,就会更新 UI(第一层级 没变化,则不会更新 UI)
<template>
  <div>
    <p>{{state.a}}</p>
    <p>{{state.gf.b}}</p>
    <p>{{state.gf.f.c}}</p>
    <p>{{state.gf.f.s.d}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { shallowReactive } from 'vue'
  
  let state = shallowReactive({
    a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
  })
  
  function myFn() {
    state.a = '1'
    state.gf.b = '2'
    state.gf.f.c = '3'
    state.gf.f.s.d = '4'
    
    console.log(state) // Proxy 对象
    console.log(state.gf) // 普通对象
    console.log(state.gf.f) // 普通对象
    console.log(state.gf.f.s) // 普通对象
  }
  
  return { state, myFn }
</script>
<template>
  <div>
    <p>{{state.value.a}}</p>
    <p>{{state.value.gf.b}}</p>
    <p>{{state.value.gf.f.c}}</p>
    <p>{{state.value.gf.f.s.d}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { shallowRef } from 'vue'
  
  let state = shallowRef({
    a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
  })
  
  function myFn() {
    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, myFn }
</script>

非递归监听的 注意点:

如果是通过 shallowRef 创建数据,那么 Vue 监听的是 .value 的变化,并不是第一层的变化
<template>
  <div>
    <p>{{state.value.a}}</p>
    <p>{{state.value.gf.b}}</p>
    <p>{{state.value.gf.f.c}}</p>
    <p>{{state.value.gf.f.s.d}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { shallowRef } from 'vue'
  
  let state = shallowRef({
    a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
  })
  
  function myFn() {
    // state.value.a = '1'
    // state.value.gf.b = '2'
    // state.value.gf.f.c = '3'
    // state.value.gf.f.s.d = '4'
    
    state.value = {
      a: '1',
      gf: {
        b: '2',
        f: {
          c: '3',
          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, myFn }
</script>

如果只想修改某一层的数据:使用 triggerRef

triggerRef:

根据传入的数据,主动更新界面
<template>
  <div>
    <p>{{state.value.a}}</p>
    <p>{{state.value.gf.b}}</p>
    <p>{{state.value.gf.f.c}}</p>
    <p>{{state.value.gf.f.s.d}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { shallowRef, triggerRef } from 'vue'
  
  let state = shallowRef({
    a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
  })
  
  function myFn() {
    state.value.gf.f.s.d = '4'
    triggerRef(state)
    
    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, myFn }
</script>

注意点:

Vue3.x 只提供了 triggerRef 的方法,并没有提供 triggerReactive 的方法!因此,如果是 reactive 类型的数据,无法主动触发界面更新

应用场景:

1、一般情况下,使用 ref 和 reactive 即可;
2、当需要监听的数据量比较大的时候,才能使用 shallowRef 和 shallowReactive

shallowRef 的本质:

shallowRef 的底层逻辑是 shallowReactive:
即:shallowRef(10) -> shallowReactive({value: 10})
因此,如果是通过 shallowRef 创建的数据,它监听的是 .value 的变化!因为底层本质上 value 才是第一层
let state2 = shallowRef({
  a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
})

等同于:

let state2 = shallowReactive({
  value: {
    a: 'a',
    gf: {
      b: 'b',
      f: {
        c: 'c',
        s: {
          d: 'd'
        }
      }
    }
  }
})


toRaw:

前置知识:数据的引用关系

<template>
  <div>
    <p>{{state}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { reactive } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let obj = {name: 'AHu', age: 24}
      let state = reactive(obj)
      
      console.log(obj === state) // false
      
      function myFn() {
        obj.name = 'zs' // 数据会发生变化,但页面不更新
        console.log(obj)
        console.log(state)
        // state.name = 'zs' // 数据会发生变化,同时页面更新
        // console.log(state)
      }
      return { state, myFn }
    }
  }
</script>

state 和 obj 的关系:

【引用关系】state 的本质是一个 Proxy 对象,在这个 Proxy 对象中引用了 obj

state 和 obj 关系的注意点:

- 如果直接修改 obj,那么无法触发界面更新;
- 只有通过包装之后的对象来修改,才会触发界面更新

toRaw 方法:

定义:

ref 或 reactive 中得到 原始数据
<template>
  <div>
    <p>{{state}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { reactive, toRaw } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let obj = {name: 'AHu', age: 24}
      let state = reactive(obj)
      
      let obj2 = toRaw(state)
      
      console.log(obj2 === obj) // true
          
      console.log(obj === state) // false
      
      function myFn() {
        obj.name = 'zs' // 数据会发生变化,但页面不更新
        console.log(obj)
        console.log(state)
        // state.name = 'zs' // 数据会发生变化,同时页面更新
        // console.log(state)
      }
      return { state, myFn }
    }
  }
</script>

作用:

做一些不想被监听的事情(提升性能):
- ref 和 reactive 的数据类型特点,每次修改都会被监听,并更新UI界面!但这样非常消耗性能!
- toRaw 方法可以拿到它的原始数据,对原始数据进行修改,这样 就不会被监听,也不会更新UI界面,进而达到优化性能的目的
<template>
  <div>
    <p>{{state}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { reactive, toRaw } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let obj = {name: 'AHu', age: 24}
      let state = reactive(obj)
      
      let obj2 = toRaw(state)
      
      console.log(obj2 === obj) // true
          
      console.log(obj === state) // false
      
      function myFn() {
        obj2.name = 'zs' // 数据会发生变化,但页面不更新
        console.log(obj2)
        console.log(state)
        // state.name = 'zs' // 数据会发生变化,同时页面更新
        // console.log(state)
      }
      return { state, myFn }
    }
  }
</script>

注意点:

如果想通过 toRaw 拿到 ref 类型的原始数据(创建时传入的那个数据),就必须明确地告诉 toRaw 方法,要获取的是 .value 的值!
- 因为经过 Vue 处理之后,.value 中保存的才是当初创建时传入的那个原始数据
let obj = toRaw(state.value)


markRaw 方法:

定义:

使该数据永远无法被监听
<template>
  <div>
    <p>{{state}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { reactive, markRaw } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let obj = {name: 'AHu', age: 24}
      obj = markRaw(obj)
      let state = reactive(obj)
      function myFn() {
        state.name = 'zs'
      }
      return { state, myFn }
    }
  }
</script>


toRef 方法:

前置知识:ref

<template>
  <div>
    <p>{{state}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { ref } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let obj = {name: 'AHu'}
      let state = ref(obj.name)
      
      function myFn() {
        state.value = 'zs'
        console.log(obj) // name: 'AHu'
        console.log(state) // name: 'zs'
      }
      return { state, myFn }
    }
  }
</script>

结论:

- 如果利用 ref 将某一个对象中的 属性 变成响应式的数据,修改响应式的数据是不会影响到原始数据的;
- 通过 ref 修改的数据,当数据发生改变,会影响以前的数据
- ref 实际上就是 复制:ref(obj.name) -> ref('AHu') -> reactive({value: 'AHu'})
- ref(obj.name)

注意:区分 reactive


toRef:

<template>
  <div>
    <p>{{ageRef}} - {{state.name}} {{state.age}}</p>
  </div>
</template><script>
  import { toRef, reactive } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
      // let state = {
      //   age: 18,
      //   name: 'AHu'
      // }
      
      let state = reactive({
        name: 'AHu',
        age: 24
      })
      
      let ageRef = toRef(state, 'age') // 针对 响应式数据 的 属性
      
      setTimeout(() => {
        state.age = 18
      }, 1500)
      
      setTimeout(() => {
        ageRef.value = 20
      }, 3000)
      
      return { state, ageRef }
    }
  }
</script>

结论:

- 如果利用 toref 将某一个对象中的属性变成响应式的数据,修改响应式的数据是会影响到原始数据的;
- 但是如果响应式的数据是通过 toRef 创建的,那么修改了数据并不会触发 UI 界面的更新;
- toRef 实际上就是 引用;
- toRef(obj, 'name')

注意:

- toRef 针对的是一个响应式对象(reactiv封装)的prop;
- 普通对象 用 reactive 实现响应式,如果该响应式 单独某个数据 需要响应式,用 toRef

ref 和 toRef 的区别:

- ref -> 复制:修改响应式数据不会影响以前的数据;
  toRef -> 引用:修改响应式数据会影响以前的数据
  
- ref -> 数据发生改变,界面就会自动更新;
  toRef -> 数据发生改变,界面不会自动更新【因此,需要先用 reactive】
  
- ref -> ref(obj.name)
  toRef -> toRef(obj, 'name')

toRef 的应用场景:

如果想让 响应式数据 和 以前的数据 关联起来,并且更新响应式数据之后不想更新UI,那么就可以使用 toRef


toRefs 方法:

定义:

- 将响应式对象(reactive封装)转换为 普通对象;- 对象的每个 prop 都对应 ref;- 两者保持引用关系

toRef 和 toRefs 的区别:

- toRef 是针对 某一个单一属性;
- toRefs 是针对 所有(整个)的属性
<template>
  <div>
    <p>{{name}} - {{age}}</p>
  </div>
</template><script>
  import { toRefs, reactive} from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let state = reactive({
        age: 24,
        name: 'AHu'
      })
      
      let stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
      
      // const {age: ageRef, name: nameRef} = stateAsRefs //每个属性,都是 ref 对象
      // return {
      //  ageRef,
      //  nameRef
      //  }
      
      setTimeout(() => {
        state.age = 18
      }, 1500)
      
      return stateAsRefs
    }
  }
</script>

应用场景:合成函数返回响应式对象

function useFeatureX() {
  const state = reactive({
    x: 1,
    y: 2
  })
  
  // 逻辑运行状态,省略 N 行
  
  // 返回时转换为 ref
  return toRefs(state)
}
export default {
  setup() {
    // 可以在不失去响应性的情况下破坏结构
    // 如果直接 解构,会失去 响应性
    const { x, y } = useFeatureX()
    
    return {
      x,
      y
    }
  }
}

reactive、ref、toRef 和 toRefs 的最佳使用方式:

- 用 reactive 做对象的响应式,用 ref 做值类型的响应式【用 reactive 定义】;
​
- setup 中返回 toRefs(state),或者 toRef(state, 'xxx')【用 toRef 或 toRefs 包装后返回】;
​
- ref 的变量命名都用 xxxRef;
​
- 合成函数返回响应式对象时,使用 toRefs【方便解构】

为什么需要 ref:

- 返回 值类型,会丢失响应式;如在 setup、computed、合成函数,都有可能返回 值类型;Vue 如不定义 ref,用户将自造 ref,反而混乱

为什么需要.value:

- ref 是一个对象(不丢失响应式),.value 存储值;
​
- 通过 .value 属性的 getset 实现响应式
​
- 用于 模板、reactive 时,不需要 .value,其他情况都需要
// 错误
function computed(getter) {
  let value = 0
  setTimeout(() => {
    value = getter()
  }, 1500)
}
/*
相当于:
let a = 100
let b = a
a = 200
console.log(b) // 100
*/
​
// 正确
function computed(getter) {
  const ref = {
    value: null
  }
  setTimeout(() => {
    ref.value = getter()
  }, 1500)
  return ref
}
/*
相当于:
let obj1 = {x: 100}
let obj2 = obj1
obj1.x = 200
console.log(obj2.x) // 200
*/

为什么需要 toRef 和 toRefs:

- 初衷:在不丢失响应式的情况下,将对象数据进行 解构;- 前提:针对的是 响应式对象(reactive封装的),而非普通对象;- 注意:不是 创造 响应式,而是 延续 响应式


customRef 方法:

定义:

- 允许自定义一个 ref
- 返回一个 ref 对象,可以显式地控制依赖追踪和触发响应
<template>
  <div>
    <p>{{age}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { ref, customRef } from 'vue'
  
  function myRef(value) {
    return customRef((track, trigger) => {
      return {
        get() {
          track() // 告诉 Vue 这个数据需要追踪变化
          console.log('get', value)
          return value
        },
        set(newValue) {
          console.log('set', newValue)
          value = newValue
          trigger() // 告诉 Vue 触发界面更新
        }
      }
    })
  }
  
  export default {
    name: 'App',
    setup() {
      // let age = ref(18) // reactive({value: 18})
      let age = myRef(18)
      function myFn() {
        age.value += 1
      }
      
      return { age, myFn }
    }
  }
</script>

为什么需要自定义一个 ref:

应用场景:

当发送 网络请求 时,是异步操作,但是 setup() 只能是 同步函数,无法使用 async/await 语法处理 backhell!因此,需要在 自定义的ref 中,将 网络请求 的处理放在 myRef() 中进行
<template>
  <div>
    <ul>
      <li v-for="item in state" :key="item.id">{{item.name}}</li>  
    </ul>
  </div>
</template><script>
  import { ref, customRef } from 'vue'
  
  function myRef(value) {
    return customRef((track, trigger) => {
      fetch(value).then((res) => {
            return res.json()
          }).then((data) => {
            value = data
            trigger() // 获取成功则更新UI界面
          }).catch((err) => {
            console.log(err)
          })
      
      return {
        get() {
          track()
          
          // 注意:不能在 get 中发送网络请求
          // 渲染界面 -> 调用 get -> 发送网路请求 -> 保存数据 -> 更新界面 -> 调用 get
          
          /*
          fetch(value).then((res) => {
            return res.json()
          }).then((data) => {
            value = data
            trigger()
          }).catch((err) => {
            console.log(err)
          })
          */
          
          return value
        },
        set(newValue) {
          value = newValue
          trigger()
        }
      }
    })
  }
  
  export default {
    name: 'App',
    // setup() 只能是一个 同步的函数,不能是一个异步函数
    // 因此 无法采用 async/await 解决 backhell
    setup() {
      /*
      let state = ref([])
      fetch('../public/data.json').then((res) => {
      return res.json()
      }).then((data) => {
      console.log(data)
        state.value = data
      }).catch((err) => {
      console.log(err)
      })
      
      return { state }
      */
      let state = myRef('../public.data.json')
      return { state }
    }
  }
</script>


ref 获取元素:

  - 在 Vue2.x 中,可以通过给元素添加 ref='xxx' 属性,然后在代码中通过 this.$refs.xxx 的方式来获取元素

注意点:

- setup() 的作用时机:
  beforeCreate
  setup
  Created
- 但是在上述生命周期中,$el 还拿不到
- 因此,需要 Mounted 生命周期中 获取元素
<template>
  <div ref="box">
    我是 div
  </div>
</template><script>
  import { ref, onMounted } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let box = ref(null)
      
      onMounted(() => {
        console.log('onMounted', box.value) // 第二个出现,正常值
      })
      
      console.log(box.value) // null
      
      return { box }
    }
  }
</script>


readonly、isreadonly、shallowreadonly:

定义:

- readonly:用于创建一个 只读 的数据,并且是 递归只读;【数据不会发生改变,页面不会发生更新】- shallowReadonly:用于创建一个 第一层数据 只读;【数据会发生改变,页面不会发生更新】- isreadonly:用于 判断是否为 readonly
<template>
  <div>
    <p>{{state.name}}</p>
    <p>{{state.attr.age}}</p>
    <p>{{state.attr.height}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template><script>
  import { readonly, isReadonly, shallowReadonly } from 'vue'
  
  export default {
    name: 'App',
    setup() {
      let state = readonly({name: 'AHu', attr:{age: 24, height: 1.88}})
      function myFn() {
        state.name = 'zs'
        state.attr.age = 18
        state.attr.height = 1.66
        console.log(state) // 此时,数据没改动、页面没改动
      }
      
      return { state, myFn }
    }
  }
</script>

注意点:

const 和 readonly 的区别:

- const:赋值保护,不能给 变量 重新赋值;
- readonly:属性保护,不能给 属性 重新赋值

const:

const value = 123
value = 456
console.log(value) // error
const value = {name: 'AHu', age: 24}
value.name = 'zs'
value.age = 18
console.log(value) // {name: 'zs', age: 18}



Vue3.x 中 响应式数据 的本质:

- 在 Vue2.x 中是通过 Object.defineProperty 来实现响应式数据;
- 在 Vue3.x 中是通过 Proxy 来实现响应式数据

Proxy 的基本使用:(对象)

let data = {
  name: 'AHu',
  age: 24
} // 对象形式
// let data = ['a', 'b', 'c'] // 数组形式let proxyData = new Proxy(data, {
  get(target, key, receiver) {
    let result = Reflect.get(target, key, receiver)
    console.log('get', key)
    return result // 返回结果
  }
  set(target, key, val, receiver) {
    let result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val)
    console.log('result', result) // true / false
    return result // 是否设置成功
  }
  deleteProperty(target, key) {
    let result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    console.log('result', result) // true / false
    return result // 是否删除成功
  }
})

注意点:

- 由于每次 set 不一定仅仅是设置一个数据,因此,Vue 规定,需要在 setreturn true 来判断是否已经设置完成

案例:(数组)

- 例如:push 一个数组,实际上需要改变的两个部分:(即:同一数据处理两次)
       1、arr.push 追加元素进去;
       2、修改 arr.length
// let data = {
  // name: 'AHu',
  // age: 24
// } // 对象形式
let data = ['a', 'b', 'c'] // 数组形式let proxyData = new Proxy(data, {
  get(target, key, receiver) {
    // 只处理本身(非原型的)属性,即:不包含数组的方法,仅仅是数据本身
    const ownKeys = Reflect.ownKeys(target)
    if (ownKeys.includes(key)) {
      console.log('get', key) // 仅仅起到监听的作用
    }
    
    let result = Reflect.get(target, key, receiver)
    return result // 返回结果
  }
  
  set(target, key, val, receiver) {
    // 重复的数据不处理
    const oldVal = target[key]
    if (val === oldVal) {
      return true
    }
  
    let result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val) // set 3 d; 4
    console.log('result', result) // true / false
    return result // 是否设置成功
  }
​
  deleteProperty(target, key) {
    let result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    console.log('result', result) // true / false
    return result // 是否删除成功
  }
})
​
// test
proxyData.push('d')

Reflect 的作用:

- 和 Proxy 的能力 一一对象(包括 API 和 参数);
​
- 规范化、标准化、函数式
  JS中:'a' in obj // true
        delete.obj.b // true
  Reflect中:Reflect.has(obj, 'a') // true
            Reflect.deleteProperty(obj, 'b') // true
            
- 替代掉 Object 上的工具函数(使得 Object 更加纯粹为 数据结构)
  Object: Object.getOwnPropertyNames(obj)
  Reflect: Reflect.ownKeys(obj)

Proxy 实现响应式:

- 深度监听(包括 性能优化);- 可监听 新增/删除 属性;- 可监听 数组变化
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) // 仅仅起到监听的作用
    }
    
    let result = Reflect.get(target, key, receiver)
    // 深度监听
    // 性能的提升:在 get 中进行递归,什么时候用什么时候递归,而不是一次性递归到底
    // 本质:获取到哪一层,那一层才会立即触发响应式,深层次不会立即触发响应式
    return reactive(result) // 返回结果
  }
  
  set(target, key, val, receiver) {
    // 重复的数据不处理
    const oldVal = target[key]
    if (val === oldVal) {
      return true
    }
    
    // 判断是否为新增属性
    const ownKeys = Reflect.ownKeys(target)
    if (ownKeys.includes(key)) {
        console.log('已有的 key', key)
    } else {
        console.log('新增的 key', key)
    }
  
    let result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val) // set 3 d; 4
    console.log('result', result) // true / false
    return result // 是否设置成功
  }
​
  deleteProperty(target, key) {
    let result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    console.log('result', result) // true / false
    return result // 是否删除成功
  }
}
  
  // 生成代理对象
  const observed = new Proxy(target, proxyConf)
  return observed
}
​
// 测试数据
let data = {
  name: 'AHu',
  age: 24,
  info: {
    city: 'chengdu'
  }
}
​
const proxyData = reactive(data)

总结:

- Proxy 能规避 Object.defineProperty 的问题;
​
- Proxy 无法兼容所有浏览器,无法 polyfill;



v-model 参数的用法:

前置知识:Vue2.x 中的 .sync 修饰符:

- 本质上是 update.myPropName 的语法糖,能够更加简单地对 prop 进行“双向绑定”

案例:

子组件中:

this.$emit('update:title', newTitle)

父组件中:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

为了方便起见,使用 .sync 修饰符:

<text-document v-bind:title.sync="doc.title"></text-document>

在 Vue3 中使用 v-model 替换了 .sync:

案例:

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

注意点:

对于所有不带参数的 v-model,需要分别将 prop 和 event 命名更改为 modelValue 和 update:modelValue
<ChildComponent v-model="pageTitle" />
// ChildComponent.vueexport default {
  props: {
    modelValue: String // 以前是`value:String`
  },
  emits: ['update:modelValue'],
  methods: {
    changePageTitle(title) {
      this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)`
    }
  }
}
​



watch 和 watchEffect 的区别:

- 两者都可以监听 data 属性的变化;- watch:需要明确监听哪个属性;【默认初始化不执行】- watchEffect:会根据其中的属性,自动监听其变化【初始化时,执行一次】(因为,收集需要监听的数据)

watch:

<template>
  <p>watch vs watchEffect</p>
  <p>{{numberRef}}</p>
  <p>{{name}} - {{age}}</p>
</template><script>
  import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
  
  export default {
    name: 'Watch',
    setup() {
      let numberRef = ref(100)
      let state = reactive({
        name: 'AHu',
        age: 24
      })
      
      watch(numberRef, (newNumber, oldNumber) => {
        // 注意:无需再写 .value
        console.log('ref watch', newNumber, oldNumber)
      }, {
        immediate: true // 初始化之前就监听【可选】
      })
      
      setTimeout(() => {
        numberRef.value = 200
      }, 1500)
      
      watch (
        // 第一个参数,确定要监听哪个属性(函数形式)
        () => state.age,
        // 第二个参数,回调函数
        (newAge, oldAge) => {
          console.log('state watch', newState, oldState)
        },
        // 第三个参数,配置项【可选】
        {
          immediate: true // 初始化之前就监听
          deep: true // 深度监听
        }
      )
      
      setTimeout(() => {
        state.age = 18
      }, 1500)
      setTimeout(() => {
        state.name = 'zs'
      }, 3000)
      
      return { numberRef, toRefs(state) }
    }
  }
</script>

watchEffect:

<template>
  <p>watch vs watchEffect</p>
  <p>{{numberRef}}</p>
  <p>{{name}} - {{age}}</p>
</template><script>
  import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
  
  export default {
    name: 'Watch',
    setup() {
      let numberRef = ref(100)
      let state = reactive({
        name: 'AHu',
        age: 24
      })
      
      watchEffect(() => {
        // 初始化时,一定会执行一次(收集要监听的数据)
        console.log('hello watchEffect')
      })
      
      watchEffect(() => {
        console.log('state.name', state.name)
      })
      watchEffect(() => {
        console.log('state.name', state.name)
      })
      
      watchEffect(() => {
        console.log('state.name', state.name)
        console.log('state.name', state.name)
      })
      
      setTimeout(() => {
        state.age = 18
      }, 1500)
      setTimeout(() => {
        state.name = 'zs'
      }, 3000)
      
      return { numberRef, toRefs(state) }
    }
  }
</script>