返回

专栏第三篇-基本使用和原型设计

  在前面两篇文章中,我们熟悉了源码的目录结构以及构建方式。

  今天我们继续来说下Vue的原型设计。

一、Vue的基础使用

官方推荐 Vue 的使用方式主要有两种:通过 CDNNPM

使用 CDN,我们可以通过在 HTML 中添加 <script> 标签直接引入打包好的 vue.js 文件,这是一种快速且简便的方法。

1
2
// CDN方式
<script src="vue.js"></script>

而 NPM 方式则涉及到与模块打包工具如 webpack 或 Browserify 的集成,通过执行 npm install vue 命令来安装 Vue,这通常是我们在开发大型应用时的首选方式。

1
2
// NPM方式
import Vue from 'vue';

当Vue被引入时,通常我们会在入口文件中去 new 一个Vue实例。

然后再利用实例上的$mount方法将对应的模版内容挂载到浏览器中#app节点的位置上。

1
2
3
4
5
6
7
8
// NPM方式
// Nodejs版本
import Vue from 'vue/dist/vue.common';

// 需要实例化 Vue
new Vue({
template:`<div>Hello World</div>`
}).$mount("#app");

二、为什么Vue不是一个类

上一节我们说了使用 Vue 需要 new 一下。

所以你可能会习惯性的认为 Vue 是一个类。

但是我们打开Vue被定义的core/instance/index.js文件。

1
2
3
4
5
6
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

会发现Vue是使用函数定义的。

那么Vue源码中为什么不使用类呢?

在Vue中有大量的扩展实例属性的操作如:Vue.prototype.$mount=xxx

其实本质上类只是function的语法糖。

虽然说使用类也可以进行扩展:

1
2
3
4
5
6
class Animate{
}
Animate.prototype.eat = function(){
console.log("动物吃东西")
}
new Animate().eat();// 动物吃东西

但是用类和原型这么混合使用,难免会让人感到不适,也算是一种开发规范和习惯吧。

大部分开源库依旧使用的是构造函数function的方式。

三、必须使用new关键字来调用

可以看到在Vue构造函数内部存在一个判断 this instanceof Vue,那么这行代码应该如何理解呢?

我们知道通过 instanceof可以顺着原型链向上查找对应的构造函数。

所以这个判断的意思就是检查当前上下文(this)是否是一个 Vue实例。

如果不是一个实例,就会给你一个警告。

这要告知开发者Vue应该要作为一个构造函数来使用。

1
2
3
4
5
6
// instance/index.js
function Vue(options) {
if(__DEV__ && !this instanceof Vue){
warn('Vue is a constructor and should be called with the `new` keyword')
}
}

假设你这么使用 Vue:

1
const vm = Vue()

那么你将会在控制台看到一条报错信息。

四、Vue的原型设计

Vue是一个基于原型设计的前端框架。

在Vue被引入 import Vue from vue时,会通过多个函数在Vue原型上添加上一系列的方法。

1
2
3
4
5
6
// 该函数在 Vue被引入时执行
export function lifecycleMixin(Vue){
Vue.prototype._update = ()=>{
// xxxx
}
}

那么在Vue.prototype上定义方法有什么作用呢?

前面2节,我们说到 Vue本质上是一个构造函数:

1
2
3
function Vue(){
//xxx
}

所以我们可以通过 new关键字来创建一个 vue实例。

1
const vm = new Vue();
  1. 构造函数与原型对象:每个构造函数都有一个 prototype属性,指向一个对象。这个对象被叫做原型对象,包含了由该构造函数创建的实例共享属性和方法。

  2. 实例对象的 __proto__ 属性:每个实例对象都有一个 __proto__ 属性,指向构造函数的原型对象。

所以我们可以得出结论:Vue构造函数的显式原型(Vue.prototype)和基于它创建的实例的隐式原型(vm.__proto__)指向的是同一块内存空间

当 Vue实例访问某个属性时,如果在自身属性中找不到,则会沿着__proto__属性指向的原型对象进行查找。

所以通过 vm 可以访问到定义在 Vue.prototype 的属性和方法。

