new vue
vue究竟是什么?为什么就能实现这么多酷炫的功能,不知道大家有没有思考过这个问题。其实在每次初始化vue,使用new Vue({...})时,不难发现vue其实是一个类。不过即使在ES6已经如此普及的今天,vue的定义却是普通构造函数定义的,为什么没有采用ES6的class呢?这个我们稍后回答,通过层层追踪终于找到了vue被定义的地方:
function Vue(options) {
...
this._init(options)
}
因为是原理解析,
flow的类型检测及一些边界情况,如使用方式不对或参数不对或不是主要逻辑的代码我们就省略掉吧。比如省略号这里边界情况是使用时必须是new Vue()的形式,否则会报错。
这就是vue最初始被定义的地方,你没看错,就是这么简单。当执行new Vue时,内部会执行一个方法 this._init(options),将初始化的参数传入。
这里需要说明一点,在vue的内部,_符号开头定义的变量是供内部私有使用的,而$ 符号定义的变量是供用户使用的,而且用户自定义的变量不能以_或$开头,以防止内部冲突。我们接着看:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
现在可以回答之前的问题了,为什么不采用ES6的class来定义,因为这样可以方便的把vue的功能拆分到不同的目录中去维护,将vue的构造函数传入到以下方法内:
- initMixin(Vue):定义
_init方法。 - stateMixin(Vue):定义数据相关的方法
$set,$delete,$watch方法。 - eventsMixin(Vue):定义事件相关的方法
$on,$once,$off,$emit。 - lifecycleMixin(Vue):定义
_update,及生命周期相关的$forceUpdate和$destroy。 - renderMixin(Vue):定义
$nextTick,_render将render函数转为vnode。 这些方法都是在各自的文件内维护的,从而让代码结构更加清晰易懂可维护。
这些xxxMixin完成后,接着会定义一些全局的API:
export function initGlobalAPI(Vue) {
Vue.set方法
Vue.delete方法
Vue.nextTick方法
...
内置组件:
keep-alive
transition
transition-group
...
initUse(Vue):Vue.use方法
initMixin(Vue):Vue.mixin方法
initExtend(Vue):Vue.extend方法
initAssetRegisters(Vue):Vue.component,Vue.directive,Vue.filter方法
}
源码:
/* @flow */
import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
这里有部分API和xxxMixin定义的原型方法功能是类似或相同的,如this.$set和Vue.set他们都是使用set这样一个内部定义的方法。
这里需要提一下vue的架构设计,它的架构是分层式的。最底层是一个ES5的构造函数,再上层在原型上会定义一些_init、$watch、_render等这样的方法,再上层会在构造函数自身定义全局的一些API,如set、nextTick、use等(以上这些是不区分平台的核心代码),接着是跨平台和服务端渲染(这些暂时不在讨论范围)及编译器。将这些属性方法都定义好了之后,最后会导出一个完整的构造函数给到用户使用,而new Vue就是启动的钥匙。
目录结构
刚才是从比较微观的角度近距离的观察了vue,现在我们从宏观角度来了解它内部的代码结构是如何组建起来的。 目录如下:
|-- dist 打包后的vue版本
|-- flow 类型检测,3.0换了typeScript
|-- script 构建不同版本vue的相关配置
|-- src 源码
|-- compiler 编译器
|-- core 不区分平台的核心代码
|-- components 通用的抽象组件
|-- global-api 全局API
|-- instance 实例的构造函数和原型方法
|-- observer 数据响应式
|-- util 常用的工具方法
|-- vdom 虚拟dom相关
|-- platforms 不同平台不同实现
|-- server 服务端渲染
|-- sfc .vue单文件组件解析
|-- shared 全局通用工具方法
|-- test 测试
- flow:
javaScript是弱类型语言,使用flow以定义类型和检测类型,增加代码的健壮性。 - src/compiler:将
template模板编译为render函数。 - src/core:与平台无关通用的逻辑,可以运行在任何
javaScript环境下,如web、Node.js、weex嵌入原生应用中。 - src/platforms:针对
web平台和weex平台分别的实现,并提供统一的API供调用。 - src/observer:
vue检测数据数据变化改变视图的代码实现。 - src/vdom:将
render函数转为vnode从而patch为真实dom以及diff算法的代码实现。 - dist:存放着针对不同使用方式的不同的
vue版本。
vue版本
vue使用的是rollup构建的,具体怎么构建的不重要,总之会构建出很多不同版本的vue。按照使用方式的不同,可以分为以下三类:
- UMD:通过
<script>标签直接在浏览器中使用。 - CommonJS:使用比较旧的打包工具使用,如
webpack1。 - ES Module:配合现代打包工具使用,如
webpack2及以上。
而每个使用方式内又分为了完整版和运行时版本,这里主要以ES Module为例,有了官方脚手架其他两类应该没多少人用了。
在
vue的内部是只认render函数的,我们来自己定义一个render函数,也就是这么个东西:
new Vue({
data: {
msg: 'hello Vue!'
},
render(h) {
return h('span', this.msg);
}
}).$mount('#app');
可能有人会纳闷了,既然只认render函数,同时我们开发好像从来并没有写过render函数,而是使用的template模板。这是因为有vue-loader,它会将我们在template内定义的内容编译为render函数,而这个编译就是区分完整版和运行时版本的关键所在,完整版就自带这个编译器,而运行时版本就没有,如下面这段代码如果是在运行时版本环境下就会报错了:
new Vue({
data: {
msg: 'hello Vue!'
},
template: `<div>{{msg}}</div>`
})
vue-cli默认是使用运行时版本的,更改或覆盖脚手架内的默认配置,将其更改为完整版即可通过编译:'vue$': 'vue/dist/vue.esm.js',推荐还是使用运行时版本。
runtime和runtime-only这两个版本的区别?
- 最明显的就是大小的区别,带编译器会比不带的版本大
6kb。 - 编译的时机不同,编译器是运行时编译,性能会有一定的损耗;运行时版本是借助
loader做的离线编译,运行性能更高。
this._init()的初始化之旅
我们知道new Vue()时,内部会执行一个this._init()方法,这个方法是在initMixin(Vue)内定义的:
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
...
}
}
当执行new Vue()执行后,触发的一系列初始化都在_init方法中启动,它的实现如下:
let uid = 0
Vue.prototype._init = function(options) {
const vm = this
vm._uid = uid++ // 唯一标识
vm.$options = mergeOptions( // 合并options
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
...
initLifecycle(vm) // 开始一系列的初始化
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
每一个组件都是一个Vue构造函数的子类
会定义_uid属性,这是为每个组件每一次初始化时做的一个唯一的私有属性标识,有时候会有些作用。
合并options配置
面试题
请问可以在
beforeCreate钩子内通过this访问到data中定义的变量么,为什么以及请问这个钩子可以做什么?
是不可以访问的,因为在vue初始化阶段,这个时候data中的变量还没有被挂载到this上,这个时候访问值会是undefined。beforeCreate这个钩子在平时业务开发中用的比较少,而像插件内部的instanll方法通过Vue.use方法安装时一般会选在beforeCreate这个钩子内执行,vue-router和vuex就是这么干的。
请问
methods内的方法可以使用箭头函数么,会造成什么样的结果?
是不可以使用箭头函数的,因为箭头函数的this是定义时就绑定的。在vue的内部,methods内每个方法的上下文是当前的vm组件实例,methods[key].bind(vm),而如果使用使用箭头函数,函数的上下文就变成了父级的上下文,也就是undefined了,结果就是通过undefined访问任何变量都会报错。
请问
vue@2为什么要引入虚拟Dom,谈谈对虚拟Dom的理解?
-
随着现代应用对页面的功能要求越复杂,管理的状态越多,如果还是使用之前的
JavaScript线程去频繁操作GUI线程的硕大Dom,对性能会有很大的损耗,而且也会造成状态难以管理,逻辑混乱等情况。引入虚拟Dom后,在框架的内部就将虚拟Dom树形结构与真实Dom做了映射,让我们不用在命令式的去操作Dom,可以将重心转为去维护这棵树形结构内的状态即可,状态的变化就会驱动Dom发生改变,具体的Dom操作vue帮我们完成,而且这些大部分可以在JavaScript线程完成,性能更高。 -
虚拟
Dom只是一种数据结构,可以让它不仅仅使用在浏览器环境,还可以用与SSR以及Weex等场景。
父子两个组件同时定义了
beforeCreate、created、beforeMounte、mounted四个钩子,它们的执行顺序是怎么样的?
首先会执行父组件的初始化过程,所以会依次执行beforeCreate、created、在执行挂载前又会执行beforeMount钩子,不过在生成真实dom的__patch__过程中遇到嵌套子组件后又会转为去执行子组件的初始化钩子beforeCreate、created,子组件在挂载前会执行beforeMounte,再完成子组件的Dom创建后执行mounted。这个父组件的__patch__过程才算完成,最后执行父组件的mounted钩子,这就是它们的执行顺序。执行顺序如下:
parent beforeCreate
parent created
parent beforeMounte
child beforeCreate
child created
child beforeMounte
child mounted
parent mounted
当前组件模板中用到的变量一定要定义在
data里么?
data中的变量都会被代理到当前this下,所以我们也可以在this下挂载属性,只要不重名即可。而且定义在data中的变量在vue的内部会将它包装成响应式的数据,让它拥有变更即可驱动视图变化的能力。但是如果这个数据不需要驱动视图,定义在created或mounted钩子内也是可以的,因为不会执行响应式的包装方法,对性能也是一种提升
请简单描述下
vue响应式系统?
简单来说就是使用Object.defineProperty这个API为数据设置get和set。当读取到某个属性时,触发get将读取它的组件对应的render watcher收集起来;当重置赋值时,触发set通知组件重新渲染页面。如果数据的类型是数组的话,还做了单独的处理,对可以改变数组自身的方法进行重写,因为这些方法不是通过重新赋值改变的数组,不会触发set,所以要单独处理。响应系统也有自身的不足,所以官方给出了$set和$delete来弥补。
为什么
v-for里建议为每一项绑定key,而且最好具有唯一性,而不建议使用index?
在diff比对内部做更新子节点时,会根据oldVnode内没有处理的节点得到一个key值和下标对应的对象集合,为的就是当处理vnode每一个节点时,能快速查找该节点是否是已有的节点,从而提高整个diff比对的性能。如果是一个动态列表,key值最好能保持唯一性,但像轮播图那种不会变更的列表,使用index也是没问题的。
请问computed属性和watch属性分别什么场景使用?
当模板中的某个值需要通过一个或多个数据计算得到时,就可以使用计算属性,还有计算属性的函数不接受参数;监听属性主要是监听某个值发生变化后,对新值去进行逻辑处理。
说一下自定义事件机制
子组件使用this.emit里的参数会传递给父组件的回调函数,从而完成父子组件通信。
请说明下组件库中命令式弹窗组件的原理?
使用extend将组件转为构造函数,在实例化这个这个构造函数后,就会得到$el属性,也就是组件的真实Dom,这个时候我们就可以操作得到的真实的Dom去任意挂载,使用命令式也可以调用。
请说明下transition组件的实现原理?
transition组件是一个抽象组件,不会渲染出任何的Dom,它主要是帮助我们更加方便的写出动画。以插槽的形式对内部单一的子节点进行动画的管理,在渲染阶段就会往子节点的虚拟Dom上挂载一个transition属性,表示它的一个被transition组件包裹的节点,在path阶段就会执行transition组件内部钩子,钩子里分为enter和leave状态,在这个被包裹的子节点上使用v-if或v-show进行状态的切换。你可以使用Css也可以使用JavaScript钩子,使用Css方式时会在enter/leave状态内进行class类名的添加和删除,用户只需要写出对应类名的动画即可。如果使用JavaScript钩子,则也是按照顺序的执行指定的函数,而这些函数也是需要用户自己定义,组件只是控制这个的流程而已。