重学vue(2, 3)及其生态+TypeScript 之 vue

在此之前也做了好多个vue3项目,这次通过coderwhy老师的视频,系统的学习一下vue3。

项目一:仿知乎项目 github.com/zhang-glitc

项目二: 数据大屏项目: github.com/zhang-glitc

项目三: 自己构建的个人blog: github.com/zhang-glitc

如何使用Vue呢?

Vue的本质,就是一个JavaScript的库。

  • 方式一:在页面中通过CDN的方式来引入;<script src="https://unpkg.com/vue@next"></script>

  • 方式二:下载Vue的JavaScript文件,并且自己手动引入;

  • 方式三:通过npm包管理工具安装使用它;

  • 方式四:直接通过Vue CLI创建项目,并且使用它; 简单使用 我们可以调用Vue.createApp()来创建一个应用实例,并通过mount将其挂载到指定的dom上。

  <div id="app"></div>

  <script src="https://unpkg.com/vue@next"></script>
  <script>
    const options = {
      template: '<h2>Hello Vue3</h2>'
    }

    const app = Vue.createApp(options);
    app.mount("#app")
  </script>

模板语法

React的开发模式:

  • React使用的jsx,所以对应的代码都是编写的类似于js的一种语法;

  • 之后通过Babel将jsx编译成 React.createElement 函数调用;

Vue也支持jsx的开发模式:

  • 但是大多数情况下,使用基于HTML的模板语法;

  • 在模板中,允许开发者以声明式的方式将DOM和底层组件实例的数据绑定在一起;

  • 在底层的实现中,Vue将模板编译成虚拟DOM渲染函数。

Mustache双大括号语法

如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值。

  • 并且我们前端提到过,data返回的对象是有添加到Vue的响应式系统中;

  • 当data中的数据发生改变时,对应的内容也会发生更新。

  • 当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式。

指令

渲染内容相关指令
  • v-once用于指定元素或者组件只渲染一次:

    • 当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过;

    • 该指令可以用于性能优化;

    • 如果是子节点,也是只会渲染一次。即使用v-once的标签中的内容都只会渲染一次。

  • v-text用于更新元素的 textContent

    • 等价于{{}}

    • 并且他会覆盖标签中的任何内容。

  • v-html用于将html字符串当做html渲染到页面,这个指令一般在个人blog渲染文章用的比较多。

    • 我们通过{{}}展示html字符串时,vue并不会对其进行特殊的解析。仍然渲染成html字符串。

    • 如果我们希望这个内容被Vue可以解析出来,那么可以使用 v-html 来展示;

  • v-pre用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签:

    • 跳过不需要编译的节点,加快编译的速度;

属性相关指令
  • v-bind 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。

    • 缩写:

    • 修饰符

      • .camel 将 kebab-case attribute 名转换为 camelCase。

      • .prop - 将一个绑定强制设置为一个 DOM property。

      • .attr - 将一个绑定强制设置为一个 DOM attribute。

    • 绑定class有两种方式:

      • 对象语法: 传入一个对象作为class的值。key为class属性值,value为一个boolean,看key是否绑定到该元素的class。 如果想使用data中定义的变量作为class值,我们需要使用动态属性绑定[],下面的title将被看作是一个变量,而不是一个字符串。

       <div :class="{active: isActive, [title]: true}">对象形式添加class</div>
      
      
      • 数组语法: 传入一个数组作为class的值。数组中的每个元素作为class属性值。如果遇到元素是表达式或者对象,那么就看其值是否是true。就被添加到class上。 注意如果是元素值不加上引号,那么他将会去定义的data中查找是否有该变量值。例如下面的title

      <div :class="['abc', title, isActive ? 'active': '', {active: isActive}]">
          数组形式添加class
      </div>
      
      
    • 绑定style:某些样式我们需要根据数据动态来决定。

      • CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名

      • 对象语法:等同于写内联的css样式。只不过属性必须是字符串。如果不是字符串,那么他将被当做成变量,然后将去data中查找是否有该变量。

          <div :style="{color: finalColor, 'font-size': '30px'}">对象形式</div>
      
      
      • 数组语法:基本不用。就是将键值对的样式对象当做元素放在数组中。

          <div :style="[style1Obj, style2Obj]">数组形式</div>
          data() {
             return {
               message: "Hello World",
               style1Obj: {
                 color: 'red',
                 fontSize: '30px'
               },
               style2Obj: {
                 textDecoration: "underline"
               }
             }
           }
      
      
    • 动态绑定自定义属性。通过:[自定义属性]的形式。

        <div :[name]="value">动态绑定自定义属性</div>
        data() {
           return {
             name: "cba",
             value: "kobe"
           }
         }
    
    
    • 将对象数据映射到dom元素的属性。这个一般用于将inheritAttrs: false后,将父元素传入的非props属性挂载到指定的dom上。 v-bind="$attrs"

        
       <div v-bind="info">将对象数据映射到dom元素的属性</div>
       <div :="info">将对象数据映射到dom元素的属性</div>
       data() {
           return {
             info: {
               name: "zh",
               age: 20
             }
           }
         }
    
    
事件指令
  • v-on: 用于绑定事件监听。

    • 简写: @

    • 修饰符

      • .stop - 调用 event.stopPropagation()。

      • .prevent - 调用 event.preventDefault()。

      • .capture - 添加事件侦听器时使用 capture 模式。

      • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。

      • .{keyAlias}- 仅当事件是从特定键触发时才触发回调。

      • .once - 只触发一次回调。

      • .lef - 只当点击鼠标左键时触发。

      • .right - 只当点击鼠标右键时触发。

      • .middle - 只当点击鼠标中键时触发。

      • .passive - { passive: true } 模式添加侦听器

    • 开发时基本上都是绑定一个function,但是如果需要绑定多个函数,我们就需要传入一个对象。

       <div  v-on="{click: btn1Click, mousemove: mouseMove}"></div>
       <div  @="{click: btn1Click, mousemove: mouseMove}"></div>
    
    
    • 当通过methods中定义方法,以供@click调用时,需要注意参数问题:

      • 情况一:如果该方法不需要额外参数,那么方法后的()可以不添加。

      但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去

      • 情况二:如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件。

