S11-08 Vue-进阶
[TOC]
自定义指令
自定义指令
在 Vue 的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model 等等,除了使用这些指令之外,Vue 也允许我们来自定义自己的指令。
注意:在 Vue 中,代码的复用和抽象主要还是通过组件;
通常在某些情况下,你需要对 DOM 元素进行底层操作,这个时候就会用到自定义指令;
自定义指令分为两种:
自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
自定义全局指令:app 的 directive 方法,可以在任意组件中被使用;
比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
实现方式一:如果我们使用默认的实现方式;
实现方式二:自定义一个 v-focus 的局部指令;
实现方式三:自定义一个 v-focus 的全局指令;
实现方式一:聚焦的默认实现

实现方式二:局部自定义指令
实现方式二:自定义一个 v-focus 的局部指令
这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;
它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加 v-);
自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;

方式三:自定义全局指令
自定义一个全局的 v-focus 指令可以让我们在任何地方直接使用

指令的生命周期
一个指令定义的对象,Vue 提供了如下的几个钩子函数:
created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
指令的参数和修饰符
如果我们指令需要接受一些参数或者修饰符应该如何操作呢?
info 是参数的名称;
aaa-bbb 是修饰符的名称;
后面是传入的具体的值;
在我们的生命周期中,我们可以通过 bindings 获取到对应的内容:


案例:时间格式化指令
自定义指令案例:时间戳的显示需求:
在开发中,大多数情况下从服务器获取到的都是时间戳;
我们需要将时间戳转换成具体格式化的时间来展示;
在 Vue2 中我们可以通过过滤器来完成;
在 Vue3 中我们可以通过 计算属性(computed) 或者 自定义一个方法(methods) 来完成;
其实我们还可以通过一个自定义的指令来完成;
我们来实现一个可以自动对时间格式化的指令 v-format-time:
- 这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入 app 即可;
代码:

高阶组件
<Teleport>
认识 Teleport
在组件化开发中,我们封装一个组件 A,在另外一个组件 B 中使用:
那么组件 A 中 template 的元素,会被挂载到组件 B 中 template 的某个位置;
最终我们的应用程序会形成一颗 DOM 树结构;
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到 Vue app 之外的其他位置:
比如移动到 body 元素上,或者我们有其他的 div#app 之外的元素上;
这个时候我们就可以通过 teleport 来完成;
Teleport 是什么呢?
它是一个 Vue 提供的内置组件,类似于 react 的 Portals;
teleport 翻译过来是心灵传输、远距离运输的意思;
- 它有两个属性:
- to:指定将其中的内容移动到的目标元素,可以使用选择器;
- disabled:是否禁用 teleport 的功能;
- 它有两个属性:
代码的效果


结合组件使用
当然,teleport 也可以和组件结合一起来使用:
- 我们可以在 teleport 中使用组件,并且也可以给他传入一些数据;


多个 teleport
如果我们将多个 teleport 应用到同一个目标上(to 的值相同),那么这些目标会进行合并:

实现效果如下:

<Suspense>
异步组件和 Suspense
注意:目前(2022-08-01)Suspense 显示的是一个实验性的特性,API 随时可能会修改。
Suspense 是一个内置的全局组件,该组件有两个插槽:
default:如果 default 可以显示,那么显示 default 的内容;
fallback:如果 default 无法显示,那么会显示 fallback 插槽的内容;

自定义插件
认识 Vue 插件
通常我们向 Vue 全局添加一些功能时,会采用插件的模式,它有两种编写方式:
对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
函数类型:一个 function,这个函数会在安装插件时自动执行;
插件可以完成的功能没有限制,比如下面的几种都是可以的:
添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
添加全局资源:指令/过滤器/过渡等;
通过全局 mixin 来添加一些组件选项;
一个库,提供自己的 API,同时提供上面提到的一个或多个功能;
插件的编写方式
对象类型的写法

函数类型的写法

h 函数
h 函数
Vue 推荐在绝大数情况下使用模板来创建你的 HTML,然后一些特殊的场景,你真的需要 JavaScript 的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
前面我们讲解过 VNode 和 VDOM 的概念:
Vue 在生成真实的 DOM 之前,会将我们的节点转换成 VNode,而 VNode 组合在一起形成一颗树结构,就是虚拟 DOM (VDOM);
事实上,我们之前编写的 template 中的 HTML 最终也是使用渲染函数生成对应的 VNode;
那么,如果你想充分的利用 JavaScript 的编程能力,我们可以自己来编写 createVNode 函数,生成对应的 VNode;
那么我们应该怎么来做呢?使用 h()函数:
h() 函数是一个用于创建 vnode 的一个函数;
其实更准备的命名是 createVNode() 函数,但是为了简便在 Vue 将之简化为 h() 函数;
基本使用
h()函数 如何使用呢?它接受三个参数:



注意事项:
如果没有 props,那么通常可以将 children 作为第二个参数传入;
如果会产生歧义,可以将 null 作为第二个参数传入,将 children 作为第三个参数传入;
h 函数可以在两个地方使用:
render 函数选项中;
setup 函数选项中(setup 本身需要是一个函数类型,函数再返回 h 函数创建的 VNode);


案例:计数器

JSX
babel 配置
如果我们希望在项目中使用 jsx,那么我们需要添加对 jsx 的支持:
jsx 我们通常会通过 Babel 来进行转换(React 编写的 jsx 就是通过 babel 转换的);
对于 Vue 来说,我们只需要在 Babel 中配置对应的插件即可;
安装 Babel 支持 Vue 的 jsx 插件:npm install @vue/babel-plugin-jsx -D
在 babel.config.js 配置文件中配置插件:


如果是 Vite 环境,需要安装插件:npm install @vitejs/plugin-vue-jsx -D
案例:计数器

响应式原理
响应式
案例:普通值的响应式:我们先来看一下响应式意味着什么?我们来看一段代码:
num 有一个初始化的值,有一段代码使用了这个值;
那么在 num 有一个新的值时,这段代码可以自动重新执行;

响应式:上面的这样一种可以自动响应数据变化的代码机制,我们就称之为是响应式的。
案例:对象的响应式:那么我们再来看一下对象的响应式。


响应式实现
实现-响应式函数
设计
响应式函数:执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中。问题就变成了当数据发生变化时,自动去执行某个函数。


问题:在开发中我们是有很多的函数的,我们如何区分一个函数是否需要响应式。
下面的函数中 foo 需要在 obj 的 name 发生变化时,重新执行,做出相应。

bar 函数是一个完全独立于 obj 的函数,它不需要执行任何响应式的操作。

手动收集
思路:区分一个函数是否需要响应式:手动收集响应式函数。
- 封装一个新的函数 watchFn;
- 凡是传入到 watchFn 的函数,就是需要响应式的;
- 其他默认定义的函数都是不需要响应式的;
代码实现:
1、封装专门手动收集响应式函数的函数 watchFn(),收集后立即执行一次函数。

2、调用 watchFn()函数,传入响应式的函数 foo、bar。

3、当属性变化时,遍历执行收集的函数。

封装-Depend
需求:监听多对象的响应式

目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
- 我们不可能在全局维护一大堆的数组来保存这些响应函数;
所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
封装 Depend 类:替代原来简单的 reactiveFns 数组
1、将上述的逻辑封装到一个 Depend 类。

2、调用类中的addDepend()方法收集依赖。

3、调用 watchFn()函数,传入响应式的函数 foo、bar。

4、数据变化后,手动调用notify()方法重新运行所有收集到的函数。

实现-监听对象变化

需求:监听对象变化:替代之前使用dep.notify()手动运行响应式函数。
- Vue2:通过
Object.defineProperty()的方式监听 - Vue3:通过
new Proxy()的方式监听
代码实现:
Vue2:通过Object.defineProperty()的方式监听

Vue3:通过new Proxy()的方式监听

实现-自动收集依赖
对象依赖管理

需求:管理多对象的多属性依赖
我们目前是创建了一个 Depend 对象,用来管理对于 name 变化需要监听的响应函数:
- 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
- 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
思路图解:在前面我们刚刚学习过WeakMap,并且在学习 WeakMap 的时候我讲到了后面通过 WeakMap 如何管理这种响应式的数据依赖。
- 1、dep 对象数据结构的管理
- 每一个对象的每一个属性都会对应一个 dep 对象
- 同一个对象的多个属性的 dep 对象存放在一个 map 对象中
- 多个对象的 map 对象,存放在一个 objMap 对象(弱引用)中
- 2、依赖收集:当执行 get 函数时,自动添加 fn 函数

对象依赖管理-实现
我们可以写一个getDepend()函数专门来管理这种依赖关系:
1、通过Object.defineProperty()函数的 setter 方法监听对象的修改,触发时回调 setter 方法。在其中生成 dep 实例,并返回保存到对象Map.get(属性)的 Map 中的 dep 实例,对象修改时通过dep.notify()方法重新运行收集到的响应函数。

2、封装 getDepend() 函数,负责通过 obj 的 key 生成/获取对应的 Depend 实例。

3、当对象属性变化时,会自动运行相关的响应函数。

5、


对象的依赖收集
问题:我们之前收集依赖的地方是在 watchFn 中,但是这种收集依赖的方式我们根本不知道是哪一个 key 的哪一个 depend 需要收集依赖,只能针对一个单独的 depend 对象来添加你的依赖对象。
思路:应该在调用 Proxy 或 Object.defineProperty 的 get 捕获器中收集依赖函数。
代码实现:
1、在watchFn()方法中将监听的函数保存到全局变量 reactiveFn 中。

2、在getter中访问到相关对象的属性时,将其添加到其对应的 dep 实例中。

3、当执行watchFn()函数时,会在它的回调中访问到对象的属性,我们需要将这些属性自动收集起来。

细节补充
问题一:如果函数中有用到两次 key,比如 name,那么这个函数会被收集两次。

思路:不使用数组,而是使用 Set 保存收集的响应函数。
代码实现:

问题二:我们并不希望将添加 reactiveFn 放到 get 中,以为它是属于 Dep 的行为。
思路:添加一个新的方法,用于收集依赖。
代码实现:
1、去除defineProperty()中对外部变量的依赖。

2、将对外部变量的依赖放入 Depend 类的 depend 方法中。

多个对象响应式
问题:目前的响应式是针对于 obj 一个对象的,其他对象无法实现响应式。

思路:封装 reactive()函数,针对所有的对象都可以变成响应式对象。
代码实现:
1、封装 reactive()函数。

2、使用 reactive()函数创建一个响应式对象。

3、通过watchFn()函数,收集依赖。

4、当对象变化时重新执行依赖该对象的函数。

重构-Vue3 Proxy
前面所实现的响应式的代码是 Vue2 的响应式原理。Vue3 主要通过 Proxy 来监听数据的变化以及收集相关的依赖。
代码实现:Proxy + Reflect