通用这种方式,可以很方便的扩展方法,并不用显示的在 vm 上设置方法,做到了相对隔离。

五、Vue.extend利用原型链继承生成“子类“构造函数

Vue.extend是定义在Vue这个构造函数上的方法。

该方法主要用于创建Vue构造函数的“子类“,该“子类“继承 Vue构造函数上的原型方法和原型属性。

虽然Vue在技术上不是传统意义上的类,但是Vue.extend提供了一种类似于面向对象编程中继承的方式来定义组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对原函数进行了一些简化 只保留了核心
Vue.extend = function(){
// this 为 Vue构造函数
const Super = this;
// Sub 为 VueComponent构造函数,代表组件构造函数
const Sub = function VueComponent(this){
// 和 Vue构造函数一样 会调用_init方法
this._init(options);
};
// 基于 Vue.prototype 创建一块新的内存,共享其属性和方法。
Sub.prototype = Object.create(Super.prototype);
// 修正 constructor指向
Sub.prototype.constructor = Sub;
return Sub;
}

我们简单的分析一下这几行代码。

声明了Super变量和 Sub变量分别指向Vue构造函数VueComponent构造函数

5.1 使用场景

在Vue源码内部和使用Vue编写业务代码时都可以使用 Vue.extend这个 api。

5.1.1 内部创建组件

每一个Vue组件都对应着一个实例。

而这些实例都是通过 extend 方法创建的 VueComponent构造函数 生成的。

在render阶段,也就是在生成组件的vnode的时候会通过 extend 方法创建VueComponent构造函数

并赋值到 vnode 中的 componentOptions属性中。

1
2
3
4
5
6
7
8
9
10
11
//  创建组件的 vnode 的方法
export function createComponent(Ctor,context){
// _base在引入时被设置为 Vue
// 这里的options后面我们会详细说明
const base = context.$options._base;
// 创建Vue子类构造函数
Ctor = base.extend();
return new Vnode(
{componentOptions:{Ctor}}
)
}

这里的_base实际上就是 Vue。

这里的 context是vm实例,vm.$options是在实例化构造函数时通过 mergeOptions函数生成的。

然后在update阶段(渲染页面),会基于Ctor生成对应的实例,执行相应的初始化、渲染方法等。

1
2
3
4
5
// 每个组件都会调用这个方法来创建对应的实例
// 这里的 componentOptions.Ctor 就是对应的VueComponent构造函数
export function createComponentInstanceForVnode(vnode){
return new vnode.componentOptions.Ctor()
}

5.1.2 在业务中的实际应用场景

在实际业务场景中,有很多地方都可以利用 extend 来扩展组件。

包括创建可复用的组件动态组件全局和局部注册临时组件自定义指令和插件

我们常用的 Element框架内部就利用了 Vue.extend 来扩展某些临时性的组件,例如模态对话框、提示信息等。

通过 Vue.extend 创建的组件构造函数可以按需创建和销毁,适合这类临时组件的管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Main = {
template:'<div>hello main</div>'
}

const NotificationConstructor = Vue.extend(Main);

let instance;

const Notification = function() {
// 通过VueComponent创建组件实例
instance = new NotificationConstructor();
// 使用$mount可以创建一个DOM节点 并挂载到instance.$el上
instance.$mount();
document.body.appendChild(instance.$el);
return instance;
}

Notification();

$mount方法如果没有传参不会挂载,但是依旧可以生成 DOM节点,并赋值在 vm.$el上。

在 element 中的 Notification组件 就使用了 extend 进行扩展。

5.1.3 使用VueComponent继续扩展它的“子类”构造函数

需要关注的是在Vue.extend中,将Vue.extend方法同时赋值给了 VueComponent。

意味着赋予了 VueComponent继续扩展的能力:

1
2
3
4
5
6
7
8
Vue.extend = function(){
const Super = this;
const Sub = function VueComponent(){
this._init();
}
// 省略部分代码
Sub.extend = Super.extend;
}

这意味着我们可以无限的基于 VueComponent和它的“子类”扩展子类。

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
import Vue from "@/my-vue2/platforms/web/entry-runtime-with-compiler-esm"