条件渲染相关指令
  • v-if
    • v-if是惰性的。

    • 当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉。

    • 当条件为true时,才会真正渲染条件块中的内容。

    • 如果想要多个dom同时显示或者隐藏,我们可以将v-if写在template标签上,并且让其包裹该多个dom元素。

  • v-else(配合v-if使用)

  • v-else-if(配合v-if使用)

  • v-show
    • v-show是不能添加在template标签上

    • v-show不可以和v-else一起使用。

    • 本质是通过设置css的display的属性值来显示或者隐藏元素的。

列表渲染指令
  • v-for

    • 它既可以遍历对象也可以遍历数组

    • 格式:

      • "value in object / Array / Number";

      • "(value, key) in object / Array / Number";

      • "(value, key, index) in object";

    • v-for同时也支持数字的遍历。

    • 可以使用template来对多个元素进行包裹,而不是使用div来完成。

    • 需要结合key来使用。 v-for中的key是什么作用?

  • key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。

  • 如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。

  • 而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素。

VNode是什么?

VNode的全称是Virtual Node,也就是虚拟节点。事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode,VNode的本质是一个JavaScript的对象。

虚拟DOM?

如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree。然后就组成了虚拟DOM。

下面我们来看一个小案例

vue中的diff算法

没有添加key的处理过程

添加key的处理过程

表单指令
  • v-model: 用于表单数据和提供的数据双向绑定。
    • 在表单 <input><textarea><select> 元素上创建双向数据绑定。

    • 它会根据控件类型自动选取正确的方法来更新元素。

    • 它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。

    • 他的本质就是监听input事件,并且通过事件对象将值赋值给提供的数据。

    • 修饰符
      • .lazy: 将v-model的事件绑定从input转变为change事件。

      • .number: 将v-model绑定的值转化为数字

      • .trim: 将v-model绑定的值两边的空格去除。

    • 如果是复选框和多选框,v-model将给选中的值加入到绑定的数组中。并且每个选项都必须设置value属性。

      <div id="app"></div>
      <template id="my-app">
        
        <label for="intro">
          自我介绍
          <textarea name="intro" id="intro" cols="30" rows="10" v-model="intro"></textarea>
        </label>
        <h2>intro: {{intro}}</h2>

        
        
        <label for="agree">
          <input id="agree" type="checkbox" v-model="isAgree"> 同意协议
        </label>
        <h2>isAgree: {{isAgree}}</h2>

        
        <span>你的爱好: </span>
        <label for="basketball">
          <input id="basketball" type="checkbox" v-model="hobbies" value="basketball"> 篮球
        </label>
        <label for="football">
          <input id="football" type="checkbox" v-model="hobbies" value="football"> 足球
        </label>
        <label for="tennis">
          <input id="tennis" type="checkbox" v-model="hobbies" value="tennis"> 网球
        </label>
        <h2>hobbies: {{hobbies}}</h2>

        
        <span>你的爱好: </span>
        <label for="male">
          <input id="male" type="radio" v-model="gender" value="male">男
        </label>
        <label for="female">
          <input id="female" type="radio" v-model="gender" value="female">女
        </label>
        <h2>gender: {{gender}}</h2>

        
        <span>喜欢的水果: </span>
        <select v-model="fruit" multiple size="2">
          <option value="apple">苹果</option>
          <option value="orange">橘子</option>
          <option value="banana">香蕉</option>
        </select>
        <h2>fruit: {{fruit}}</h2>
      </template>

      <script src="../js/vue.js"></script>
      <script>
        const App = {
          template: '#my-app',
          data() {
            return {
              intro: "Hello World",
              isAgree: false,
              hobbies: ["basketball"],
              gender: "",
              fruit: "orange"
            }
          }
        }

        Vue.createApp(App).mount('#app');
      </script>

在组件中使用v-model指令

我们在表单元素中很容易的使用v-model来做双向绑定。他的原理是通过v-bind:value的数据绑定和@input的事件监听

如果我们想要在自定义组件中使用v-model呢?该如何实现呢?

    <!-- 组件上使用v-model -->
    <hy-input v-model="message"></hy-input>
    
    <hy-input :modelValue="message" @update:model-value="message = $event"></hy-input>

其实在组件中使用v-model,默认情况下其实就是在组件中提供modelValueprops, 并且定义update:modelValue事件。

如果我们想要在表单元素上使用v-model来代替上面的input事件中的属性操作。我们可以借助computed来实现,并且提供getter, setter方法。

    
    <input v-model="updateModelValue">
    
    props: {
      modelValue: String
    },
    emits: ["update:modelValue"],
    computed: {
      updateModelValue: {
        
        set(value) {
          this.$emit("update:modelValue", value);
        },
        get() {
          return this.modelValue;
        }
      }
    },

如果我们想要自定义props来实现在组件上使用v-model,我们需要给v-model传递自定义属性名。

    <hy-input v-model:title="title"></hy-input>
    data() {
      return {
        title: "title"
      }
    }

    
    <input v-model="updateTitle">
    
    props: {
      title: String 
    },
    emits: ["update:title"],
    computed: {
      updateTitle: {
        set(value) {
          this.$emit("update:title", value);
        },
        get() {
          return this.title;
        }
      }
    }

当我们想在自定义组件中绑定多个属性(即使用多个v-model)时,我们就需要通过上面自定义props绑定名来实现了。

    <hy-input v-model="message" v-model:title="title"></hy-input>
    data() {
      return {
        message: "message",
        title: "title"
      }
    }

    
    <input v-model="updateModelValue">
    <input v-model="updateTitle">
    
    props: {
      modelValue: String,
      title: String 
    },
    emits: ["update:modelValue", "update:title"],
    computed: {
      updateModelValue: {
        set(value) {
          this.$emit("update:modelValue", value);
        },
        get() {
          return this.modelValue;
        }
      },
      updateTitle: {
        set(value) {
          this.$emit("update:title", value);
        },
        get() {
          return this.title;
        }
      }
    }


optionsAPI

computed计算属性

