返回
专栏第三篇-基本使用和原型设计
在前面两篇文章中,我们熟悉了源码的目录结构以及构建方式。
今天我们继续来说下Vue的原型设计。
一、Vue的基础使用
官方推荐 Vue 的使用方式主要有两种:通过 CDN 和 NPM。
使用 CDN,我们可以通过在 HTML 中添加 <script>
标签直接引入打包好的 vue.js 文件,这是一种快速且简便的方法。
1 | // CDN方式 |
而 NPM 方式则涉及到与模块打包工具如 webpack 或 Browserify 的集成,通过执行 npm install vue 命令来安装 Vue,这通常是我们在开发大型应用时的首选方式。
1 | // NPM方式 |
当Vue被引入时,通常我们会在入口文件中去 new 一个Vue实例。
然后再利用实例上的$mount方法
将对应的模版内容挂载到浏览器中#app
节点的位置上。
1 | // NPM方式 |
二、为什么Vue不是一个类
上一节我们说了使用 Vue 需要 new 一下。
所以你可能会习惯性的认为 Vue 是一个类。
但是我们打开Vue被定义的core/instance/index.js
文件。
1 | function Vue(options) { |
会发现Vue是使用函数定义的。
那么Vue源码中为什么不使用类呢?
在Vue中有大量的扩展实例属性的操作如:Vue.prototype.$mount=xxx
。
其实本质上类只是function的语法糖。
虽然说使用类也可以进行扩展:
1 | class Animate{ |
但是用类和原型这么混合使用,难免会让人感到不适,也算是一种开发规范和习惯吧。
大部分开源库依旧使用的是构造函数function的方式。
三、必须使用new关键字来调用
可以看到在Vue构造函数内部存在一个判断 this instanceof Vue,那么这行代码应该如何理解呢?
我们知道通过 instanceof可以顺着原型链向上查找对应的构造函数。
所以这个判断的意思就是检查当前上下文(this)是否是一个 Vue实例。
如果不是一个实例,就会给你一个警告。
这要告知开发者Vue应该要作为一个构造函数来使用。
1 | // instance/index.js |
假设你这么使用 Vue:
1 | const vm = Vue() |
那么你将会在控制台看到一条报错信息。
四、Vue的原型设计
Vue是一个基于原型设计的前端框架。
在Vue被引入 import Vue from vue时,会通过多个函数在Vue原型上添加上一系列的方法。
1 | // 该函数在 Vue被引入时执行 |
那么在Vue.prototype上定义方法有什么作用呢?
前面2节,我们说到 Vue本质上是一个构造函数:
1 | function Vue(){ |
所以我们可以通过 new关键字来创建一个 vue实例。
1 | const vm = new Vue(); |
构造函数与原型对象
:每个构造函数都有一个 prototype属性,指向一个对象。这个对象被叫做原型对象,包含了由该构造函数创建的实例共享属性和方法。
实例对象的 __proto__ 属性
:每个实例对象都有一个__proto__
属性,指向构造函数的原型对象。
所以我们可以得出结论:Vue构造函数的显式原型(Vue.prototype)和基于它创建的实例的隐式原型(vm.__proto__)指向的是同一块内存空间。
当 Vue实例访问某个属性时,如果在自身属性中找不到,则会沿着__proto__属性指向的原型对象进行查找。
所以通过 vm 可以访问到定义在 Vue.prototype 的属性和方法。
通用这种方式,可以很方便的扩展方法,并不用显示的在 vm 上设置方法,做到了相对隔离。
五、Vue.extend利用原型链继承生成“子类“构造函数
Vue.extend是定义在Vue这个构造函数上的方法。
该方法主要用于创建Vue构造函数的“子类“,该“子类“继承 Vue构造函数上的原型方法和原型属性。
虽然Vue在技术上不是传统意义上的类,但是Vue.extend提供了一种类似于面向对象编程中继承的方式来定义组件。
1 | // 对原函数进行了一些简化 只保留了核心 |
我们简单的分析一下这几行代码。
声明了Super变量和 Sub变量分别指向
Vue构造函数
和VueComponent构造函数
。
5.1 使用场景
在Vue源码内部和使用Vue编写业务代码时都可以使用 Vue.extend这个 api。
5.1.1 内部创建组件
每一个Vue组件都对应着一个实例。
而这些实例都是通过 extend 方法创建的 VueComponent构造函数
生成的。
在render阶段,也就是在生成组件的vnode的时候会通过 extend 方法创建VueComponent构造函数
。
并赋值到 vnode 中的 componentOptions属性中。
1 | // 创建组件的 vnode 的方法 |
这里的
_base
实际上就是 Vue。这里的 context是vm实例,
vm.$options
是在实例化构造函数时通过mergeOptions
函数生成的。
然后在update阶段(渲染页面),会基于Ctor生成对应的实例,执行相应的初始化、渲染方法等。
1 | // 每个组件都会调用这个方法来创建对应的实例 |
5.1.2 在业务中的实际应用场景
在实际业务场景中,有很多地方都可以利用 extend 来扩展组件。
包括创建可复用的组件
、动态组件
、全局和局部注册
、临时组件
、自定义指令和插件
。
我们常用的 Element框架内部就利用了 Vue.extend 来扩展某些临时性的组件,例如模态对话框、提示信息等。
通过 Vue.extend 创建的组件构造函数可以按需创建和销毁,适合这类临时组件的管理。
1 | const Main = { |
$mount方法如果没有传参不会挂载,但是依旧可以生成 DOM节点,并赋值在 vm.$el上。
在 element 中的 Notification组件 就使用了 extend 进行扩展。
5.1.3 使用VueComponent继续扩展它的“子类”构造函数
需要关注的是在Vue.extend中,将Vue.extend方法同时赋值给了 VueComponent。
意味着赋予了 VueComponent继续扩展的能力:
1 | Vue.extend = function(){ |
这意味着我们可以无限的基于 VueComponent和它的“子类”扩展子类。
1 | import Vue from "@/my-vue2/platforms/web/entry-runtime-with-compiler-esm" |
上述代码中,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 被有层次的导入多个文件中,然后在文件中添加上对应的原型方法。
比如在:
- 在
platforms/web/runtime-with-compiler.js
文件中:
- Vue上扩展了定义了Vue.compile
- 重写了 Vue.prototype.$mount
- 在
platforms/web/runtime/index.js
文件中:
- 定义了Vue.prototype.$mount
- 定义了Vue.prototype.patch
- 扩展了扩展 Vue.config一些属性
- 扩展 Vue.options.directive
- 扩展 Vue.options.components
- 在
core/index.js
文件中:
- 使用了initGlobalAPI定义了一些全局方法 如 mixin
- 定义了Vue.prototype.$isServer
- 定义了Vue.prototype.$ssrContext
- 定义了Vue.FunctionalRenderContext
- 定义了Vue.version
- 在
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的维护和迭代。