返回

Vue 高级特性及过渡动画

Table of contents

Open Table of contents

自定义指令

类似 v-forv-show 等指令,当标签上写入 v-xxx 时,希望执行某种功能,官方提供了可以自定义的指令操作。

例如:<input/> 使用 v-focus 指令,可以自动获取输入框焦点

局部自定义指令

局部自定义指令

在 选项式 API 中, v-xxx 的 xxx 对应的是 directives 中的 key

在 setup 中,自定义指令必须以 v 开头,vFa 对应的自定义指令就是 v-fa

全局自定义指令

全局自定义指令需应用于 main.js 中

app.directive("focus", {
  mounted(el) {
    console.log("el", el);
    el?.focus();
  },
});

当自定义指令过多时,会导致 main.js 中的挂载非常多,为了更易于,可以将其抽离出去

自定义指令抽离

自定义指令生命周期

生命周期描述
created在绑定元素的 attribute 或事件监听器被应用之前调用
beforeMount当指令第一次绑定到元素并且在挂载父组件之前调用
mounted在绑定元素的父组件被挂载后调用
beforeUpdate在更新包含组件的 VNode 之前调用
updated在包含组件的 VNode 及其子组件的 VNode 更新后调用
beforeUnmount在卸载绑定元素的父组件之前调用
unmounted当指令与元素解除绑定且父组件已卸载时,只调用一次

指令的参数和修饰符

指令的参数和修饰符

Teleport

在组件化开发中,我们封装一个组件 A,在另外一个组件 B 中使用:

总的来说就是挂载到 #app 之外的地方,可以使用 teleport

Teleport 是一个 Vue 提供的内置组件,类似于 react 的 Portals

当在 <teleport> 标签上使用 disabled 时,<teleport> 的功能就会失效,相当于 <templete>

当多个 <teleport> 的 to 属性都指向一个节点时,多个 <teleport> 内部的元素内容会进行合并

异步组件和 Suspense

和 react 的 Suspense 比较类似,当一个组件未挂载时,显示其它内容(比如 loading)进行展示。

<Suspense>
  <templete #default>
    <AsyncApp />
  </templete>
  <templete #fallback>
    <Loading />
  </templete>
</Suspense>

<Suspense> 在 vue 中是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。

插件

// 对象类型写法
app.use({
  name: "plugin1",
  install(app, option) {
    console.log("插件被安装", app);
  },
});

// 函数写法
app.use(function direactives(app, option) {
  console.log("插件被安装", app);
});

对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
函数类型:一个 function,这个函数会在安装插件时自动执行
执行的内部会自动传入两个参数 app 和 option

Vue 使用 jsx 语法

案例:

export default {
    setup(){
        const counter = ref(0)
        const minus =()=> counter--
        const add = () => counter++

        return {
            counter,
            minus,
            add
        }
    }

    render(){
        return (
            <div>
                <div>当前计数: {{ this.counter }}</div>
                <button onClick={this.add}>+1</button>
                <button onClick={this.minus}>+1</button>
            </div>
        )
    }
}

过渡动画

React 框架本身并没有提供任何动画相关的 API,所以在 React 中使用过渡动画我们需要使用一个第三方库 react-transition-group
Vue 中为我们提供一些内置组件和对应的 API 来完成动画,利用它们我们可以方便的实现过渡动画效果

class 的 name 命名规则如下:

transition

<template>
  <div class="animation">
    <button @click="isShow = !isShow">切换</button>
    <transition name="my">
      <h2 v-if="isShow">How old are you?</h2>
    </transition>
  </div>
</template>

<script setup>
  import { ref } from "vue";
  const isShow = ref(false);
</script>

<style>
  /* 开始进入时的状态 */
  /* 离开完成时的状态 */
  .my-enter-from,
  .my-leave-to {
    opacity: 0;
  }

  /* 进入过程需要执行的操作 */
  /* 离开过程需要执行的操作 */
  .my-enter-active,
  .my-leave-active {
    transition: all 2s ease;
  }

  /* 进入完成时的状态 */
  /* 开始离开时的状态 */
  .my-enter-to,
  .my-leave-from {
    opacity: 1;
  }