我们知道,在模板中可以直接通过插值语法显示一些data中的数据。但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示。

  • 需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示

  • 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算,在模板中放入太多的逻辑会让模板过重和难以维护。所以需要使用计算属性。

  • 如果多个地方都使用到,那么会有大量重复的代码,将它抽离到计算属性中,可以得到重用。 其实,我们也可以通过methods来实现这些逻辑,那为什么要用计算属性呢?他们有什么区别呢?

  • 调用逻辑函数的时候,计算属性不需要写(), 但是methods需要写()

  • 计算属性方法多次使用会有缓存,只会执行一次,再调用就会使用缓存的结果。当引用的数据发生变化他会重新结算结果,并缓存。但是methods方法不会存在缓存,每次调用对应的方法,都会重新执行一遍。 计算属性的gettersetter方法

计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数。但是,如果我们确实想设置计算属性的值呢? 这个时候我们也可以给计算属性设置一个setter的方法,并且调用计算属性函数时,可以传入值。

    methods: {
        handleName() {
        
          this.test = "llm zh"
        }
    },
    computed: {
        test: {
          get() {
            return this.name
          },
          set(value) {
            
            this.name = value
          }
        }
      }

Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢?

事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已

watch监听器

如果我们需要监听数据变化,然后做一些逻辑处理,就需要用到watch了。

如何使用? 默认情况下,监听器只能监听本身数据的变化,内部属性的变化是不能被监听的(对于对象来说)

    watch: {
        
        a(val, oldVal) {
          console.log(`new: ${val}, old: ${oldVal}`)
        }
    }

如果想要监听内部数据(数组或者对象)的变化,我们可以将监听写成一个对象,并且传入一个deep: true属性,来让他深度监听,不管内部嵌套多深,都会被监听到。这里需要注意的是,监听函数的新旧值是一模一样的,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。如果想要使用旧数据,我们需要自己拷贝副本。

    watch: {
        
        c: {
          handler(val, oldVal) {
            console.log('c changed')
          },
          deep: true
        },
    }

如果我们想要立即执行监听器,我们就需要传递一个immediate: true属性。

    watch: {
        
        e: {
          handler(val, oldVal) {
            console.log('e changed')
          },
          immediate: true
        },
    }

我们还可以对一个属性传入多个监听函数。他将被依次调用

     watch: {
        
        f: [
          'handle1', 
          function handle2(val, oldVal) {
            console.log('handle2 triggered')
          },
          {
            handler: function handle3(val, oldVal) {
              console.log('handle3 triggered')
            }
            
          }
        ]
    }

我们还可以单独监听一个对象中的特定属性值的变化。注意:监听函数拿到的新旧值依旧是一样的。都是改变后的新值。而且是整个对象。而非单独监听的这个属性的值。

     watch: {
        
        'c.d': function (val, oldVal) {
          
        }
    }

如果我们想要监听数组中对象属性值的变化,我们不可以像上面那种监听方法,我们或者通过deep: true来深度监听,或者在子组件中监听传递的数组中的每一项

我们还可以调用this.$watch()来监听。并且他会返回一个函数,用于取消监听。

  • 第一个参数是要侦听的源。

  • 第二个参数是侦听的回调函数callback。

  • 第三个参数是额外的其他选项,比如deep、immediate。

    const unwatch = this.$watch("info", 
        function (newInfo, oldInfo) {
          console.log(newInfo, oldInfo);
        }, 
        {
          deep: true,
          immediate: true
        }
     )
     
     
     unwatch()

mixins 混入

组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。这个属性对于vue2中代码抽离和复用,非常有效。但是vue3中我们可以使用另外的方式来对代码进行抽离复用

在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:

  • Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能。

  • 一个Mixin对象可以包含任何组件选项。

  • 当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中。 如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?

这里分成不同的情况来进行处理;

  • 情况一:如果是data函数的返回值对象 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据。

  • 情况二:混入生命周期钩子函数

生命周期的钩子函数会被合并到数组中,都会被调用。

  • 情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
    • 比如都有methods选项,并且都定义了方法,那么它们都会生效;

    • 但是如果对象的key相同,那么会取组件对象的键值对; 如果每个组件都需要用到一段相同的逻辑,那么我们就可以使用全局混入。

  • 全局的Mixin可以使用 应用app的方法 mixin 来完成注册。

  • 一旦注册,那么全局混入的选项将会影响每一个组件。

    app.mixin(混入的对象)

混入的代码和该组件中本身的代码执行顺序:全局混入 > 混入 > 自身组件中的代码。

Vue的组件化

我们将一个完整的页面分成很多个组件,每个组件都用于实现页面的一个功能块,而每一个组件又可以进行细分,而组件本身又可以在多个地方进行复用。前面我们的createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件。

vue中的组件其实很简单,官网讲的很详细。

v3.cn.vuejs.org/guide/compo…[4]

但是有很多需要注意的地方。接下来我们就介绍一下:

props约束

  • 当传递的是对象或者数组,我们指定默认值必须是一个工厂函数。并且返回默认值对象和数组。

  • 我们可以通过数组来表示可以是多个类型。

  • 我们还可以通过validator检验函数来自定义约束类型。

  • Prop 的大小写命名,最好使用-链接命名。

非props属性处理

当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute。

  • 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中

  • 如果我们不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false
    • 禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素。

    • 我们可以通过 $attrs来访问所有的 非props的attribute。

  • 多个根节点的attribute
    • 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上。

子组件向父组件传参

我们可以通过emits来对传递的事件参数进行校验,如果出现不符合的,将会出现警告。

如果我们徐想要校验参数,直接写数组就行。

     emits: ["add", "sub", "addN"]

全局事件总线

主要用在非父子组件传递参数。

Vue3从实例中移除了 on、on、off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库mitt

    
    import mitt from 'mitt';

    const emitter = mitt();
    export default emitter;

注册和监听事件

    
    emitter.emit("字符串事件名", 参数) 
    
    emitter.on('字符串事件名', 回调函数)
    
    emitter.on('*', (事件类型, 对应事件传递的参数) => {})

移除事件

    
    emitter.all.clear()
    
    emitter.off("事件名", 移除事件的引用)

vite的简单介绍

下一代前端开发与构建工具。

