# 基础例子

  • 在 vue2 中是采用 new Vue 的方式创建一个应用程序,代码组织方式是 Options API 的形式,也就是把代码分类写在几个 Option 中。
new Vue({
  el: "#app",
  template: `
    <div>
      <div>Single Count: {{this.single}}, Double Count: {{this.double}}</div>
      <button @click="increment">add</button>
    </div>
  `,
  data: {
    single: 0
  },
  computed: {
    double() {
      return this.single * 2;
    }
  },
  methods: {
    increment() {
      this.single++;
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 而在 vue3 中,通过 createApp 方法来创建一个应用程序,代码组织方式是 Composition API,组件将逻辑封装到函数中。

  • 此外,还有一点变化就是,vue2 中模板最外层只能有一个 div 元素,但是在 vue3 中没有了这个限制。

<script>
  const { createApp } = Vue;
  createApp(App).mount("#app");
</script>
1
2
3
4
const { reactive, computed } = Vue;

const App = {
  template: `
    <div>Single Count: {{state.single}}, Double Count: {{state.double}}</div>
    <button @click="increment">add</button>
  `,
  setup() {
    const state = reactive({
      single: 0,
      double: computed(() => state.single * 2)
    });

    function increment() {
      state.single++;
    }

    return {
      state,
      increment
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Computed

  • vue3 的 computed 计算值的行为和计算属性一样:只有当依赖变化的时候它才会被重新计算。

  • computed() 返回的是一个包装对象,它可以和普通的包装对象一样在 setup() 中被返回,也一样会在渲染上下文中被自动展开。

  • vue3 中的 computed 被抽成了一个 API,直接从 vue 中获取;而 vue2 中,computed 是一个对象,在对象中定义一个个 computed。

# vue2 中 computed

const App = new Vue({
  el: "#app",
  template: `
    <div>
      {{handleName}}
      <button @click="handleClick">点击</button>
    </div>
  `,
  data: { name: "shenzhen" },
  computed: {
    // 仅读取
    getName: function() {
      return this.name;
    },
    // 读取和设置
    handleName: {
      get: function() {
        return this.name;
      },
      set: function(v) {
        this.name = v + "-good";
      }
    }
  },
  methods: {
    handleClick() {
      this.handleName = this.name;
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# vue3 中 computed

  • 可以直接传一个函数,返回你所依赖的值的计算结果,这个值是个包装对象,默认情况下,如果用户试图去修改一个只读包装对象,会触发警告,说白了就是你只能 get 无法 set。
const App = {
  setup() {
    const count = ref(1);
    // computed() 函数的返回值是一个 ref 的实例
    // 根据 count 的值,创建一个响应式的计算属性 plusOne
    // 它会根据依赖的 ref 自动计算并返回一个新的 ref
    const plusOne = computed(() => count.value + 1);
    console.log(plusOne); // 打印结果可以看到 isRef 为 true
    console.log(plusOne.value); // 2
    plusOne.value++; // 触发警告,默认情况下,如果用户试图去修改一个只读包装对象,会触发警告
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
  • 也可以在调用 computed() 函数期间,传入一个包含 get 和 set 函数的对象,可以得到一个可读可写的计算属性。
const App = {
  setup() {
    // 创建一个 ref 响应式数据
    const count = ref(1);
    // 创建一个 computed 计算属性
    const plusOne = computed({
      // 取值函数
      get: () => count.value + 1,
      // 赋值函数
      set: (val) => {
        count.value = val - 1;
      }
    });
    // 为计算属性赋值的操作,会触发 set 函数
    plusOne.value = 1;
    // 触发 set 函数后,count 的值会被更新
    console.log(count.value); // 0
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 配合 TS 使用
// read-only
function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>;

// writable
function computed<T>(options: {
  get: () => T;
  set: (value: T) => void;
}): Ref<T>;
1
2
3
4
5
6
7
8

# 生命周期钩子

# vue2 和 vue3 生命周期钩子对比

mac

# vue2 中的生命周期钩子

const App = new Vue({
  el: "#app",
  template: `
    <div>{{this.a}}</div>
  `,
  data: {
    a: 1
  },
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  updated() {
    console.log("updated");
  },
  beforeDestroy() {
    console.log("beforeDestroy");
  },
  destroy() {
    console.log("destroy");
  },
  errorCaptured() {
    console.log("errorCaptured");
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# vue3 中的生命周期钩子

所有现有的生命周期钩子都会有对应的 onXXX 函数(只能在 setup() 中使用),去除了 created、beforeCreate。

const {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} = Vue;
const App = {
  setup() {
    onBeforeMount(() => {
      console.log("onBeforeMount");
    });
    onMounted(() => {
      console.log("onMounted!");
    });
    onBeforeUpdate(() => {
      console.log("onBeforeUpdate!");
    });
    onUpdated(() => {
      console.log("onUpdated!");
    });
    onBeforeUnmount(() => {
      console.log("onBeforeUnmount!");
    });
    onUnmounted(() => {
      console.log("onUnmounted!");
    });
    onErrorCaptured(() => {
      console.log("onErrorCaptured!");
    });
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

除此之外,还新增了两个新的调试钩子。

const {
  onRenderTriggered,
  onRenderTracked,
} = Vue;

const App = {
  setup() {
    onRenderTriggered(() => {
      console.log("onRenderTriggered!");
    }),
    onRenderTracked(() => {
      console.log("onRenderTracked!");
    }),
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# ref

# vue2 中的 ref

  • 在 vue2 中,ref 主要是用来获取 dom 元素。
// 基本用法
const App = new Vue({
  el: "#app",
  template: `
    <div ref="divDom">ref示例</div>
  `,
  data: {},
  mounted() {
    console.log(this.$refs.divDom); // <div>ref示例</div>
  }
});

// 搭配v-for使用
new Vue({
  el: "#app1",
  template: `
    <ul>
      <li v-for="(obj,index) in list1" :key="index" :ref="index">{{obj}}</li>
    </ul>
  `,
  data: {
    list1: ["a", "b", "c", "d"]
  },
  mounted() {
    this.list1.forEach((obj, index, arr) => {
      console.log(this.$refs[index][0].innerText);
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# vue3 中的 ref

  • 在 vue3 中,ref 主要是用来创建响应式数据的。当然,它也可以引用页面上的元素或组件。

  • ref 接收一个原始值,返回一个包装对象,包装对象具有 .value 属性。通过 .value 访问这个值。

ref 和 reactive 的适用场景

  • 被 ref 方法包裹后的元素就变成了一个代理对象。一般而言,这里的元素参数指基本元素或者称之为 inner value,如:number,string,boolean,null,undefied 等,object 一般不使用 ref,而是使用上文的 reactive。

  • 也就是说 ref 一般适用于某个元素的;而 reactive 适用于一个对象。

  • ref 也就相当于把单个元素转换成一个 reactive 对象了,对象默认的键值名是:value。

const App = {
  setup() {
    // 创建响应式数据对象 count,初始值为 0
    const count = ref(0);
    // 如果要访问 ref() 创建出来的响应式数据对象的值,必须通过 .value 属性才可以
    console.log(count.value); // 0
    count.value++;
    console.log(count.value); // 1
  }
};
1
2
3
4
5
6
7
8
9
10
  • 如果是在在渲染上下文中访问 ref,vue 会帮你自动展开,无须用 .value 访问。
const App = {
  template: `
    <div>{{ count }}</div>
  `,
  setup() {
    return {
      count: ref(0)
    };
  }
};
1
2
3
4
5
6
7
8
9
10
  • 如果是在 reactive 对象中访问 ref 创建的响应式数据,当把 ref() 创建出来的响应式数据对象挂载到 reactive() 上时,会自动把响应式数据对象展开为原始的值,不需通过 .value 就可以直接被访问。换句话说就是当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开
const App = {
  setup() {
    const count = ref(0);
    const state = reactive({
      count
    });
    // 1.不用 state.count.value
    console.log(state.count); // 0
    state.count = 1;
    // 作为值仍然需要通过 .value 访问
    console.log(count.value); // 1

    // 2.if a new ref is assigned to a property linked to an existing ref, it will replace the old ref:
    // 新的 ref 会覆盖旧的 ref

    //再次创建 ref,命名为 otherCount
    const otherCount = ref(2);
    // 将旧 ref count 替换为新 ref otherCount
    state.count = otherCount;
    console.log(state.count); // 2 被覆盖
    console.log(count.value); // 1

    // 3.ref unwrapping only happens when nested inside a reactive Object. There is no unwrapping performed when the ref is accessed from an Array or a native collection type like Map:
    // 如果不是作为对象访问,则需要通过 .value 进行访问,例如 array,map
    const arr = reactive([ref(0)]);
    // need .value here
    console.log(arr[0].value);
    const map = reactive(new Map([["foo", ref(0)]]));
    // need .value here
    console.log(map.get("foo").value);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  • 配合 TS 使用
interface Ref<T> {
  value: T;
}
function ref<T>(value: T): Ref<T>;

const foo = ref<string | number>("foo"); // foo's type: Ref<string | number>
foo.value = 123; // ok!
1
2
3
4
5
6
7

# isRef

  • isRef() 用来判断某个值是否为 ref() 创建出来的对象。

  • 应用场景:当需要展开某个可能为 ref() 创建出来的值的时候。

const unwrapped = isRef(foo) ? foo.value : foo;
1

# toRefs

  • toRefs 函数可以将 reactive() 创建出来的响应式对象转换为普通的对象,只不过,这个对象上的每个属性节点,都是 ref() 类型的响应式数据,配合 v-model 指令能完成数据的双向绑定,在开发中非常高效。

  • 比如将一个 reactive 代理对象打平,转换为 ref 代理对象,使得对象的属性可以直接在 template 上使用。

const App = {
  template: `
    <div>
      <p>{{ obj.count }}</p>
      <p>{{ count }}</p>
      <p>{{ value }}</p>
    </div>
  `,
  setup() {
    const obj = reactive({
      count: 0,
      value: 100
    });
    return {
      obj,
      // 如果这里的 obj 来自另一个文件,
      // 这里就可以不用包裹一层 key,可以将 obj 的元素直接平铺到这里
      // template 中可以直接获取属性
      ...toRefs(obj)
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 使用 ref 引用元素或组件

  • 引用页面上的元素,引用组件也是同样的使用方式。
const { ref, onMounted, h, reactive, onBeforeUpdate } = Vue;
const App = {
  template: `
    <div ref="root">ref使用示例</div>
  `,
  setup() {
    // 创建一个 DOM 引用
    const root = ref(null);
    // 在 DOM 首次加载完毕之后,才能获取到元素的引用
    onMounted(() => {
      // the DOM element will be assigned to the ref after initial render
      // root.value 是原生DOM对象
      console.log(root.value);
    });
    // 把创建的引用 return 出去
    return {
      root
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Usage with Render Function / JSX
const App1 = {
  template: `
    <div ref="root">ref使用示例</div>
  `,
  setup() {
    const root = ref(null);
    onMounted(() => {
      // the DOM element will be assigned to the ref after initial render
      console.log(root.value); // <div/>
    });
    // function
    return () =>
      h("div", {
        ref: root
      });

    // with JSX
    // return () => <div ref={root} />;
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Usage inside v-for
const App2 = {
  template: `
    <div
      v-for="(item, i) in list"
      :ref="el => { divs[i] = el }">
      {{ item }}
    </div>
  `,
  setup() {
    const list = reactive([1, 2, 3]);
    const divs = ref([]);
    onMounted(() => {
      // the DOM element will be assigned to the ref after initial render
      console.log(divs.value[0]); // <div>1</div>
    });
    // make sure to reset the refs before each update
    onBeforeUpdate(() => {
      divs.value = [];
    });
    return {
      list,
      divs
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# ref 和 reactive 总结

📌 1. ref 和 reactive 的区别

其实 ref 相当于 reactive 的小弟,ref 背后也是通过 reactive 实现的,唯一的区别是 ref 返回的是包装对象。

const count = ref(0);
// 等价于
const count = reactive({ value: 0 });
1
2
3

📌 2. 为什么 ref 要返回一个包装对象?

  • 我们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。

  • 这有点像 React Hooks 中的 useRef —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。

  • 有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新):类似 react 的自定义 Hook。

setup() {
  const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新
  const valueB = useLogicB()
  return {
    valueA,
    valueB
  }
}
1
2
3
4
5
6
7
8

📌 3. ref 和 reactive 需要注意的点

  • 在 setup 函数中,如果通过解构返回 ref 和 reactive,那么在模板渲染上下文中,获取不到它们的响应式变化。因为解构它们就意味着 copy 了它们的引用。所以尽量不要用解构去返回一个你期望响应式的数据。
const App = {
  template: ` 
    <div class="container"> 
      <div>{{name1}}--{{name2}}</div> 
      <button @click="add1">add</button>      
    </div>
  `,
  setup() {
    const name1 = ref({ name1: "我是name" });
    const name2 = reactive({ name2: "aa" });
    const add1 = () => {
      console.log((name1.value.name1 = "test"));
      console.log((name2.name2 = "test"));
    };
    // 因为解构,数据不再是响应式的了
    /*     return {
      add1,
      ...name1.value,
      ...name2,
    }; */
    // 如果一定要使用解构,使用toRefs进行转换成响应式数据
    return {
      add1,
      ...toRefs({ name1: name1.value }),
      ...toRefs(name2)
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# reactive

  • reactive 是一个数据监听函数。

  • reactive 函数接收一个对象作为参数,返回这个对象的响应式代理,等价于 Vue2.x 的 Vue.observable()。

  • 对比 Vue2.x 的 observable(),组件实例在初始化的时候会将 data 整个对象变为可观察对象,通过递归的方式给每个 Key 使用 Object.defineProperty 加上 getter 和 settter ,如果是数组就重写代理数组对象的七个方法。虽然给我们带来的便利,但是在大型项目上来说,性能开销就很大了。

  • Vue3.0 之后不再将主动监听所有的数据,而是将选择权给你,实例在初始化时不需要再去递归 data 对象了,从而降低了组件实例化的时间。

// Vue2.x
const App = new Vue({
  el: "#app",
  template: `
    <div>{{reactiveData.name}}--{{reactiveData.address}}</div>
  `,
  data() {
    return {
      reactiveData: { name: "shenzhenwan", address: "shenzhen" }
    };
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
const { reactive } = Vue;
const App = {
  template: `
    <div>{{reactiveData.name}}--{{reactiveData.address}}</div>
  `,
  setup() {
    // 需要注意的是加工后的对象跟原对象是不相等的,并且加工后的对象属于深度克隆的对象。
    let reactiveData = reactive({ name: "shenzhenwan", address: "shenzhen" });
    return {
      reactiveData
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 配合 TS 使用。
function reactive<T extends object>(raw: T): T;
1

# setup

# vue2 中的 “setup”

  • vue2 中其实并没有 setup 函数。
// Usage with Templates
const App = new Vue({
  el: "#app",
  template: `
    <div>
      <h2>{{ this.foo }}</h2>
      <span @click="findClick">{{getName}}</span>
      <hr/>
    </div>
  `,
  computed: {
    getName() {
      return store.name;
    }
  },
  methods: {
    findClick() {
      mutations.setName("我改了");
    }
  },
  mounted() {
    this.foo = store.name;
  },
  data: {
    foo: "bar"
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Usage with Render Functions / JSX
const state = Vue.observable({ count: 0 });
const App1 = new Vue({
  el: "#app1",
  data: {
    count: 0,
    foo: "bar"
  },
  render(h) {
    return h(
      "button",
      {
        on: {
          click: () => {
            mutations.setName("我改了" + Math.random());
          }
        }
      },
      `简单的store: ${store.name}`
    );
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • props
const App2 = Vue.component("app-2", {
  template: `
    <div>{{name}}</div>
  `,
  props: {
    name: String
  },
  created() {
    console.log(this.name);
  }
});
const App2_parent = new Vue({
  el: "#app2",
  components: { "app-2": App2 },
  template: `
    <div ><app-2 name="app-2传递的值"></app-2></div>
  `
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • vue2 中可以使用 this
const App3 = new Vue({
  el: "#app3",
  template: `
      <div>
        <div>{{ count }}</div>
        <button @click="handleClick">点击</button>
      </div>
    `,
  data: {
    count: 0
  },
  methods: {
    handleClick() {
      console.log(this); // vue实例
      console.log(this.count); // 0
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# vue3 新增的 setup 选项

  • setup 是一个新的组件选项,也是其他 API 的入口。也就是说,你所有的操作都将在 setup 函数内部定义和执行,vue3.0 也将用函数代替 vue2.x 的类也就是 new Vue()。

  • 何时调用?

    setup 是在一个组件实例被创建时,初始化了 props 之后调用,其实就是取代了 Vue2.x 的 careted 和 beforeCreate。

  • setup 返回一个对象,对象中的属性将直接暴露给模板渲染的上下文。而在 Vue2.x 中,你定义的属性都会被 Vue 内部无条件暴露给模板渲染的上下文。

// Usage with Templates
const App1 = {
  template: `
      <div>
        <div>{{ count }} {{ object.foo }}</div>
      </div>
    `,
  // setup 是在一个组件实例被创建时,初始化了 props 之后调用
  //  相当于 vue2 的 beforeCreate,created
  setup() {
    const count = ref(0);
    const object = reactive({ foo: "bar" });
    // expose to template
    // setup 返回一个对象,对象中的属性将直接暴露给模板渲染的上下文。
    return {
      count,
      object
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Usage with Render Functions / JSX
const App = {
  setup() {
    const count = ref(0);
    const object = reactive({ foo: "bar" });
    // rende 函数渲染
    return () => h("div", [count.value, object.foo]);
  }
};
1
2
3
4
5
6
7
8
9

📌 props

  • setup 第一个参数是 props,这里的 props 和 Vue2.x 中的 props 一致。
const App3_1 = {
  props: {
    name: {}
  },
  setup(props) {
    console.log(props.name); // 因为没有传值所以是 undefined
  }
};
// 现在在 App3_1_parent 中进行调用,就有值了
const App3_1_parent = {
  // 组件注册
  components: { "app-31": App3_1 },
  template: `
    <div><app-31 name="app-31传递的值"></app-31></div>
  `
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 除此之外,还可以直接通过 watch 方法来观察某个 props 的变动,这是为什么呢?props 本身在源码中,也是一个被 reactive 包裹后的对象,因此它具有响应性,所以在 watch 方法中的回调函数会自动收集依赖,当 name 变动后,会自动调用这些回调逻辑。

  • 可以通过 watchEffect 方法进行监听。

const App3_2 = {
  // 类型定义的时候,仍然可以像 Vue2.x 一样
  props: {
    name: String
  },
  setup(props) {
    watchEffect(() => {
      // 可以通过 watchEffect 进行监听
      console.log(`name is: ` + props.name); // name is: app-32传递的值
    });
  }
};
const App3_2_parent = {
  components: { "app-32": App3_2 },
  template: `
    <div><app-32 name="app-32传递的值"></app-32></div>
  `
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • props 对象是响应式的 —— 它可以被当作数据源去观测,当后续 props 发生变动时它也会被框架内部同步更新。但对于用户代码来说,它是不可修改的(会导致警告)。
const App3_3 = {
  props: {
    name: String
  },
  setup(props) {
    watchEffect(() => {
      console.log(`name is: ` + name); // Will not be reactive! 需要通过 props 进行访问
    });
    // 如果修改
    // 触发警告:Set operation on key "name" failed: target is readonly. {name: "app-33传递的值"}
    props.name = "修改props";
  }
};
const App3_3_parent = {
  components: { "app-33": App3_3 },
  template: `
    <div><app-33 name="app-33传递的值"></app-33></div>
  `
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • props 也支持 TS。
interface IProps {
  name: string;
}
const MyComponent = {
  setup(props: IProps) {
    return {
      msg: `hello ${props.name}!`
    };
  },
  template: `<div>{{ msg }}</div>`
};
1
2
3
4
5
6
7
8
9
10
11

📌 context

  • setup 的第二个参数提供一个上下文对象,这个上下文对象提供一个可选的属性列表,和 Vue2.x 中挂载在 this 上的属性列表一致。

  • context 具有与 this.slots,this.parent,this.$root 对应的属性(属性,插槽,emit,parent,root)。

const App4 = {
  setup(props, context) {
    // Vue2.0 中是通过 this 才能访问到
    // setup 是不能使用 this 的,是通过 context 上下文进行访问的
    console.log(context.attrs);
    console.log(context.slots);
    console.log(context.emit);
  }
  // 或者使用解构获取值
  /*   setup(props, { attrs, slots, emit }) {
    console.log('解构',attrs, slots, emit);
  }, */
};
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 请记住 this 关键字在 setup 函数中是不可用的。
const App5 = {
  template: `
    <div>
      <div>{{ count }}</div>
      <button @click="handleClick">点击</button>
    </div>
  `,
  setup() {
    const count = ref(0);
    //  this 关键字在 setup() 函数中不可用。
    console.log(this); // this -> window
    console.log(this.count); // undefined
    function handleClick() {
      console.log(this); // this -> window
      console.log(this.count); // undefined
    }
    return {
      count,
      handleClick
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • context 也支持 TS。
interface Data {
  [key: string]: unknown
}

interface SetupContext {
  attrs: Data
  slots: Slots
  emit: ((event: string, ...args: unknown[]) => void)
}
function setup(
  props: Data,
  context: SetupContext
): Data
1
2
3
4
5
6
7
8
9
10
11
12
13

# watch

# vue2 中的 watch

const App = new Vue({
  el: "#app",
  template: `
    <button @click="handleClick">点击</button>
  `,
  data: {
    name: "深圳湾",
    address: "深圳"
  },
  watch: {
    name: {
      handler: function(value, oldValue) {
        console.log(value, oldValue);
      },
      immediate: true, // 设置 immediate,创建的时候会立即执行
      deep: true // 设置 deep,会进行深度监听
    },
    address: function(value, oldValue) {
      // 没有设置 immediate,在创建的时候不会执行,
      console.log(value, oldValue);
    }
  },
  methods: {
    handleClick() {
      // 触发监听
      this.name = "shenzhenwan";
      this.address = "shenzhen";
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# vue3 中的 watch 和 watchEffect

📌 wacth

  • vue3 的 watch 有更新,之前是 watch(fn,options),现在变成了 watch(source,cb,options?)

  • watch() API 提供了基于观察状态的变化来执行副作用的能力。它默认是 lazy,只有当监听的值发生变化的时候,才会触发回调函数。

  • watch() 接收三个参数:watch(source,cb,options?)

参数说明

  • 第一个参数被称作 “数据源”,它可以是:

    一个返回任意值的函数

    一个包装对象

    一个包含上述两种数据源的数组

source:可以是 getter 函数,值包装器或包含上述两种类型的数组(如果要查看多个源)

  • 第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发:

    callback:是类似于 Vue2 watcher 处理程序的函数,带有 2 个参数:newVal,oldVal。

    每个参数都可以是一个数组(用于观察多个源): [newVal1,newVal2,... newValN],[oldVal1,oldVal2,... oldValN]

  • 第三个可选参数 options。 deep 深度监听,类型: boolean,default: false,和 Vue2.x 行为一致,都是对对象的深度监听

    Lazy 和 Vue2.x immediate 正好相反,类型:Boolean, default: false

// 监视指定的数据源
const { reactive, watch, ref, watchEffect } = Vue;
const App = {
  setup() {
    // 监视 reactive 类型的数据源:
    const state = reactive({ count: 0 });
    // 定义 watch,只要 count 值变化,就会触发 watch 回调
    // watch 会在创建时会自动调用一次
    watch(
      () => state.count,
      (count, prevCount) => {
        //  默认是 lazy,所以一开始不会触发回调
        console.log("第一个监听", count, prevCount);
      }
    );
    setTimeout(() => {
      state.count++;
      // 现在就会触发回调
    }, 1000);

    // 监视 ref 类型的数据源:
    // 定义数据源
    const count = ref(0);
    // 指定要监视的数据源
    watch(count, (count, prevCount) => {
      console.log("第二个监听", count, prevCount);
    });
    setTimeout(() => {
      count.value++;
      // 现在就会触发回调
    }, 1000);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// deep 的使用,watch 和 watchEffect 的 options 可选参数,配置项相同
const App_1 = {
  template: `
    <div><button @click="add1">点击</button></div>
  `,
  setup() {
    const count1 = reactive({ count: { count: 0 } });
    watch(
      () => count1.count,
      (val, oldVal) => {
        console.log(count1, "count1");
      },
      { deep: true }
    );
    const add1 = () => {
      count1.count.count = Math.random();
    };
    return {
      add1
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
 * 监视多个数据源
 * 这种情况下,任意一个数据源的变化都会触发回调,同时回调会接收到包含对应值的数组作为参数
 */
const App1 = {
  setup() {
    const count = ref(0);
    const test = ref(0);
    const state = reactive({ name: "深圳湾", address: "深圳" });
    watch(
      [() => state.name, () => state.address], // Object.values(toRefs(state));
      ([name, address], [prevName, prevAddress]) => {
        console.log(name); // 新的 name 值
        console.log(address); // 新的 address 值
        console.log("------------");
        console.log(prevName); // 旧的 name 值
        console.log(prevAddress); // 旧的 address 值
      },
      {
        // 默认就是 lazy,在 watch 被创建的时候,不执行回调函数中的代码
        // 比如这里设置 watch 的可选参数 options 选项,immediate,则在创建的时候会立即执行
        // 源码中:function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
        immediate: true
      }
    );
    setTimeout(() => {
      // 任意一个数据源的变化都会触发回调
      state.name = "shenzhen";
    }, 1000);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 可以将多个源观察程序拆分为较小的观察程序。这有助于我们组织代码并创建具有不同选项的观察程序
const App1_1 = {
  setup() {
    const value = ref(0);
    const count = ref(0);
    const test = ref(0);
    // 拆分成两个
    watch([value, count], ([newValue, newCount], [oldValue, oldCount]) => {
      console.log(value.value, "第一个watch");
    });
    watch(
      () => test.value,
      (newTest, oldTest) => {
        console.log(value.value, "第二个watch");
      }
    );
    setTimeout(() => {
      value.value++;
      test.value++;
    }, 1000);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
 * 原先是 watch 的第三个参数来进行副作用的清除,现在使用 watchEffect 第一个参数 fn 来进行
 * watch(source,cb,options?)
 * watchEffect(fn,options?)
 */
const App2 = {
  setup() {
    watchEffect((onInvalidate) => {
      // const token = performAsyncOperation(id.value);
      onInvalidate(() => {
        // id has changed or watcher is stopped.
        // invalidate previously pending async operation
        // 副作用的清除
        // token.cancel();
        console.log("清除副作用等");
      });
    });
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 配合 TS 使用
type WatcherSource<T> = Ref<T> | (() => T)

type MapSources<T> = {
  [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}

// see `watchEffect` typing for shared options
interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // default: false
  deep?: boolean
}

// wacthing single source
function watch<T>(
  source: WatcherSource<T>,
  callback: (
    value: T,
    oldValue: T,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options?: WatchOptions
): StopHandle

// watching multiple sources
function watch<T extends WatcherSource<unknown>[]>(
  sources: T
  callback: (
    values: MapSources<T>,
    oldValues: MapSources<T>,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options? : WatchOptions
): StopHandle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

📌 watchEffect

// 基本使用示例
const { ref, reactive, watchEffect, watch, onUpdated, onMounted } = Vue;
const App = {
  setup() {
    const count = ref(0);
    watchEffect(() => console.log(count.value));
    // -> logs 0

    setTimeout(() => {
      count.value++;
      // -> logs 1
    }, 100);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 在组件的 setup()函数或生命周期挂钩期间调用 watchEffect 时,watch 会自动链接到组件的生命周期,并且在卸载组件时将自动停止。在其他情况下,它返回停止函数,可以调用该函数停止 watch。
// Stopping the Watcher
const App2 = {
  setup() {
    // 创建监视,并得到停止函数
    const stop = watchEffect(() => {
      /* ... */
    });
    // 调用停止函数,清除对应的监视
    stop();
  }
};
1
2
3
4
5
6
7
8
9
10
11
  • 清理副作用

    熟悉 react 的 useEffect 的同学就知道,useEffect 可以 return 一个函数来清理自身的副作用,而 Vue3.0 是以参数的形式。

    一般情况下,在生命周期销毁阶段或是你手动 stop 这个监听函数的情况下,都会自动清理副作用。但是有时候,当观察的数据源变化后,我们可能需要执行一些异步操作,如 setTimeOut、fetch,当这些异步操作完成之前,监测的数据源又发生变化的时候,我们可能要撤销还在等待的前一个操作,比如 clearTimeOut。为了处理这种情况,watchEffect 接收一个 fn 参数,可以注册一个回调函数来清除副作用。

// Side Effect Invalidation
const App3 = {
  setup() {
    watchEffect((onInvalidate) => {
      onInvalidate(() => {
        // id has changed or watcher is stopped.
        // invalidate previously pending async operation
        /* 清除副作用 */
      });
    });
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

注意

那么为什么 Vue 不像 React 那样,return 一个清理副作用的函数,而是通过参数呢?

这是因为,我们可能这么写 watch: async 函数隐性地返回一个 promise,这样的情况下,我们是无法返回一个需要被立刻注册的清理函数的。

const App3_1 = {
  setup() {
    const data = ref(null);
    function async() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve("async数据");
        }, 2000);
      });
    }
    watchEffect(async () => {
      data.value = await async();
    });
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 如果你想拿到 dom,或者 template 上的 refs,可以放在 mounted hook。
const App4_1 = {
  template: `
    <div>{{count}}</div>
  `,
  setup() {
    const count = ref(0);
    onMounted(() => {
      watchEffect(() => {
        // access the DOM or template refs
        // 现在组件已经被挂载
      });
    });
    return {
      count
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 控制 watch 的回调调用时机

    默认情况下,watchEffect 会在组件更新之后调用,如果你想在组件更新前调用,你可以传第三个参数 flush,表示回调调用时机。第三个参数是个对象,有几个选项:

    post 默认值,在组件更新之后

    pre 组件更新之前

    sync 同步调用

// 同步调用
const App5_1 = {
  setup() {
    watchEffect(
      () => {
        console.log("会被同步调用");
      },
      {
        flush: "sync"
      }
    );
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// 组件更新之前调用
const App5_2 = {
  template: `
    <div>{{count}}<button @click="handleClick">点击</button></div>
  `,
  setup() {
    const count = ref(0);
    function handleClick() {
      count.value++;
    }
    onMounted(() => {
      console.log("onMounted");
    });
    watchEffect(
      () => {
        console.log("onMounted前调用", count.value);
      },
      {
        flush: "pre"
      }
    );
    return {
      count,
      handleClick
    };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • watch 的 debugger 钩子函数 onTrackonTrigger,分别在依赖追踪(跟踪 reactive 或 ref 作为依赖项)和依赖发生变化时调用。
const App6 = {
  setup() {
    const num = ref(0);
    watchEffect(
      () => {
        /* side effect */
        console.log(num.value);
      },
      {
        onTrigger(e) {
          debugger;
        }
      }
    );
    setTimeout(() => {
      // 示意触发onTrigger
      num.value++;
    }, 1000);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注意

onTrack 和 onTrigger 仅在开发环境有效。

  • 配合 TS 使用
interface WatchEffectOptions {
  flush?: "pre" | "post" | "sync";
  onTrack?: (event: DebuggerEvent) => void;
  onTrigger?: (event: DebuggerEvent) => void;
}

interface DebuggerEvent {
  effect: ReactiveEffect;
  target: any;
  type: OperationTypes;
  key: string | symbol | undefined;
}

type InvalidateCbRegistrator = (invalidate: () => void) => void;

type StopHandle = () => void;

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# defineComponent

  • 这个函数不是必须的,除非你想要完美结合 TypeScript 提供的类型推断来进行项目的开发。

  • 这个函数仅仅提供了类型推断,方便在结合 TypeScript 书写代码时,能为 setup() 中的 props 提供完整的类型推断。

const { defineComponent } = Vue;
const testFC = defineComponent({
  props: {
    name: String
  },
  setup(props) {
    console.log(props.name);
    return {
      props
    };
  }
});
1
2
3
4
5
6
7
8
9
10
11
12

# readonly

  • readonly 函数接收一个 ref 或者 reactive 包装对象,返回一个只读的响应式对象。
const { readonly, reactive, watch, watchEffect } = Vue;

const App = {
  setup() {
    const original = reactive({ count: 0 });
    const copy = readonly(original);
    // [Vue warn]: `watch(fn, options?)` signature has been moved to a separate API. Use `watchEffect(fn, options?)` instead.
    // `watch` now only supports `watch(source, cb, options?) signature.
    // watch 有更新,之前是,watch(fn,options),现在打包的这个vue3版本是最新的一版,有了一些更新,
    // watch 的用法变成了,watch(source,cb,options?)
    // 另外增加了 watchEffect(fn,options?)

    // watch(() => {
    // 这里换成 watchEffect 就没有警告了
    watchEffect(() => {
      // works for reactivity tracking
      console.log("值发生变化", copy.count); // 第一次是0,watchEffect 会首先执行一次,这个执行时机是可以通过 options 进行配置的,后边改变后监听到数据变化,第二次打印1
    });
    // mutating original will trigger watchers relying on the copy
    // 更改原始值将触发副本的更新
    original.count++;
    // mutating the copy will fail and result in a warning
    copy.count++; // 触发警告!  Set operation on key "count" failed: target is readonly.
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# provide & inject

  • 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,实现全局状态共享。不论组件层次有多深,在上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性 context 很相似。

  • 在 Vue 项目中,如果你不想引入 Vuex,也可以考虑用 provide + inject 取代它。类似 React 的 Context + useReducer 一定程度上可以取代 redux 一样,效果也非常不错。

# vue2 中的用法

  • vue2.2.0 中新增了这对特性。

  • provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key(避免重复),但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。

  • inject 选项应该是一个字符串数组,或一个对象,对象的 key 是本地的绑定名,value 是在可用的注入内容中搜索用的 key (字符串或 Symbol),或一个对象,该对象的 from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol),default 属性是降级情况下使用的 value。

// 子组件注入 'foo'
const childComp = Vue.component("child-comp", {
  template: `
    <div>子组件</div>
  `,
  inject: ["foo"],
  created() {
    console.log(this.foo); // => "bar"
  }
});

// 父级组件提供 'foo'
const Parent = new Vue({
  components: { "child-comp": childComp },
  el: "#app",
  template: `
    <div>父组件<child-comp></child-comp></div>
  `,
  provide: {
    foo: "bar"
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 利用 ES2015 Symbols、函数 provide 和对象 inject
const s = Symbol();

const Provider = {
  provide() {
    return {
      [s]: "foo"
    };
  }
};

const Child = {
  inject: { s }
  // ...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在 2.5.0+ 的注入可以通过设置默认值使其变成可选项
const Child1 = {
  inject: {
    foo: { default: "foo" }
  }
};
1
2
3
4
5
6
// 如果它需要从一个不同名字的属性注入,则使用 from 来表示其源属性
const Child2 = {
  inject: {
    foo: {
      from: "bar",
      default: "foo"
    }
  }
};
1
2
3
4
5
6
7
8
9
// 与 prop 的默认值类似,你需要对非原始值使用一个工厂方法
const Child3 = {
  inject: {
    foo: {
      from: "bar",
      default: () => [1, 2, 3]
    }
  }
};
1
2
3
4
5
6
7
8
9

# vue3 中的用法

  • provide 和 inject 都只能在 setup 函数中使用。

  • provide 接受两个参数,第一个参数是 provide 唯一名称,最好用 Symbol,避免重复;第二个参数是你要暴露的数据。

  • inject 接收两个参数,第一个参数是 provide 名称,第二个参数是默认数据。如果 provide 没有暴露自己的数据,那么使用 inject 的默认数据。

// 子级组件
// 按需导入 inject
const { inject } = Vue;
const Descendent = {
  setup() {
    // 调用 inject 函数时,通过指定的数据名称,获取到父级共享的数据
    const theme = inject(ThemeSymbol1, "light");
    console.log(theme); // dark
    // 如果父组件不传,就显示为light
    // 把接收到的共享数据 return 给 Template 使用
    return {
      theme
    };
  }
};

// 祖先组件
const { provide, ref, watchEffect } = Vue;
const ThemeSymbol1 = Symbol();
const Ancestor = {
  components: { descendent: Descendent },
  template: `
    <div><descendent></descendent></div>
  `,
  setup() {
    // 通过 provide 函数向子级组件共享数据(不限层级)
    // provide('要共享的数据名称', 被共享的数据)
    provide(ThemeSymbol1, "dark");
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 共享 ref 响应数据

// 子级组件
const ThemeSymbol2 = Symbol();
// 按需导入 inject
const Descendent1 = {
  setup() {
    const theme = inject(ThemeSymbol2, ref("light"));
    watchEffect(() => {
      console.log(`theme set to: ${theme.value}`);
    });
    // 把接收到的共享数据 return 给 Template 使用
    return {
      theme
    };
  }
};

// 祖先组件
const Ancestor1 = {
  components: { descendent: Descendent1 },
  template: `
    <div><descendent></descendent></div>
  `,
  setup() {
    // 定义 ref 响应式数据
    const themeRef = ref("dark");
    // 把 ref 数据通过 provide 提供的子组件使用
    provide(ThemeSymbol2, themeRef);
    setTimeout(() => {
      // 现在数据是响应式的了
      themeRef.value = "red";
    }, 1000);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  • 配合 TS 使用
interface InjectionKey<T> extends Symbol {}
function provide<T>(key: InjectionKey<T> | string, value: T): void;
1
2
// without default value
function inject<T>(key: InjectionKey<T> | string): T | undefined;
// with default value
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T;
1
2
3
4
// Vue 提供了 InjectionKey 接口,它是扩展 Symbol 的通用类型。
// 它可用于在提供者和使用者之间同步注入值的类型
import { InjectionKey, provide, inject } from "vue";

const key: InjectionKey<string> = Symbol();

provide(key, "foo"); // providing non-string value will result in error

const foo = inject(key); // type of foo: string | undefined
1
2
3
4
5
6
7
8
9
// 如果使用字符串键或非类型符号,则需要显式声明注入值的类型
const foo = inject<string>("foo"); // string | undefined
1
2

# 少用但重要的特性

# 全局挂载/配置 API 更改

  • Vue3.0 取消了 vue 全局变量,改为实例函数 createApp() 创建实例对象。

  • 为什么这么改变?其实也好理解,Vue3.x 基于函数式编程,所以一切皆函数。为了保证每个函数都有自己的小圈子能独立运行,所以从源头上就开始开刀。

// Vue2.x
import Vue from "vue";
import App from "./App.vue";

Vue.config.ignoredElements = [/^app-/];
Vue.use();
Vue.mixin();
Vue.component();
Vue.directive();

new Vue({
  render: (h) => h(App)
}).$mount("#app");

// Vue3.0
import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);
app.config.ignoredElements = [/^app-/];
app.use();
app.mixin();
app.component();
app.directive();
app.mount("#app");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 自定义指令 API

  • 自定义指令 API 在 Vue 3 中将略有变化,以便更好地与组件生命周期保持一致。
// Vue2.x
const MyDirective = {
  bind(el, binding, vnode, prevVnode) {
    console.log(el); // <div>自定义指令</div>
  },
  inserted() {},
  update() {},
  componentUpdated() {},
  unbind() {}
};
new Vue({
  el: "#app",
  directives: { MyDirective },
  template: `
    <div v-MyDirective>自定义指令</div>
  `
});

// Vue3.0
const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {
    console.log(el); // <div>自定义指令</div>
  },
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {}, // 新增的
  unmounted() {}
};
const App = {
  directives: { MyDirective },
  template: `
    <div v-MyDirective>自定义指令</div>
  `
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# v-model

  • 在 Vue2.3.0 新增了 .sync 修饰符。父组件调用子组件 text-document 时,子组件就可以修改父组件的 doc.title。
<text-document :title.sync="doc.title"></text-document>
1
  • v-model 是一种指令,可用于在给定组件上实现双向绑定。我们可以传递响应性属性,并从组件内部对其进行修改。比如常用在表单元素上:
<input v-model="property />
1

但是你知道可以对每个组件都使用 v-model 吗?在内部, v-model 只是传递 value 属性和侦听 input 事件的捷径。把上面的例子重写为以下语法,将具有完全相同的效果:

<input v-bind:value="property" v-on:input="property = $event.target.value" />
1

我们甚至可以用组件 model 属性来更改默认属性和事件的名称:

model: {
  prop: 'checked',
  event: 'change'
}
1
2
3
4

不过,在 vue2.x 中,每个组件只能有一个 v-model。

  • 在 vue3 中,我们能够给 v-model 属性名,并根据需要拥有尽可能多的属性名。比如:
<InviteeForm v-model:name="inviteeName" v-model:email="inviteeEmail" />
1

# Suspense 组件

  • Vue 3 中的另一个从 React 学来的功能是 Suspense 组件。

  • Suspense 能够暂停你的组件渲染,并渲染后备组件,直到条件满足为止。事实证明,Suspense 只是带有插槽的组件。

  • 在 Vue2.x 中你应该会经常遇到这种场景:

<template>
  <div>
    <div v-if="!loading">...</div>
    <div v-if="loading">Loading...</div>
  </div>
</template>
1
2
3
4
5
6

安装 vue-async-manager (opens new window) 插件,就变成了:

<template>
  <div>
    <Suspense>
      <div>...</div>
      <div slot="fallback">Loading...</div>
    </Suspense>
  </div>
</template>
1
2
3
4
5
6
7
8
  • vue3.0 中新增了 Suspense 组件后可以这么写:
<Suspense>
  <template #default>
    <Suspended-component />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>
1
2
3
4
5
6
7
8

#default 可以省略,#fallback 其实在 Vue3.x 中就是 slot 的简写。

直到 Suspended-component 完全渲染前将会显示后备内容。挂起可以等待,直到该组件被下载(如果该组件是异步组件的话),或者在 setup 函数中执行一些异步操作。

顺便说下,在 Vue3.x 中,定义一个异步组件使用:defineAsyncComponent

# Teleport 组件

  • 注意: teleport 是 3.0.0-alpha.11 刚改的名字,之前叫:portal。

  • Portals 是特殊的组件,用来在当前组件之外渲染某些内容。它也是在 React 中实现 (opens new window)的功能之一。这就是 React 文档关于 Portals 的内容:

    Portals

    “Portals 提供了一种独特的方法来将子级渲染到父组件的 DOM 层次结构之外的 DOM 节点中。”

这种处理模式,是弹出式窗口以及通常显示在页面顶部的组件所使用的一种非常好的方法。通过使用 Portals,你可以确保没有任何主机组件 CSS 规则会影响你要显示的组件,并且可以避免用 z-index 进行的黑客攻击。

  • Vue2.x 中你应该会经常遇到这种场景:
<!-- UserCard.vue -->
<template>
  <div class="user-card">
    <b> {{ user.name }} </b>
    <button @click="isPopUpOpen = true">删除用户</button>

    <!-- 注意这一块代码 -->
    <div v-show="isPopUpOpen">
      <p>确定删除?</p>
      <button @click="removeUser">确定</button>
      <button @click="isPopUpOpen = false">取消</button>
    </div>
  </div>
</template>

<!-- 在最外层的 App.vue 中: -->
<!-- 预留一块空地,专门用来显示这个容易被遮挡的层 -->
<div id="modal-container"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

以上代码就是当我们需要做一个弹窗的时候,按照业务逻辑,弹窗和其他代码在一块写着。但是这么写往往会出现问题,就是一旦我们点击 “删除用户”,本希望弹窗显示,但是往往这个弹出框被外边的元素挡住!由于 z-index 的原因。那我们就想方设法让这个弹出框直接挂在到 body 节点上,这样就没问题了。比如:可以通过 Javascript 追加这个弹出框到 body 中等,处理起来比较麻烦。关键的问题是:业务逻辑被打断了,代码也不连贯了。

  • vue3.0 新增了 Teleport 这个组件后,可以这么写:
const { ref } = Vue;
const App3 = {
  template: `
    <div>
      <button @click="handleClick">点击显示弹窗</button>
      <Teleport to="#modal-container">
        <div v-show="isShow">
          弹窗
        </div>            
      </Teleport>
    </div>
  `,
  setup() {
    const isShow = ref(false);
    function handleClick() {
      isShow.value = true;
    }
    return {
      isShow,
      handleClick
    };
  }
};

// 在最外层的 App.vue 中:
// 预留一块空地,专门用来显示这个容易被遮挡的层
<div id="modal-container"></div>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 逻辑抽取与复用

# vue2 中的实现方法

在 vue2 中要实现逻辑复用主要有两种方式:

📌 1. mixin

mixin 的确能抽取逻辑并实现逻辑复用(我更多用它来定义接口),但这种方式着实不好,mixin 更多是一种代码复用的手段:

  • 命名冲突。mixin 的所有 option 如果与组件或其它 mixin 相同会被覆盖(这个问题可以使用 Mixin Factory 解决)。

  • 没有运行时实例。顾名思义,mixin 不过是把对应的 option 混入组件内,运行时不存在所抽取的逻辑实例。

  • 松散的关系。Options API 注定所抽取的逻辑的组织是松散,逻辑内部之间的关系也是松散的。

  • 含蓄的属性增加。mixin 加入的 option 是含蓄的,新手会迷惑于莫名其妙就存在的一个属性,尤其是在有多个 mixin 的时候,无法知道当前属性是哪个 mixin 的。

📌 2. scoped slot

scoped slot 也可以实现逻辑抽取,使用一个组件抽取逻辑,然后通过作用域插槽暴露给子组件。但是这种方式也有缺点:

  • 性能差。仅为了抽取逻辑,需要创建维护一个组件实例。

  • 一般需要增加配置,不灵活。需要在 slot 上增加配置,以应对更多的情况。

<!-- GenericSort.vue -->
<template>
  <div>
    <!-- 暴露逻辑的数据给子组件 -->
    <slot :data="data"></slot>
  </div>
</template>
<script>
  export default {
    // 在这里完成逻辑
    data() {}
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 使用的时候 -->
<template>
  <!--传入未排序的数据unSortdata-->
  <GenericSort data="unSortdata">
    <template v-slot="sortData">
      <!-- 使用经过处理后的sortData数据 -->
    <template>
  </GenericSearch>
</template>
1
2
3
4
5
6
7
8
9

# vue3 中的实现方法

  1. vue3 中提供了 Composition API 的方式,这种方式允许像函数般抽离逻辑。

  2. Options API 和 Composition API 的区别在于:

  • Options API:组件包含属性/方法/选项的类型。

    组件选项可能变得组织起来复杂且难以维护(怪异的组件)。逻辑可能涉及 props 和 data() 的属性,某些方法,某个钩子(beforeMount/mounted)以及 watch。因此,一个逻辑将分散在多个选项中。

  • Composition API:组件将逻辑封装到函数中。

    使用 Composition API,每个功能都是大型组件的一部分,它封装了与逻辑相关的所有代码(属性,方法,钩子,watch 观察者)。现在,较小的代码(函数)可以重复使用,并且组织得很好。

const { ref } = Vue;
// 逻辑抽取
const useCountdown = (initialCount) => {
  const count = ref(initialCount);
  const state = ref(false);
  const start = (initCount) => {
    state.value = true;
    if (initCount > 0) {
      count.value = initCount;
    }
    if (!count.value) {
      count.value = initialCount;
    }
    const interval = setInterval(() => {
      if (count.value === 0) {
        clearInterval(interval);
        state.value = false;
      } else {
        count.value--;
      }
    }, 1000);
  };
  return {
    count,
    start,
    state
  };
};

const App = {
  template: `
    <div>
      <p> {{count}}</p>
      <button @click="onClick" :disabled="state">Start</button>  
    </div>
  `,
  setup() {
    // 直接使用倒计时逻辑
    const { count, start, state } = useCountdown(10);
    const onClick = () => {
      start();
    };
    return { count, onClick, state };
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

建议

vue3 建议使用如 React hook 中一样使用 use 开头命名抽取的逻辑函数,如上代码抽取的逻辑几乎如函数一般,使用的时候也极其方便,完胜 vue2 中抽取逻辑的方法。

# vue2 中的 data 为什么是函数而不是对象

因为使用对象的话,每个实例上使用的 data 数据是相互影响的,因此 vue 组件为了保证每个实例上的 data 数据的独立性,规定了必须使用函数,而不是对象。通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新的副本数据对象。

# vue3 的 hook 和 react 的 hook 有什么区别

# 语法命名

Vue3 的 Composition API 使用 setup 函数和 ref、reactive、watch 等函数,采用了一种不同于 React 的风格。Vue3 的 API 更加函数式和声明式。

React 的 Hook 使用 useState、useEffect、useContext 等函数,以及自定义的 useCustomHook。React 的 Hook 命名以 "use" 开头。

# 使用限制

在 Vue3 的 Composition API 中,逻辑可以更灵活地组织在 setup 函数中,可以使用任意 JavaScript 特性。Vue3 的 Composition API 可以在条件或循环语句中使用。

在 React 中,Hook 的使用更加规范化,逻辑通常分散在不同的 Hook 中,便于复用和组合。React 的 Hook 不能在循环、条件或嵌套函数中使用。

# 生命周期

在 Vue3 的 Composition API 中,可以使用 onMounted、onUpdated、onUnmounted 等函数来处理生命周期钩子。

在 React 中,使用 useEffect 来处理组件的生命周期,包括组件挂载、更新和卸载时的逻辑。

# Vue.use 具体做了哪些工作

1. 检查插件是否已经安装:首先,Vue.use 会检查插件是否已经被安装过。如果已经安装过,它会直接返回,避免重复安装相同的插件。

2. 执行插件的 install 方法:如果插件尚未安装,Vue.use 会调用插件的 install 方法。这个方法是插件作者预先定义好的,它接收 Vue 构造函数作为参数,以及可选的选项对象。

3. 传递 Vue 构造函数和额外参数:在调用 install 方法时,Vue.use 会将 Vue 构造函数和 options 对象传递给插件的 install 方法。这使得插件可以在安装时执行一些初始化操作,并使用 Vue 的功能扩展应用。

4. 标记插件已安装:安装完成后,Vue.use 会在内部标记插件已经被安装,以避免重复安装。

# vue3 的性能提升体现在哪些方面

1. 响应式系统升级

Vue3 使⽤ Proxy-based 的响应式系统,相⽐ Vue2 的 Object.defineProperty 提供了更⾼的性能和更多的特性。

2. 编译优化

  • 静态树提升:编译时会识别出不需要动态改变的 DOM,这部分 DOM 会被提升出去,减少了重渲染的成本。

  • 静态属性提升:和静态树类似,静态的属性也会被提升。

  • 模板中的动态节点只会⽐对它们⾃⼰的 “范围”,⽽不是整个组件。

3. 更⼩的体积

Vue3 的源码是按功能模块化的,所以通过 tree-shaking 可以更有效地减⼩最终应⽤的体积。

4. Fragment 和 Portal

  • Fragment 允许你没有根元素的组件,这让 DOM 结构更加灵活。

  • Portal 提供了⼀种将⼦组件渲染到 DOM 树的其他位置的⽅法,提供了更⾼效的 DOM 更新策略。

5. 更快的挂载和更新

新的 Diff 算法(Longest Stable Subsequence)优化了列表渲染的性能。

6. ⾃定义渲染器 API

Vue 3 提供了更多底层的 API,允许开发者创建⾃定义渲染器,从⽽优化特定场景下的性能。

7. Composition API

提供了更灵活的逻辑复⽤和组合⽅式,⽽不需要通过 mixins 或者⾼阶组件,这样可以减少组件之间不必要的依赖和冲突,从⽽提⾼性能。

8. 更⾼效的事件处理

事件侦听器在内部被更⾼效地管理,尤其是在⼤型应⽤中。

9. 异步组件和代码分割

Vue3 提供了更简洁的异步组件和代码分割的⽀持,使得按需加载更加容易,从⽽提⾼了应⽤的加载性能。

10. 更多⽣命周期钩⼦和⼯具⽅法

新增的⽣命周期钩⼦和⼯具⽅法(如 nextTick )为性能优化提供了更多可能。

# vue2、vue3 和 react 中 dom diff 算法的区别

vue2 是双端比较,vue3 是最长递增子序列,react 是从左向右比较(仅右移)。

# vue 父子组件的通信方式

# props / $emit

⽗组件通过 props 向⼦组件传递数据。

<!-- ⽗组件 -->
<ChildComponent :someProp="parentData" />

<!-- ⼦组件 -->
props: ['someProp']
1
2
3
4
5

⼦组件通过⾃定义事件向⽗组件发送消息。

<!-- ⽗组件 -->
<ChildComponent @customEvent="handleEvent" />

<!-- ⼦组件 -->
this.$emit('customEvent', eventData);
1
2
3
4
5

从内部实现的⻆度看,Vue 内部会维护⼀个事件监听器的列表(通常是⼀个对象或 Map),键是事件名称,值是⼀个包含所有监听函数的数组。当 emit 被调⽤时,Vue 会查找这个列表,找到与事件名称匹配的所有监听函数,并按照它们被添加的顺序执⾏它们,同时将传⼊的数据作为参数传递给这些监听函数。

这个机制允许 Vue 组件具有很⾼的灵活性,因为你可以随时添加或删除事件监听器,也可以通过 emit 发送任何类型的数据。这也是 Vue 在组件间通信⽅⾯⾮常强⼤的⼀个原因。

# 作用域插槽

⽗组件可以通过作⽤域插槽访问⼦组件暴露出的属性或⽅法。

<!-- ⽗组件 -->
<ChildComponent>
  <template #default="slotProps">
    {{ slotProps.someValue }}
  </template>
</ChildComponent>
1
2
3
4
5
6

# Refs

⽗组件可以使⽤ ref 直接访问⼦组件的实例。

<!-- ⽗组件 -->
<ChildComponent ref="childRef" />

this.$refs.childRef.someMethod();
1
2
3
4

# Provide / Inject

provide 和 inject 可以⽤于跨多级⽗⼦组件进⾏数据或⽅法的传递。

  • 在⼀个组件中,你可以使⽤ provide 选项(或 provide 函数)来声明该组件要提供哪些数据或⽅法给后代组件。

  • 在任何后代组件中,你可以使⽤ inject 选项(或 inject 函数)来声明该组件需要注⼊哪些数据或⽅法。

实现原理如下:

1. 响应式存储:当⼀个组件调⽤ provide 时,Vue 会在内部将提供的数据和⽅法存储在⼀个响应式的依赖注⼊对象中。

2. 查找逻辑:当⼀个组件调⽤ inject 时,Vue 会从该组件开始,向上遍历组件树,直到找到⼀个组件,该组件提供了相应的依赖。

3. 建⽴响应式连接:如果提供的数据是响应式的,通过 inject 获取的数据也将是响应式的。这意味着,如果 provide 提供的数据发⽣变化,所有注⼊了该数据的组件都会⾃动更新。

4. Symbol:为了避免命名冲突,你也可以使⽤ ES6 的 Symbol 作为 provide 和 inject 的键。

# Bus / EventEmitter

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。

var Event = new Vue();

Event.$emit(事件名, 数据);

Event.$on(事件名, (data) => {});
1
2
3
4
5

# Vuex

对于更复杂的状态管理,通常使⽤ Vuex,它提供了⼀个集中式存储来管理多个组件之间的状态。

Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 进行,Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而所有异步操作或批量的同步操作都需要走 Action,但 Action 也是无法直接修改 State 的,还是需要通过 Mutation 来修改 State 的数据。最后,根据 State 的变化,渲染到视图上。

  • Vue Components:Vue 组件。HTML 页面上,负责接收用户操作等交互行为,执行 dispatch 方法触发对应 action 进行回应。

  • dispatch:操作行为触发方法,是唯一能执行 action 的方法。

  • actions:操作行为处理模块,由组件中的 $store.dispatch(action 名称, data1) 来触发。然后由 commit() 来触发 mutation 的调用 , 间接更新 state。负责处理 Vue Components 接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台 API 请求的操作就在这个模块中进行,包括触发其他 action 以及提交 mutation 的操作。该模块提供了 Promise 的封装,以支持 action 的链式触发。

  • commit:状态改变提交操作方法。对 mutation 进行提交,是唯一能执行 mutation 的方法。

  • mutations:状态改变操作方法,由 actions 中的 commit(mutation 名称) 来触发。是 Vuex 修改 state 的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些 hook 暴露出来,以进行 state 的监控等。

  • state:页面状态管理容器对象。集中存储 Vue components 中 data 对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用 Vue 的细粒度数据响应机制来进行高效的状态更新。

  • getters:state 对象读取方法。图中没有单独列出该模块,应该被包含在了 render 中,Vue Components 通过该方法读取全局 state 对象。

Vuex 存储的数据是响应式的,但是并不会保存起来,刷新之后就回到了初始状态。因此应该在 Vuex 里数据改变的时候把数据拷贝一份保存到 localStorage 里面,刷新之后,如果 localStorage 里有保存的数据,取出来再替换 store 里的 state。

也可以使用 vuex-persistedstate (opens new window) 这个库来解决 Vuex 刷新后状态丢失的问题,不过这个库现在作者已经不维护了。

# vue 中的 ref 和 reactive 有什么区别

在 Vue3 中, ref 和 reactive 都是⽤于创建响应式数据的,但它们有⼀些关键区别和使⽤场景。

# ref

  • 单⼀值:ref ⽤于创建⼀个响应式引⽤,通常⽤于基本类型(如 String, Number, Boolean)。

  • 访问值:当你需要访问 ref 创建的响应式变量时,需要使⽤ .value 属性。

const count = ref(0);
console.log(count.value); // 0
1
2
  • 模板中的简化:在模板中,你不需要使⽤ .value ,Vue 会⾃动解引⽤。
<template>
  <div>{{ count }}</div>
  <!-- ⾃动解引⽤ -->
</template>
1
2
3
4
  • 可重新赋值:你可以通过 count.value = 1 赋予⼀个新值。

  • 转换对象:使⽤ ref 创建的响应式对象,在解构或传递到其他函数时,会失去其响应性。需要使⽤ toRefs 或 toRef 来保持响应性。

# reactive

  • 对象/数组:reactive ⽤于创建响应式对象或数组。

  • 访问值:访问 reactive 创建的响应式对象就像访问普通对象⼀样。

const state = reactive({ count: 0 });
console.log(state.count); // 0
1
2
  • 不可重新赋值:对 reactive 创建的响应式对象的根重新赋值(例如 state = { ... } )不会改变响应性。但其内部属性可以被修改。

  • 嵌套响应性:当你创建⼀个响应式对象,其内部嵌套的所有对象和数组也会⾃动变为响应式。

  • 解构问题:如果你解构⼀个使⽤ reactive 创建的响应式对象,解构出来的值将失去响应性。这与 ref 是相似的,但你可以使⽤ toRefs 或 toRef 函数来解决这个问题。

ref 更适⽤于单⼀的、独⽴的值,或者当你需要在模板中直接使⽤时。reactive 更适⽤于复杂的、嵌套的对象或数组。你可以使⽤ toRef 和 toRefs 辅助函数在 ref 和 reactive 之间转换,以适应不同的使⽤场景。

# vue 中的 shallowReactive 和 shallowRef 有什么用

shallowReactive 和 shallowRef 是 Vue3 中⽤于创建浅响应式对象的 API,只会追踪第⼀层属性的变化。与 reactive 和 ref 不同,它们不会将嵌套的对象或数组转换为响应式。

当你使⽤ shallowReactive 包装⼀个对象后,这个对象的第⼀层属性会变成响应式的,但任何嵌套的对象或数组都不会变成响应式。这在 你不希望 Vue 跟踪嵌套属性变化时⾮常有⽤。

const shallow = shallowReactive({ a: { b: 1 } });
shallow.a.b = 2; // 这⾥不会触发视图的更新
1
2

shallowRef 类似于 shallowReactive ,但⽤于单个值。shallowRef 创建的 ref 对象在 .value 属性被访问时不会⾃动解包(unwrapping),并且它持有的嵌套对象不会被转换为响应式。

const sRef = shallowRef({ a: 1 });
sRef.value.a = 2; // 这⾥不会触发视图的更新
1
2

这两个 API 在以下⼏种情况下特别有⽤:

  • 当你处理⾮常⼤的对象或数组,且不需要跟踪它们所有嵌套属性的变化时,以减少性能开销。

  • 当你希望避免对象内部状态被 Vue 管理,例如第三⽅库的实例对象。

# v-model 的双向数据绑定是如何实现的

Vue3 中的 v-model 指令⽤于实现双向数据绑定,主要⽤在表单元素和⾃定义组件中。它是语法糖,背后实际上是⼀个组合了属性绑定和事件监听的机制。

1. 属性绑定

v-model 在内部会将变量绑定到元素或组件的某个属性上。对于原⽣ HTML 元素(如 input),通常会绑定到 value 属性。

<!-- 相当于 -->
<input :value="someVal" />
1
2

2. 事件监听

同时, v-model 也会⾃动添加⼀个事件监听器,⽤于捕获⽤户输⼊或改变。对于 input 元素,这通常是 input 事件。

<!-- 相当于 -->
<input @input="someVal = $event.target.value" />
1
2

3. 组合

结合上述两个步骤, v-model="someVal" 实际上是以下两个操作的缩写。

<input :value="someVal" @input="someVal = $event.target.value" />
1

4. ⾃定义组件

在⾃定义组件中, v-model 的⾏为也是类似的。默认情况下,它会绑定到组件的 modelValue 属性,并监听组件的 update:modelValue 事件。但你也可以⾃定义 prop 名称和事件。

5. 响应式更新

由于 Vue 的响应式系统,当数据变量(在这⾥是 someVal)改变时,所有与之绑定的 UI 元素也会⾃动更新。反之,当 UI 元素触发绑定的事件(如 input )时,数据变量也会被更新。

# vue 中的 watch 和 watchEffect 有何区别

watch (opens new window) 需要明确指定依赖,⽽ watchEffect (opens new window) 会⾃动收集依赖。

# vue 如何实现组件的懒加载

使⽤ defineAsyncComponent (opens new window) ⽅法,它会返回⼀个异步组件。

# vue 中的 computed 是如何实现的

在 Vue3 中,computed 函数是通过依赖跟踪和缓存机制实现的。

当你访问⼀个计算属性时,Vue 会执⾏与该计算属性相关联的函数,并同时记录所有被访问的响应式依赖(例如,其他响应式变量或其他计算属性)。

当这些依赖中的任何⼀个发⽣变化时,Vue 会知道需要重新计算这个计算属性。但是如果依赖没有变化,Vue 会直接返回上⼀次计算的结果,⽽不会重新执⾏计算函数,从⽽实现缓存和性能优化。

computed 使⽤缓存机制,只在依赖改变时重新计算。

这⼀切都是通过 Vue3 的响应式系统和 Proxy API 实现的,使得计算属性⾮常⾼效。

# 什么是 Suspense 组件,它是如何实现的

Vue3 中的 Suspense 组件是⽤于处理异步依赖的⼀种机制。

当我们有异步组件或者在 setup 函数中有异步操作并希望在这些异步依赖加载完成前显示⼀个 “fallback” 内容(如加载指示器),我们就可以使⽤ Suspense 组件。

Suspense 组件的工作原理:

1. 捕捉异步组件:Suspense 可以捕捉其作⽤域内的异步组件。当这些组件还未解析完成(通常是⽹络加载)时, Suspense 会渲染它的 fallback 插槽内容。

2. 与 async setup() 配合 :在组件的 setup 函数中,你也可以返回⼀个 Promise。如果你这样做了,Suspense 也会等待这个 Promise 解析完成才渲染实际内容。

3. 多层嵌套:Suspense 组件可以嵌套使⽤,这样内层的 Suspense 可以被外层的 Suspense “捕获”。

4. 响应式:由于 Suspense 是与 Vue 的响应式系统集成的,所以⼀旦异步依赖完成,Suspense 会⾃动重新渲染,替换 fallback 为实际内容。

5. 插槽:Suspense 使⽤了两个插槽 —— default 和 fallback。default 插槽⽤于你希望渲染的实际内容,fallback 插槽⽤于在等待异步依赖期间展示的内容。

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
<script>
  import { defineAsyncComponent } from "vue";
  const AsyncComponent = defineAsyncComponent(() =>
    import("./AsyncComponent.vue")
  );
  export default {
    components: {
      AsyncComponent
    }
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# vue 中的 Transition 组件是如何⼯作的

Vue3 的 Transition 组件⽤于在元素或组件的进⼊/离开过渡中⾃动应⽤类名,从⽽可以使⽤ CSS 或 JavaScript 钩⼦函数来定义过渡效果。

Transition 组件⼯作的基本步骤如下:

1. 监听条件:Transition 组件通常与条件渲染(如 v-if 、 v-show )或动态组件⼀起使⽤,以便知道何时触发过渡。

2. 阶段类名:在不同阶段(如进⼊或离开)的过渡中, Transition 组件会⾃动附加或删除类名。默认情况下,这些类名是 v-enter-active、 v-enter-from、v-enter-to 等。

3. CSS 过渡:通过配合上述⾃动应⽤的类名,你可以在 CSS 中定义相应的过渡或动画效果。

4. JavaScript 钩⼦:如果你需要更复杂的过渡效果或逻辑,还可以使⽤如 @beforeenter、@enter、@after-enter 等钩⼦。

5. 执⾏过渡:当触发条件改变(如从 v-if="false" 切换到 v-if="true"),Transition 组件会按照预定义的规则和钩⼦来执⾏相应的过渡效果。

6. 移除或插⼊ DOM:在过渡完成后,Transition 组件会⾃动处理 DOM 元素的插⼊或移除。

Transition 组件使得实现复杂的过渡和动画效果变得相对简单,同时也允许⾼度的⾃定义和灵活性。

# vue 中的 keep-alive 是如何实现的,具体缓存的是什么

# keep-alive 的基本用法

  1. keep-alive 有 3 个 props:include、exclude 和 max。keep-alive 包裹动态组件时,会缓存不活动的组件实例

  2. 当组件在 keep-alive 内被切换时,它的 activateddeactivated 两个生命周期钩子函数会被执行。

  3. keep-alive 只对第一个组件有效,和 keep-alive 搭配使用的一般有:动态组件router-view

# keep-alive 的实现原理

keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件的 key 从 keys 数组中删除,然后 push 到 keys 数组最后,以便清除最不常用组件。

  1. 获取 keep-alive 下第一个子组件的实例对象,通过它去获取这个组件的组件名。

  2. 通过当前组件名去匹配 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例 vnode。

  3. 需要缓存,判断它当前是否在缓存数组里面,存在,则将它原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)。

  4. 不存在,将组件 key 放入数组,然后判断当前 key 数组是否超过 max 所设置的范围,如果超过,那么削减未使用时间最长的一个组件的 key 值。

  5. 最后将这个组件的 keepAlive 设置为 true。

# keep-alive 的首次渲染和缓存渲染

keep-alive 的实现中,设置了 abstract 为 true,表示它是一个抽象组件,自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

  1. 首次渲染的时候,除了在 keep-alive 中建立缓存,设置 vnode.data.keepAlive 为 true,其他的过程和普通组件一样。

  2. 缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和vnode.data.keepAlive 进行判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程,最后直接把缓存的 DOM 对象直接插入到目标元素中,完成数据更新的情况下的渲染过程。

# LRU 缓存策略

LRU

从内存中找出最久未使用的数据置换新的数据。

LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 “如果数据最近被访问过,那么将来被访问的几率也更高”。

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;

  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

  3. 链表满的时候,将链表尾部的数据丢弃。

# vue 的 nextTick 有什么用,实现机制是什么

nextTick 可以在 DOM 更新完成之后执行回调函数,此时可以在函数中获取到更新后的 DOM 信息。

nextTick 的实现原理依赖于 JavaScript 的微任务和宏任务。vue 会先尝试使⽤ Promise.then()、MutationObserver 这些微任务来实现 nextTick;如果不支持,就会降级为使用 setImmediate、MessageChannel、setTimeout 这些宏任务来实现。

简单来说,当你调⽤ nextTick 函数,vue 会将传递给它的回调函数放⼊⼀个任务队列中。这个任务队列会在当前事件循环结束和下⼀个事件循环开始之间执⾏,确保回调函数执⾏时 DOM 已经更新。

# vue 中的 ssr

Vue3 提供了对 SSR (opens new window) 的⼀流⽀持,包括异步组件、数据预取等。

上次更新时间: 2024年01月15日 22:42:06