</style>

整个动画流程状态如图所示:

动画状态

appear:初次渲染<transition/> 属性添加 apper 属性,可以实现首次渲染时的动画效果

mode:过渡的模式 当动画在两个元素之间切换的时候存在的问题一个问题:

duration:显示的指定动画时间

transition-group

一组动画的实现,比如列表的添加与删除

<template>
  <div class="app.vue">
    <h2>App.vue</h2>
    <button @click="add">添加</button>
    <TransitionGroup tag="div" name="my">
      <template v-for="(item, index) in nums" :key="item">
        <div>{{ item }} <button @click="remove(index)">删除</button></div>
      </template>
    </TransitionGroup>
  </div>
</template>

<script setup>
  import { reactive } from "vue";
  const nums = reactive([1, 2, 3, 4, 5]);

  const add = () => nums.push(nums.length + 1);
  const remove = (index) => nums.splice(index, 1);
</script>

<style>
  /* 开始进入时的状态 */
  /* 离开完成时的状态 */
  .my-enter-from,
  .my-leave-to {
    opacity: 0;
  }

  /* 进入过程需要执行的操作 */
  /* 离开过程需要执行的操作 */
  .my-enter-active,
  .my-leave-active {
    transition: all 2s ease;
  }

  /* 解决元素移除时移动无动画的问题 */
  .my-leave-active {
    position: absolute;
  }

  /* 进入完成时的状态 */
  /* 开始离开时的状态 */
  .my-enter-to,
  .my-leave-from {
    opacity: 1;
  }

  .my-move {
    transition: all 2s ease;
  }
</style>

Vue2、Vue3 响应式原理

响应式依赖收集

收集函数主动调用

const obj = {
  name: "张三",
  age: 20,
};

const reactiveFns = [];

// 设置一个专门执行响应式函数的 fn
function watchFn(fn) {
  reactiveFns.push(fn);
}

watchFn(function foo() {
  console.log("foo", obj);
});

watchFn(function bar() {
  console.log("bar", obj);
});

obj.name = "李四";
reactiveFns.forEach((fn) => {
  fn();
});

类格式收集

设置单独的类专门管理收集的依赖

class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  // 添加到收集数组中
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn);
    }
  }

  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}

const obj = {
  name: "张三",
  age: 20,
};

const dep = new Depend();

// 设置一个专门执行响应式函数的 fn
function watchFn(fn) {
  dep.addDepend(fn);
}

watchFn(function foo() {
  console.log("foo", obj);
});

watchFn(function bar() {
  console.log("bar", obj);
});

obj.name = "李四";
dep.notify();

监听属性变化

主动通知过于繁琐,需手动调用。
需求:当属性变化时进行劫持自动通知

vue2 Object.defineProperty 监听

const obj = {
  name: "张三",
  age: 20,
};

const dep = new Depend();

// 对属性的 key 进行遍历绑定
Object.keys(obj).forEach((key) => {
  let val = obj[key]; // 属性值
  Object.defineProperty(obj, key, {
    set: function (newV) {
      console.log(`${key}发生改变`, newV);
      val = newV;
      dep.notify(); // 收集函数通知调用
    },
    get: function () {
      return val;
    },
  });
});

// 设置一个专门执行响应式函数的 fn
function watchFn(fn) {
  dep.addDepend(fn);
}

watchFn(function foo() {
  console.log("foo name", obj.name);
  console.log("foo age", obj.age);
});

watchFn(function bar() {
  console.log("bar name", obj.name);
  console.log("bar age", obj.age);
});

obj.name = "李四";

自动收集依赖

当 name 发生改变时,此时 foo 和 bar 都会被调用,甚至与 obj 无关的函数都会调用,但是 bar 内部没有关于 name 的依赖。
原因:dep 收集依赖无法区分,只能传入的函数全部调用
需求:只调用与数据变化有关依赖的函数