他是解决上一代构建工具的问题:

  • 在实际开发中,我们编写的代码往往是不能被浏览器直接识别的,比如ES6、TypeScript、Vue文件等等。所以我们必须通过构建工具来对代码进行转换、编译,类似的工具有webpack、rollup、parcel。

  • 随着项目越来越大,需要处理的JavaScript呈指数级增长,模块越来越多。

  • 构建工具需要很长的时间才能开启服务器,HMR也需要几秒钟才能在浏览器反应出来。

  • 开发阶段不需要对代码做过多的适配,并且将浏览器不能识别的文件都转化为esModule文件,提升构建速度,开发效率提升。当项目打包时,在对项目做适配。

它主要由两部分组成:

  • 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;

  • 一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;

如果我们不借助于其他工具,直接使用ES Module来开发有什么问题呢?

  • 首先,当加载一个库时,加载了这个库的所有依赖模块的js代码,对于浏览器发送请求是巨大的消耗。

  • 其次,我们的代码中如果有TypeScript、less、vue等代码时,浏览器并不能直接识别。

多以上述问题就需要vite来解决。

现在先安装vite

    npm install vite –g  

    npm install vite –D  

Vite对css的支持

  • vite可以直接支持css的处理

  • vite可以直接支持css预处理器,比如less,sass

    • 但是需要安装less,sass编译器

        npm install less -D
        npm install sass -D
    
    
  • vite直接支持postcss的转换:

    • 只需要安装postcss,并且配置 postcss.config.js 的配置文件即可

        npm install postcss postcss-preset-env -D
    
    
        
        module.exports = {
          plugins: [
            require("postcss-preset-env")
          ]
        }
    
    

vite对Typescript的支持

  • vite对TypeScript是原生支持的,它会直接使用ESBuild来完成编译:

  • 只需要直接导入即可。

如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:

这是因为vite中的服务器Connect会对我们的请求进行转发,获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析。 注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器

Vite对vue的支持

  • Vue 3 单文件组件插件支持:@vitejs/plugin-vue

  • Vue 3 JSX 插件支持:@vitejs/plugin-vue-jsx

  • Vue 2 插件支持:underfin/vite-plugin-vue2 在vite.config.js中配置插件:

    const vue = require('@vitejs/plugin-vue')

    module.exports = {
      plugins: [
        vue()
      ]
    }

上述配置完成后,我们引入.vue文件,并启动项目,会报错。这时候需要我们安装@vue/compiler-sfc插件即可。

Vite脚手架工具

执行以下命令即可创建一个完整的vue项目。

    npm install @vitejs/create-app -g

    create-app 项目名

插槽

插槽的作用

通过props传递给组件一些数据,让组件来进行展示,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。我们可以定义插槽,让外部可以自定义展示的内容和元素。

插槽的使用

插槽的使用过程其实是抽取共性、预留不同。我们会将共同的元素、内容依然在组件内进行封装。同时会将不同的元素使用slot标签作为占位,让外部决定到底显示什么样的元素。

具体使用可以访问官网。

v3.cn.vuejs.org/guide/compo…[5]

下面我们来介绍插槽使用的需要注意什么。

注意事项

  • 如果想要给插槽做样式定义,我们需要给slot标签包裹上一个div元素,如果直接在slot标签上写class,那么插槽替换的内容将替代完整的slot插槽。

  • 除了默认插槽外,我们传入内容是都需要在template标签上指定插槽的名称。

  • 可以通过 v-slot:[SlotName]方式动态绑定一个名称。

  • 插槽不能访问提供插槽的组件中的属性。

  • 如果我们渲染插槽的时候,需要用到子组件中的数据,我们就可以通过作用域插槽来将数据传递到父组件进行使用。数据放在一个对象中,并且将作为v-slot指令的值。

     
     <slot :item="item" :index="index"></slot>

动态组件

我们如果需要根据条件切换组件,我们就可以使用component标签。并指定一个is属性。其中is属性的值:

  • 可以是通过component函数注册的组件。

  • 在一个组件对象的components对象中注册的组件。 所以切换组件,我们就可以改变is属性中的值。

    并且,我们还可以将组件中的props和事件写入component组件进行传递。当渲染对应的组件时,就会将props和事件传入到对应的组件中。

    <component :is="currentTab"
             name="zh"
             :age="20"
             @pageClick="pageClick">
    </component>

组件缓存

默认情况下,我们每次离开一个组件时,该组件都会被销毁,有时候,我们希望保持组件的状态。所以就需要使用keep-alive组件来包裹住需要缓存的组件。只要被包裹后,该组件中的状态就不会消失。

keep-alive有一些属性:**(这些一般用于动态组件,路由使用。单个组件包裹一般不需要)**

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存。

  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。

  • max - number | string。最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁。

include 和 exclude prop 允许组件有条件地缓存:

  • 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。

  • 匹配首先检查组件自身的 name 选项。

    如果我们想要在组件缓存进入和离开之前做一些事情的时候,我们不能调用create, unmounted钩子函数,但是vue给我们内置了用activateddeactivated 这两个生命周期钩子函数。

    activated() {
      console.log("about activated");
    },
    deactivated() {
      console.log("about deactivated");
    }

异步组件

如果我们的项目过大了,对于某些组件我们希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue中给我们提供了一个函数:defineAsyncComponent

defineAsyncComponent接受两种类型的参数:

  • 类型一:工厂函数,该工厂函数需要返回一个Promise对象。

    defineAsyncComponent(() => import("./AsyncCategory.vue"))

上面的import函数返回的就是一个promise对象。是es6的语法。

  • 类型二:接受一个对象类型,对异步函数进行配置。

    import { defineAsyncComponent } from 'vue'

    const AsyncComp = defineAsyncComponent({
    
    loader: () => import('./Foo.vue')
    
    loadingComponent: LoadingComponent,
    
    errorComponent: ErrorComponent,
    
    delay: 200,
    
    
    timeout: 3000,
    
    suspensible: false,
    
    *
       * @param {*} error 错误信息对象
       * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
       * @param {*} fail  一个函数,指示加载程序结束退出
       * @param {*} attempts 允许的最大重试次数
       */
      onError(error, retry, fail, attempts) {
        if (error.message.match(/fetch/) && attempts <= 3) {
          
          retry()
        } else {
          
          
          fail()
        }
      }
    })

结合Suspense组件使用