// 基于Vue构造函数创建的基础构造函数
const VueComponentConstructor = Vue.extend({
template:`<div>{{ name }}我是构造函数</div>`,
data(){
return {
name:"VueComponentConstructor"
}
}
});
// 可以复用父类构造函数VueComponentConstructor上的属性
const VueComponentChild1Constructor = VueComponentConstructor.extend({
data(){
return {
name:"VueComponentChild1Constructor"
}
}
})
// 可以复用父类构造函数VueComponentConstructor上的属性
const VueComponentChild2Constructor = VueComponentConstructor.extend({
data(){
return {
name:"VueComponentChild2Constructor"
}
}
})

function addNode(){
const vm1 = new VueComponentConstructor();
const vm2 = new VueComponentChild1Constructor();
const vm3 = new VueComponentChild2Constructor();

vm1.$mount()
vm2.$mount()
vm3.$mount()

document.body.appendChild(vm1.$el)
document.body.appendChild(vm2.$el)
document.body.appendChild(vm3.$el)
}

addNode();

上述代码中,VueComponentConstructor 是通过 Vue.extend 创建的一个基础组件构造函数。

在这个基础构造函数中传入了模板选项,我们之后创建的构造函数就可以复用 template选项,避免编写重复的模板。

我们这里只是简单的举了一个例子,通过这个例子我们了解到了extend的重要意义。真实的复用结构肯定更为复杂。

不过在实际开发中,一般我们应用中基本只存在 Vue构造函数和它的直接构造函数 VueComponent。

在组件库等基础库中可能会存在这种子类继续扩展子类的情况。

六、为什么Vue实例被叫做 vm

源码中你会看到大量的 vm。

使用 new Vue() 创建的Vue实例通常被叫做 vm。

Vue被称为VM,是因为它是一个基于MVVM(Model-View-ViewModel)架构的前端框架。

在MVVM架构中,VM代表ViewModel,负责管理视图(View)和数据模型(Model)之间的通信和交互。

七、引入时基于原型挂载方法

Vue是基于原型设计的前端框架。

后续的操作都是在调用原型上定义的方法。

那么 Vue是在什么时候对这些方法进行挂载的呢?

一部分是在 Vue引入的时候挂载的,一部分是在 Vue实例化的时候进行挂载的。

那么 Vue是如何在引入的时候进行挂载呢?

我们打开入口文件可以看到 Vue 被有层次的导入多个文件中,然后在文件中添加上对应的原型方法。

比如在:

  1. platforms/web/runtime-with-compiler.js文件中:
  • Vue上扩展了定义了Vue.compile
  • 重写了 Vue.prototype.$mount
  1. platforms/web/runtime/index.js文件中:
  • 定义了Vue.prototype.$mount
  • 定义了Vue.prototype.patch
  • 扩展了扩展 Vue.config一些属性
  • 扩展 Vue.options.directive
  • 扩展 Vue.options.components
  1. core/index.js文件中:
  • 使用了initGlobalAPI定义了一些全局方法 如 mixin
  • 定义了Vue.prototype.$isServer
  • 定义了Vue.prototype.$ssrContext
  • 定义了Vue.FunctionalRenderContext
  • 定义了Vue.version
  1. core/instance/index文件中:
  • 使用 initMixin 注入了 初始化有关的属性如:Vue.prototype.$init
  • 使用 stateMixin 注入了 跟状态有关的属性如:Vue.prototype.$set、Vue.prototype.$watch、Vue.prototype.$delete
  • 使用 eventsMixin 注入了 跟事件有关的属性如:Vue.prototype.$on、Vue.prototype.$off、Vue.prototype.$once
  • 使用 lifecycleMixin 注入了 跟整个 vue生命周期更新有关的属性如:Vue.prototype.$update
  • 使用 renderMixin 注入了 跟渲染相关的属性如:Vue.prototype._render

可以发现 Vue源码中是一层一层进行导入。

那么Vue为什么要这么设计目录结构呢?

我们可以看到每个模块都对 Vue对象做了相应的处理,比如说扩展属性、扩展实例属性等。

Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有。

这种技巧便于后期Vue的维护和迭代。


本站总访问量