watchFn(function foo() {
  console.log("foo name", obj.name);
  console.log("foo age", obj.age);
});

// bar 函数内部没有使用 name,却也调用了
watchFn(function bar() {
  console.log("bar age", obj.age);
});

obj.name = "李四";

思路:

监听属性变化 这一步,实现的流程大致如下:

监听属性变化流程思路

通过 Map 对象进行多重 map 映射,就可以很轻松的获取到数对象的 属性 对应的 dep 对象

map映射

class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  // 添加到收集数组中
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn);
    }
  }

  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}

const user = {
  name: "张三",
  age: 20,
};

/**-------------------------------------------重点 start--------------------------- */
// 封装一个函数:负责通过 obj 获取对应的 depend 对象
const allMap = new WeakMap();
function getDeped(obj, key) {
  // 1、根据对象obj,找到对应的 Map 对象
  let objMap = allMap.get(obj);
  if (!objMap) {
    // 首次加载时无 map,创建
    objMap = new Map();
    allMap.set(obj, objMap); // allMap 添加 obj 对象映射
  }

  //   2、从 objMap 中,根据key,找到 obj 对应的 Depend 对象
  let dep = objMap.get(key); // 将所有的 dep对象设置到 obj 对应的 value中
  if (!dep) {
    dep = new Depend();
    objMap.set(key, dep);
  }

  return dep;
}
/**-------------------------------------------重点 end--------------------------- */

// const dep = new Depend()

// 对属性的 key 进行遍历绑定
Object.keys(user).forEach((key) => {
  let val = user[key]; // 属性值
  Object.defineProperty(user, key, {
    set: function (newV) {
      val = newV;
      //   值改变时,通过 对象的 key,拿到 key 对应的 dep对象并执行
      const dep = getDeped(user, key);
      dep.notify();
    },
    get: function () {
      // 获取 user 对应的 dep 对象
      const dep = getDeped(user, key);
      // 当获取时将应用 obj 的 fn 添加到 依赖中
      dep.addDepend(reactiveFn);
      return val;
    },
  });
});

// 设置一个专门执行响应式函数的 fn
let reactiveFn = null;
function watchFn(fn) {
  reactiveFn = fn;
  fn(); // 首次需先执行一次,创建每个 key dep 依赖
  reactiveFn = null;
  //   dep.addDepend(fn)
}

watchFn(function foo() {
  console.log("foo name", user.name);
  console.log("foo age", user.age);
});

watchFn(function bar() {
  console.log("bar age", user.age);
});

console.log("name发生变化------");
user.name = "李四";

console.log("age发生变化------");
user.age = 30;

执行结果

  1. 每一个对象的每一个属性都会对应一个 dep 对象
  2. 同一个对象的多个属性的 dep 对象是存放在一个 map 对象中的
  3. 多个对象的 mao 对象,会被存放到一个 allMap 的对象中
  4. 依赖收集:当执行 get 函数,自动添加 fn 函数

自动收集依赖 BUG 修改

当一个函数内部,重复使用一个属性时,函数会被执行多次

BUG

每次获取时,都会执行 get 内部的逻辑

BUG 修复:对收集的依赖内部进行一个去重操作

class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  // 添加到收集数组中
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.add(fn);
    }
  },
  // 写这个,get内部就不需要手动传入参数,直接调用即可
  depend(){
    if(reactiveFn){
      this.reactiveFns.add(reactiveFn);
    }
  },

  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}

多个对象响应式

在此之前,Object.keys 循环遍历的是一个写死的对象