与之结合使用后,他会忽略提供的异步组件中的加载、错误、延迟和超时选项。 suspense组件,具有两个插槽。而且两个插槽只能有一个直接子节点。

  • default默认插槽,用来显示异步组件。

  • fallback插槽,用来显示加载时的组件。

    <suspense>
      <template #default>
        // 异步组件
        <async-category></async-category>
      </template>
      <template #fallback>
        <loading></loading>
      </template>
    </suspense>

teleport组件

在组件化开发中,我们封装一个组件A,在另外一个组件B中使用。那么组件A中template的元素,会被挂载到组件B中template的某个位置。最终我们的应用程序会形成一颗DOM树结构。

但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置。比如移动到body元素上,或者我们有其他的div#app之外的元素上。这个时候我们就可以通过teleport来完成。

Teleport是什么呢?

它是一个Vue提供的内置组件,类似于react的Portals。teleport翻译过来是心灵传输、远距离运输的意思。

它有两个属性:

  • to:指定将其中的内容移动到的目标元素,可以使用选择器。

  • disabled:是否禁用 teleport 的功能。 但是他仍然是使用方的子组件。仍然可以传递props。同一个目标父组件可以挂载多个teleport传递的组件。按先后顺序插入。

    <teleport to="#zh">
      <hello-world :name="helloVal"></hello-world>
    </teleport>
    <teleport to="#zh">
      <p>另外一个组件</p>
    </teleport>
    
      components: {
        HelloWorld,
      },
      setup() {
        const helloVal = ref('hello')
        return {
          helloVal,
        }
      }

  <div>
    <h2>{{name}}</h2>
  </div>

  props: ['name']
}

vue生命周期

每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程。在这个过程中的某一个阶段,用于可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据)。

但是我们如何可以知道目前组件正在哪一个过程呢?

Vue给我们提供了组件的生命周期函数,生命周期钩子的 this 上下文将自动绑定至实例中,因此你可以访问 data、computed 和 methods。

动画

Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果。

没有动画的情况下,整个内容的显示和隐藏会非常的生硬: 如果我们希望给单元素或者组件实现过渡动画,可以使用 transition 内置组件来完成动画。

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:

  • 条件渲染 (使用 v-if)条件展示 (使用 v-show)

  • 动态组件

  • 组件根节点

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:

  • 自动嗅探目标元素是否应用了**CSS过渡(transition)或者动画(animation)**,如果有,那么在恰当的时机添加/删除 CSS类名。

  • 如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用。

  • 如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行。将不会有动画效果。

    <button @click="isShow = !isShow">显示/隐藏</button>

    <transition name="zh">
      <h2 v-if="isShow">Hello World</h2>
    </transition>
    
    <style scoped>
        .zh-enter-from,
        .zh-leave-to {
          opacity: 0;
        }

        .zh-enter-to,
        .zh-leave-from {
          opacity: 1;
        }

        // 为添加过度属性,将不会出现动画效果。
        .zh-enter-active,
        .zh-leave-active {
          
        }
    </style>

class属性添加时机

过渡动画class

Vue就是帮助我们在这些class之间来回切换完成的动画:

  • v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

  • v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

  • v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。

  • v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

  • v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

  • v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。 当我们没有对transition组件命名时,即给出name属性,那么它将默认使用v当做name属性值。即所有的class是以 v- 作为默认前缀。给定name属性后,将会根据name属性值多为class前缀。

css动画和css过度

vue中,完成动画我们需要借助动画animation和过度transition来完成。我们需要在 v--enter-activev-leave-active class属性值中定义上面两个属性即可。

    .zh-enter-from,
    .zh-leave-to {
      opacity: 0;
    }

    .zh-enter-to,
    .zh-leave-from {
      opacity: 1;
    }

    .zh-enter-active,
    .zh-leave-active {
      transition: opacity 2s ease;
    }

  .zh-enter-active {
    animation: bounce 1s ease;
  }

  .zh-leave-active {
    animation: bounce 1s ease reverse;
  }

  @keyframes bounce {
    0% {
      transform: scale(0)
    }

    50% {
      transform: scale(1.2);
    }

    100% {
      transform: scale(1);
    }
  }

transition组件属性

name属性

为了设置class前缀。

type属性

Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则:

  • 如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听。 但是如果我们同时使用了过渡和动画呢?

  • 并且在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束。

  • 在这种情况下,我们可以设置 type 属性为 animation 或者 transition 来明确的告知Vue监听的类型。并且可以通过duration显式的指定动画时间。

    <transition name="zh" type="transition" :duration="{enter: 800, leave: 1000}">
      <h2 class="title" v-if="isShow">Hello World</h2>
    </transition>

duration属性

设置过度动画的时间。

duration可以设置两种类型的值:

  • pnumber类型:同时设置进入和离开的过渡时间。

  • pobject类型:分别设置进入和离开的过渡时间。 注意:显式设置的值会覆盖css过度和动画中指定的值。

mode属性

默认情况下,进入和离开同时发生。如果想改变默认状态。我们就需要添加mode属性。

  • in-out: 新元素先进行过渡,完成之后当前元素过渡离开。

  • out-in: 当前元素先进行过渡,完成之后新元素过渡进入。 大多数情况下,我们需要前一个动画结束时,后一个动画开始。所以需要使用out-in

appear属性

默认情况下,首次渲染的时候是没有动画的,如果我们希望给他添加上去动画,那么就可以增加appear;ture

css属性

当我们通过js来操作动画的时候,我们就不需要vue来检测css中的动画了。所以需要将css设置为false。默认情况下css: true

JavaScript 钩子

当我们想要在动画执行的各个阶段,做一些事情。我们就可以使用这个钩子。

  • @before-enter="beforeEnter",执行到v-enter-from阶段

  • @enter="enter",执行到v-enter-active

  • @after-enter="afterEnter",执行到v-enter-to阶段

  • @enter-cancelled="enterCancelled"

  • @before-leave="beforeLeave",执行到v-leave-to阶段

  • @leave="leave",执行到v-leave-active阶段

  • @after-leave="afterLeave",执行到v-leave-to阶段

  • @leave-cancelled="leaveCancelled",执行到v-enter-to阶段 当只用 JavaScript 过渡的时候,在 enterleave 钩中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。这时,我们可以添加 :css="false",让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。

上面的钩子可以和一些js动画库来实现动画。例如jsap库。 它可以通过JavaScript为CSS属性、SVG、Canvas等设置动画,并且是浏览器兼容的。其中有两个比较重要的API来实现动画。

  • jsap.from(el, options): 表示动画从什么状态开始。

  • jsap.to(el, options): 表示动画以什么状态结束。 其中el表示动画作用的元素。options表示动画css属性。

     
    enter(el, done) {
      console.log('enter')
      
      gsap.from(el, {
        scale: 0,
        x: 200,
        onComplete: done,
      })
    },
    
    leave(el, done) {
      console.log('leave')
      
      gsap.to(el, {
        scale: 0,
        x: 200,
        onComplete: done,
      })
    },

我们来使用jsap库实现一个滚动数字动画。

<template>
  <div class="app">
    <input type="number" step="100" v-model="counter">
    <h2>当前计数: {{showNumber.toFixed(0)}}</h2>
  </div>
</template>

<script>
  import gsap from 'gsap';

  export default {
    data() {
      return {
        counter: 0,
        showNumber: 0
      }
    },
    watch: {
      counter(newValue) {
        gsap.to(this, {duration: 1, showNumber: newValue})
      }
    }
  }
</script>

其他动画知识,请访问官网,讲的非常详细。

v3.cn.vuejs.org/guide/trans…[6]

composition API

一下使用的API都需要在vue中导入。

Options API的弊端

在Vue2中,我们编写组件的方式是Options API:

  • Options API的一大特点就是在对应的属性中编写对应的功能模块。比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;

但是这种代码有一个很大的弊端:

  • 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中。

  • 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散。

  • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人)。

composition API介绍

composition API的容器

其中composition API都是写在我们setup函数中的。并且在setup函数中是不能使用this的,因为vue内部再调用setup函数的时候没有绑定this。

下面我们就来研究一些setup函数。

  • 它主要有两个参数:
    • 第一个参数:props。组件接收的属性

    • 第二个参数:context。组件上下文对象
      • attrs:所有的非prop的attribute。

      • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用)。

      • emit:当我们组件内部需要发出事件时会用到emit。

      • expose:当通过ref获取该组件时,向外暴露的一些setup中的数据。 那我们如何定义响应式数据呢?

composition API处理数据
reactive: 将多个数据变成响应式数据
  • 当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集。

  • 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)。

  • 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的。

    const state = reactive({
      counter: 100,
      name: 'zh'
    })

ref: 将单个数据变成响应式。

reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告。所以我们需要使用ref。

  • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源。

  • 它内部的值是在ref的 value 属性中被维护的。

  • 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用。

  • 在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式。 注意:ref对象在模板中的解包是浅层的解包

readonly: 返回一个传入的对象的只读代理

我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改。例如我们想要将provide提供数数据传递给子孙组件,我们就可以使用readonly,让其是只读的,不能再子孙组件中修改。

该API返回普通对象, ref对象, reactive对象的只读代理。

  • readonly返回的对象都是不允许修改的。

  • 但是经过readonly处理的原来的对象是允许被修改的。

  • 其实本质上就是readonly返回的对象的setter方法被劫持了而已。

      
      const info1 = {name: "zh"};
      const readonlyInfo1 = readonly(info1);

      
      const info2 = reactive({
        name: "zh"
      })
      const readonlyInfo2 = readonly(info2);

      
      const info3 = ref("zh");
      const readonlyInfo3 = readonly(info3);

toRefs: 将传入的对象变成ref对象

当我们想要对reactive对象做解构的时候,直接解构,将使数据失去响应式。如果我们用toRefs将其包裹后解构,数据依然是响应式的。这种做法相当于已经在reactive中的属性和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化

    const info = reactive({ name: 'zh', age: 22 })
    
    let { name, age } = toRefs(info)

toRef: 将指定传入的对象那个属性变成ref对象

如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法。

    const info = reactive({ name: 'zh', age: 22 })
    
    let age = toRef(info, 'age')

isProxy
  • 检查对象是否是由 reactive 或 readonly创建的 proxy。

isReactive
  • 检查对象是否是由 reactive创建的响应式代理。

  • 如果该代理是 readonly 创建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。

isReadonly
  • 检查对象是否是由 readonly 创建的只读代理。

toRaw
  • 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。

shallowReactive
  • 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。

shallowReadonly
  • 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。

unref
  • 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法。

  • 如果参数是一个 ref,则返回内部值,否则返回参数本身。

  • 这是 val = isRef(val) ? val.value : val 的语法糖函数。

isRef
  • 判断值是否是一个ref对象。

shallowRef
  • 创建一个浅层的ref对象。只有修改了ref对象,他才是响应式的。如果修改内部对象,将不是响应式的。 这个api和shallowReactive不一样。后者是将传入的对象第一层变成一个响应式的,修改第一层对象属性依旧是可以做到响应式的。但是这个api只是修改ref对象才会是响应式的。

    const info = shallowRef({ name: 'zh' })
    const changeInfo = () => {
      
      info.value = { name: 'llm' }
      
      info.value.name = 'llm'
    }

triggerRef
  • 手动触发和 shallowRef 相关联的副作用。

      const info = shallowRef({name: "zh"})
      const changeInfo = () => {
        info.value.name = "llm";
        
        triggerRef(info);
      }

customRef: 自定义ref对象

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:

  • 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数。

  • 并且应该返回一个带有 get 和 set 的对象。

    import { customRef } from 'vue';

    
    export default function(value, delay = 300) {
      let timer = null;
      return customRef((track, trigger) => {
        return {
          get() {
            track();
            return value;
          },
          set(newValue) {
            clearTimeout(timer);
            timer = setTimeout(() => {
              value = newValue;
              trigger();
            }, delay);
          }
        }
      })
    }
    
    
    const message = debounceRef("Hello World");

获取当前组件上下文 getCurrentInstance

由于setup函数中,没有绑定this。所以我们获取不到this,即当前组件对象。

如果我们想要获取呢?

vue提供了getCurrentInstance, 可以让我们获取当前组件对象。调用该API即可。