// 对属性的 key 进行遍历绑定
function reactive(obj) {
  Object.keys(obj).forEach((key) => {
    let val = obj[key]; // 属性值
    Object.defineProperty(obj, key, {
      set: function (newV) {
        val = newV;
        //   值改变时,通过 对象的 key,拿到 key 对应的 dep对象并执行
        const dep = getDeped(obj, key);
        dep.notify();
      },
      get: function () {
        // 获取 obj 对应的 dep 对象
        const dep = getDeped(obj, key);
        // 当获取时将应用 obj 的 fn 添加到 依赖中
        dep.depend();
        return val;
      },
    });
  });
  return obj; // 返回对象,不然拿到的是 undefined
}

// ==========================业务代码=======================

const user = reactive({
  name: "张三",
  age: 20,
  sex: "",
});

const course = reactive({
  first: "HTML+CSS",
  second: "JavaScript",
});
watchFn(function () {
  console.log(user.name);
  console.log(user.age);
});

watchFn(function () {
  console.log(course.first);
});

console.log("user.age发生变化------");
user.age = 30;

console.log("course.first发生变化------");
course.first = "Vue";

Vue2 响应式监听完整源码

class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  // 添加到收集数组中
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.add(fn);
    }
  }
  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn);
    }
  }

  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}

// 设置一个专门执行响应式函数的 fn
let reactiveFn = null;
function watchFn(fn) {
  reactiveFn = fn;
  fn(); // 首次需先执行一次,创建每个 key dep 依赖
  reactiveFn = null;
  //   dep.addDepend(fn)
}

/**-------------------------------------------重点 start--------------------------- */
// 封装一个函数:负责通过 obj 获取对应的 depend 对象
const allMap = new WeakMap();
function getDeped(obj, key) {
  // 1、根据对象obj,找到对应的 Map 对象
  let objMap = allMap.get(obj);
  if (!objMap) {
    // 首次加载时无 map,创建
    objMap = new Map();
    allMap.set(obj, objMap); // allMap 添加 obj 对象映射
  }

  //   2、从 objMap 中,根据key,找到 obj 对应的 Depend 对象
  let dep = objMap.get(key); // 将所有的 dep对象设置到 obj 对应的 value中
  if (!dep) {
    dep = new Depend();
    objMap.set(key, dep);
  }

  return dep;
}
/**-------------------------------------------重点 end--------------------------- */

// const dep = new Depend()

// 对属性的 key 进行遍历绑定
function reactive(obj) {
  Object.keys(obj).forEach((key) => {
    let val = obj[key]; // 属性值
    Object.defineProperty(obj, key, {
      set: function (newV) {
        val = newV;
        //   值改变时,通过 对象的 key,拿到 key 对应的 dep对象并执行
        const dep = getDeped(obj, key);
        dep.notify();
      },
      get: function () {
        // 获取 obj 对应的 dep 对象
        const dep = getDeped(obj, key);
        // 当获取时将应用 obj 的 fn 添加到 依赖中
        dep.depend();
        return val;
      },
    });
  });
  return obj; // 返回对象,不然拿到的是 undefined
}

// ==========================业务代码=======================

const user = reactive({
  name: "张三",
  age: 20,
  sex: "",
});

const course = reactive({
  first: "HTML+CSS",
  second: "JavaScript",
});
watchFn(function () {
  console.log(user.name);
  console.log(user.age);
});

watchFn(function () {
  console.log(course.first);
});

console.log("user.age发生变化------");
user.age = 30;

console.log("course.first发生变化------");
course.first = "Vue";

Vue3 重构

主要就是从 Object.defineProperty 更改为 Proxy

// 对属性的 key 进行遍历绑定
function reactive(obj) {
  const objProxy = new Proxy(obj, {
    set: function (target, key, newV, receiver) {
      //   target[key] = newV;
      Reflect.set(target, key, newV, receiver);
      const dep = getDeped(target, key);
      dep.notify();
    },
    get: function (target, key, receiver) {
      const dep = getDeped(target, key);
      dep.depend();
      //   return target[key];
      return Reflect.get(target, key, receiver);
    },
  });
  return objProxy;
}


上一篇
React 项目工程化搭建
下一篇
Vue 路由 状态管理