如果想要获取组件提供的全局属性。我们需要获取全局对象。

    getCurrentInstance().appContext.config.globalProperties

计算属性 computed

当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理。

computed可以传入两种参数:

  • 接收一个getter函数,并为 getter 函数指定返回值

  • 接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象。当修改computed时非常有用。

  • computed返回一个ref对象。

      
      
      const fullName = computed(() => firstName.value + " " + lastName.value);

      
      const fullName = computed({
        get: () => firstName.value + " " + lastName.value,
        set(newValue) {
          const names = newValue.split(" ");
          firstName.value = names[0];
          lastName.value = names[1];
        }
      });

监听数据 watch / watchEffect

当数据变化时执行某一些操作。我们可以通过watch / watchEffect来监听。

二者的区别:

  • watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而 watch 只能监听指定的属性而做出变更(v3开始可以同时指定多个)。

  • watch 可以获取到新值与旧值(更新前的值),而 watchEffect 是拿不到的。

  • watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。 watchEffect

  • 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖。

  • 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行。

  • 并且返回一个函数,这个函数可以用来取消watchEffect的监听。或者组件卸载后自动停止监听。

     
      const name = ref("zh");
      const age = ref(20);

      const stop = watchEffect(() => {
        console.log("name:", name.value, "age:", age.value);
      });

      const changeName = () => name.value = "llm"
      const changeAge = () => {
        age.value++;
        
        if (age.value > 30) {
          stop();
        }
      }

watchEffect的执行时机。默认情况下,watchEffect是在视图更新之前执行副作用函数。如果我们想要改变他的执行时机,怎么改变呢?

watchEffect还可以传入第二个参数,为一个对象。设置flush顺序性就可以改变watchEffect的执行时机。

 flush: 'pre'(默认,在视图更新前执行) 
        'post'(在视图更新后执行)
        'sync'(同步触发,会出现问题,少用)

      const title = ref(null);

      watchEffect(() => {
        console.log(title.value); 
      }, {
        flush: "post" 
      })

watchEffect还可以清除副作用

什么是清除副作用呢?

比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用。

在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate, 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数。我们可以在传入的回调函数中,执行一些清楚工作。就类似于节流函数。

        watchEffect((onInvalidate) => {
            const timer = setTimeout(() => {
              console.log("网络请求成功~");
            }, 2000)

            onInvalidate(() => {
              
              clearTimeout(timer);
              console.log("onInvalidate");
            })
      });

watch

watch的API完全等同于组件watch选项的Property:

  • watch需要侦听特定的数据源,并在回调函数中执行副作用。

  • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调。 监听单个数据源

watch侦听函数的数据源有两种类型:

    
    const info = reactive({ name: 'zh', age: 20 })
    watch(info, (newValue, oldValue) => {
        
      console.log('newValue:', newValue, 'oldValue:', oldValue)
    })

监听多个值

可以将多个源放在数组中。

    
    const info = reactive({ name: 'zh', age: 20 })
    const name = ref('zh')

    
    watch(
      [() => ({ ...info }), name],
      ([newInfo, newName], [oldInfo, oldName]) => {
        console.log(newInfo, newName, oldInfo, oldName)
      }
    )

watch的选项

如果想要深度监听对象,我们就需要给watch传入第二个参数。用于深度监听或者立即执行。

默认情况下,watch对于监听展开的reactive对象不能深度监听,但是我们如果先改变第一层的属性即info.name,那么info.friend.name也会被改变。但是如果只改变info.friend.name,是不会触发watch回调的。只有配置了deep: true,才会被监听到。

    const info = reactive({
      name: 'zh',
      age: 18,
      friend: {
        name: 'jcl',
      },
    })

    
    watch(
      () => ({ ...info }),
      (newInfo, oldInfo) => {
        console.log(newInfo, oldInfo)
      },
      {
        
        
      }
    )

    const changeData = () => {
      
      info.friend.name = 'zheng'
    }

但是对于直接监听reactive对象,他会自动深度监听,内部有设置deep: true。

    const info = reactive({
      name: 'zh',
      age: 18,
      friend: {
        name: 'jcl',
      },
    })
     watch(info, (newInfo, oldInfo) => {
      console.log(newInfo, oldInfo)
    })

    const changeData = () => {
      info.friend.name = 'zheng'
    }

对于ref传入的对象,也是默认没有深度监听的。并且监听函数中参数都是一个对象。

    const info = ref({
      name: 'zh',
      age: 18,
      friend: {
        name: 'jcl',
      },
    })
     watch(
      info,
      (newInfo, oldInfo) => {
        console.log(newInfo, oldInfo)
      },
      {
      
        deep: true,
      }
    )

    const changeData = () => {
      info.value.friend.name = 'zheng'
    }

生命周期

options API中生命周期和composition API中生命周期对比

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated
provide 和 inject

如果我们想要向子孙组件传递数据,我们就可以通过provide API来完成。用inject API来接收传递的数据。

由于我们向下传递的数据,不需要子孙组件修改,只允许我们自己修改,然后影响下层组件,所以。可以使用readonlyAPI来传递只读数据。

provide可以传入两个参数:

  • 第一个是提供的属性名称

  • 第二个是传入的数据 inject可以传入两个参数:

  • 第一个是接收到provide传递的属性名

  • 第二个是提供默认值

    const obj = reactive({
      name: 'zh',
      age: 20,
    })
    const name = ref('llm')

    provide('obj', readonly(obj))
    provide('name', readonly(name))

    
    let obj = inject('obj')
    let name = inject('name')

如果我们真的想要在子孙组件中修改数据,我们可以提供一个函数,接收子孙组件的数据,然后在该祖先组件中修改。

    
    
    const updateName = (e, val) => {
      console.log('e---------------', e, val)
      name.value = val
    }

    
    let updateName = inject('updateName')
    <button @click="(e) => {updateName(e, '子孙组件中修改name的数据')}">改变name</button>

渲染函数 render

Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器。

Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM)。

事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode。

那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode。

我们可以通过vue提供的h函数来实现。h() 函数是一个用于创建 vnode 的一个函数。其实准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数。

下面就是h函数的参数传递和用法。

    h(
      
      
      
      
      
      'div',

      
      
      
      
      
      {},

      
      
      
      
      
      
      [
        'Some text comes first.',
        h('h1', 'A headline'),
        h(MyComponent, {
          someProp: 'foobar'
        })
      ]
    )

注意:如果没有props,那么通常可以将children作为第二个参数传入。如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入。

h函数的基本使用。h函数可以在两个地方使用:

  • render函数选项中。作为render函数的返回值。如果想要使用事件。我们可以传入on+事件名的属性,函数作为他的值。

    data() {
      return {
        counter: 0
      }
    },
    render() {
      return h("div", {class: "app"}, [
        h("h2", null, `当前计数: ${this.counter}`),
        h("button", {
          onClick: () => this.counter++
        }, "+1"),
        h("button", {
          onClick: () => this.counter--
        }, "-1"),
      ])
    }

  • setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode)。作为setup函数的返回值。

    setup() {
      const counter = ref(0);
      
      return () => {
        return h("div", {class: "app"}, [
          h("h2", null, `当前计数: ${counter.value}`),
          h("button", {
            onClick: () => counter.value++
          }, "+1"),
          h("button", {
            onClick: () => counter.value--
          }, "-1"),
        ])
      }
    }

h函数中使用插槽。 可以通过三元运算符,提供默认插槽内容。

HelloWorld.vue

 setup() {
    const instance = getCurrentInstance().ctx.$slots
    return () =>
      h('div', {}, [
        instance.first
          ? instance.first({ first: 'first=========' })
          : '默认插槽first',
        instance.second
          ? instance.second({ second: 'second=========' })
          : '默认插槽second',
      ])
  },

setup() {
    return () =>
      h(
        HelloWorld,
        {class: 'hello-world'},
        {
        
          first: (slotProps) => h('span', slotProps.first),
          second: (slotProps) => h('span', slotProps.second),
        }
      )
  },

以上只是一些个人实验,如果想要了解更多,请访问官网 v3.cn.vuejs.org/guide/rende…[7]

在vue中使用jsx

@vue/babel-plugin-jsx[8]

首先我们需要安装@vue/babel-plugin-jsx / @vitejs/plugin-vue-jsx插件,然后在babel配置文件中配置。

    
    module.exports = {
        presets: [
            "@vue/cli-plugin-babel/preset"
        ],
        plugins: [
            "@vue/babel-plugin-jsx"
        ]
    }

或者vite创建的项目vite.config.js中直接导入插件,然后在plugins调用。

使用时,需要在script标签中指定lang="jsx"

在setup模板中使用jsx时,我们只需要定义一个函数,然后返回dom树结构,然后再在template模板中使用这个函数即可。

<template>
    <jsxrender />
</template>

const a = ref("zh")
const jsxrender = () => (
    <div>
        {a.value}
    </div>
)

使用jsx的好处

  • 可以直接在jsx中得到使用变量的提示。

  • 在为传递props时,编译时会报错。

  • 也可以直接使用vue提供的指令。

  • 可以很好的扩展当前组件。

自定义指令

在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。用来复用代码,方便操作。

注意:在Vue中,代码的复用和抽象主要还是通过组件通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令。

自定义指令分为两种:

  • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用。

  • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用。 下面我们来自定义一个自动获取焦点的指令。

局部指令: 直接在dom上通过v-focus使用即可

    
    directives: {
      focus: {
        mounted(el, bindings, vnode, preVnode) {
          el.focus();
        }
      }
    }

全局指令

app.directive("focus", {
    mounted(el, bindings, vnode, preVnode) {
      el.focus();
    }
})

下面我们就来介绍一下自定义指令中的生命周期函数

一个指令定义的对象,Vue提供了如下的几个钩子函数: 注意: 这些生命周期函数名称和vue2有一些不同

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用;

  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;

  • mounted:在绑定元素的父组件被挂载后调用;

  • beforeUpdate:在更新包含组件的 VNode 之前调用;

  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;

  • beforeUnmount:在卸载绑定元素的父组件之前调用;

  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次; 生命周期函数的参数:其中el, binding比较常用

  • el: 指令绑定到的元素。这可用于直接操作 DOM。

  • binding: 包含以下 property 的对象。
    • instance:使用指令的组件实例。

    • value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2

    • oldValue:先前的值,仅在 beforeUpdateupdated 中可用。值是否已更改都可用。

    • arg:参数传递给指令 (如果有)。例如在 v-my-directive:foo 中,arg 为 "foo"

    • modifiers:包含修饰符 (如果有) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}

    • dir:一个对象,在注册指令时作为参数传递。(就是提供的生命周期函数)

  • vnode: 上面作为 el 参数收到的真实 DOM 元素的蓝图。

  • prevNode: 上一个虚拟节点,仅在 beforeUpdateupdated 钩子中可用。

    下面我们来封装一个格式化时间戳的指令

     import dayjs from 'dayjs';
     app.directive("format-time", {
        
        created(el, bindings) {
          bindings.formatString = "YYYY-MM-DD HH:mm:ss";
          
          if (bindings.value) {
            bindings.formatString = bindings.value;
          }
        },
        mounted(el, bindings) {
          const textContent = el.textContent;
          let timestamp = parseInt(textContent);
          if (textContent.length === 10) {
            
            timestamp = timestamp * 1000
          }
          el.textContent = dayjs(timestamp).format(bindings.formatString);
        }
      })
      
      
      <h2 v-format-time="'YYYY/MM/DD'">{{timestamp}}</h2>

插件

通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:

  • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行。install函数将接受两个参数,一是全局对象。二是用户传入的配置对象

  • 函数类型:一个function,这个函数会在安装插件时自动执行。将接受两个参数,一是全局对象。二是用户传入的配置对象。

插件可以完成的功能没有限制,比如下面的几种都是可以的:

  • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现。

  • 添加全局资源:指令/过滤器/过渡等。

  • 通过全局 mixin 来添加一些组件选。

  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。 使用插件调用全局对象的use方法即可。并将插件对象传入给use方法。

    
    export default {
      install(app) {
        app.config.globalProperties.$name = "zh"
      }
    }
    
    
    export default function(app) {
      console.log(app);
    }

    
    const app = createApp(App);
    app.use(plugin)

  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值