二月结束了~连续更了一个月了嘻嘻(虽然是一年最短的一个月,虽然划水了好多篇...
接下来应该会随缘更新了,有时间vue源码部分也会慢慢补充深一点。开始投简历了= =
Vue构造函数
在使用 Vue 的时候,要使用 new 操作符进行调用,这说明 Vue 是一个构造函数。
Vue构造函数原型
// 从五个文件导入五个方法(不包括 warn)
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'
// 定义 Vue 构造函数
// 提醒要使用new操作符来调用vue
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)
}
// 将 Vue 作为参数传递给导入的五个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
// 导出 Vue
export default Vue
如上面代码所示,首先分别从 ./init.js、./state.js、./render.js、./events.js、./lifecycle.js 这五个文件中导入五个方法,分别是:initMixin、stateMixin、renderMixin、eventsMixin 以及 lifecycleMixin,然后定义了 Vue 构造函数,其中使用了安全模式来提醒你要使用 new 操作符来调用 Vue,接着将 Vue 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 Vue。
下面介绍一下导进来的这几个方法的作用:
- initMixin 方法
在 Vue.prototype 上定义了两个只读属性:$data 和 $props和三个方法:$set、$delete 以及 $watch。
- eventsMixin 方法
在 Vue.prototype 上添加了四个方法:$on、$once、$off和$emit。
- lifecycleMixin 方法
在 Vue.prototype 上添加了四个方法:_update、$forceUpdate和$destroy。
- renderMixin 方法
最终经过 renderMixin 之后,Vue.prototype 又被添加了如下方法:
// installRenderHelpers 函数中
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
由上面可知,每个 *Mixin 方法的作用其实就是包装 Vue.prototype,在其上挂载一些属性和方法。
Vue构造函数的静态属性和方法(全局API)
// 从 Vue 的出生文件导入 Vue
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 将 Vue 构造函数作为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
initGlobalAPI(Vue)
// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
// 在 Vue.prototype 上添加 $ssrContext 属性
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
// Vue.version 存储了当前 Vue 的版本号
Vue.version = '__VERSION__'
// 导出 Vue
export default Vue
首先调用了initGlobalAPI方法,然后在 Vue.prototype上分别添加了两个只读的属性:$isServer和$ssrContext,在构造函数上定义了FunctionalRenderContext 静态属性,以便在ssr中使用它。最后在Vue构造函数上添加了一个静态属性version,存储当前vue的版本值。
下面介绍一下initGlobalAPI方法的作用:在Vue构造函数上添加全局API。
-
在 Vue 构造函数上添加 config 只读属性
-
在 Vue 上添加了 util 属性,util对象拥有四个属性,分别是:warn、extend、mergeOptions 以及 defineReactive。
(Vue.util 以及 util 下的四个方法都不被认为是公共API的一部分,要避免依赖他们,但是你依然可以使用,只不过风险你要自己控制。)
-
在 Vue 上添加了四个属性分别是 set、delete、nextTick 以及 options(通过 Object.create(null) 创建的空对象)
-
接下来改造Vue.options
Vue.options = { components: Object.create(null), directives: Object.create(null), filters: Object.create(null), _base: Vue } -
将 builtInComponents 的属性混合到 Vue.options.components 中。最终Vue.options 已经变成了这样:
Vue.options = { components: { KeepAlive }, directives: Object.create(null), filters: Object.create(null), _base: Vue } -
initGlobalAPI 方法的最后部分,以 Vue 为参数调用了四个 init* 方法
initUse(Vue) // 添加Vue.use方法,用来安装插件 initMixin(Vue) // 在Vue上添加mixin全局API initExtend(Vue) // 在 Vue 上添加了 Vue.cid 静态属性,和 Vue.extend 静态方法 initAssetRegisters(Vue) // 在 Vue 上添加了 Vue.component、Vue.directive、Vue.filter 三个静态方法,分别用来注册组件、指令、过滤器。
总结
在这个 core/index.js 文件里,它首先将核心的 Vue,也就是在 core/instance/index.js 文件中的 Vue,也可以说是原型被包装(添加属性和方法)后的 Vue 导入,然后使用 initGlobalAPI 方法给 Vue 添加静态方法和属性,除此之外,在这个文件里,也对原型进行了修改,为其添加了两个属性:$isServer 和 $ssrContext,最后添加了 Vue.version 属性并导出了 Vue。
Vue平台化包装
platforms/web/runtime/index.js 文件文件的作用是对 Vue 进行平台化地包装:
- 设置平台化的 Vue.config。
- 在 Vue.options 上混合了两个指令(directives),分别是 model 和 show。
- 在 Vue.options 上混合了两个组件(components),分别是 Transition 和 TransitionGroup。
- 在 Vue.prototype 上添加了两个方法:
__patch__和$mount。
添加compiler
entry-runtime-with-compiler.js文件不仅包括运行时版本的Vue构造函数,还包括compiler。
// ... 其他 import 语句
// 导入 运行时 的 Vue
import Vue from './runtime/index'
// ... 其他 import 语句
// 从 ./compiler/index.js 文件导入 compileToFunctions
import { compileToFunctions } from './compiler/index'
// 根据 id 获取元素的 innerHTML
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 使用 mount 变量缓存 Vue.prototype.$mount 方法
const mount = Vue.prototype.$mount
// 重写 Vue.prototype.$mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ... 函数体省略
}
/**
* 获取元素的 outerHTML
*/
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
// 在 Vue 上添加一个全局API `Vue.compile` 其值为上面导入进来的 compileToFunctions
Vue.compile = compileToFunctions
// 导出 Vue
export default Vue
这个文件运行下来,对Vue的影响有两个。
- 重写了Vue.prototype.$mount 方法
- 添加了Vue.compile全局API
Vue生命周期
-
钩子函数在什么时候被调用
-
beforeCreate
在实例初始化new Vue()之后,数据观测(data observer)响应式处理之前被调用
-
created
实例已经创建完成之后被调用,实例已完成以下的配置:数据观测(data observer)、属性和方法的运算、watch/event事件回调。数据可以拿到,但是没有$el。
-
beforeMount
在挂载开始之前被调用:相关的render函数首次被调用。
-
mounted
el被新创建的vm.$el替换,并挂载到实例上去之后被调用。页面渲染完毕
-
beforeUpdate
数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
-
updated
由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。
-
beforeDestroy
实例销毁之前调用,在这一步,实例仍然完全可用。
-
destroyed
Vue实例销毁后调用。调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
-
-
生命钩子中可以做什么事
-
created
实例已经创建完成,因为它时最早触发的,可以进行一些数据资源的请求。
-
mounted
实例已经挂载完成,可以进行一些DOM操作。
-
beforeUpdate
可以在这个钩子中进一步地更改状态,不会触发附加的重渲染过程。
-
updated
可以执行依赖于DOM的操作,尽量避免在此期间更改状态,因为可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
-
beforeDestory
可以执行一些优化操作,如清空定时器、解绑事件的原生绑定。如果在当前实例上使用了$on方法,需要在组件销毁之前进行解绑。
-
思考
ajax请求放在哪个生命周期中?
在created的时候,视图中的DOM并没有渲染出来,此时直接去操作DOM节点,无法找到相关元素。 在mounted中,此时DOM已经渲染出来,可以直接操作DOM节点。 一般情况下都放到mounted中,保证逻辑的统一性,因为生命周期是同步执行的,ajax是异步执行的。 服务器端渲染因为没有DOM,不支持mounted方法,所以在服务器端渲染的情况下统一放到created中。
vue父子组件生命周期调用顺序
加载渲染过程
父beforeCreate ==> 父created ==> 父beforeMount ==> 子beforeCreat ==>子created ==> 子beforeMount ==> 子mounted ==> 父mounted
子组件更新过程
父beforeUpdate ==> 子beforeUpdate ==> 子updated ==> 父updated
父组件更新过程
父beforeUpdate ==> 父updated
销毁过程
父beforeDestroy ==> 子beforeDestroy ==> 子destroyed ==> 父destroyed
理解
组件的调用顺序都是先父后子,渲染完成的顺序是先子后父
组件的销毁操作是先父后子,销毁完成的顺序是先子后父
原理图
谈谈 Vue 事件机制,手写$on,$off,$emit,$once
Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
class Vue {
constructor() {
// 事件通道调度中心
this._events = Object.create(null);
}
$on(event, fn) {
if (Array.isArray(event)) {
event.map(item => {
this.$on(item, fn);
});
} else {
(this._events[event] || (this._events[event] = [])).push(fn);
}
return this;
}
$once(event, fn) {
function on() {
this.$off(event, on);
fn.apply(this, arguments);
}
on.fn = fn;
this.$on(event, on);
return this;
}
$off(event, fn) {
if (!arguments.length) {
this._events = Object.create(null);
return this;
}
if (Array.isArray(event)) {
event.map(item => {
this.$off(item, fn);
});
return this;
}
const cbs = this._events[event];
if (!cbs) {
return this;
}
if (!fn) {
this._events[event] = null;
return this;
}
let cb;
let i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break;
}
}
return this;
}
$emit(event) {
let cbs = this._events[event];
if (cbs) {
const args = [].slice.call(arguments, 1);
cbs.map(item => {
args ? item.apply(this, args) : item.call(this);
});
}
return this;
}
}
Vue 组件 data 为什么必须是函数?
问:new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件可以复用,如果data是一个对象,对象属于引用类型,会导致子组件中的data属性值会相互污染。如果是一个函数,每个实例就可以维护一份被返回对象的独立拷贝。
new Vue()的实例并不会被复用,所以data可以是一个对象。
keep-alive 的实现原理
<keep-alive>是一个抽象组件,自身不会渲染DOM元素,也不会出现在父组件链中。包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。作用是保存组件的渲染状态。
源码
export default {
// 组件名
name: "keep-alive",
// 判断当前组件是否渲染成真实DOM的关键
abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
props: {
include: patternTypes, // 被缓存组件
exclude: patternTypes, // 不被缓存组件
max: [String, Number] // 指定缓存大小
},
created() {
this.cache = Object.create(null); // 缓存的虚拟DOM
this.keys = []; // 缓存的VNode的键
},
destroyed() {
for (const key in this.cache) {
// 删除所有缓存
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
// 监听缓存/不缓存组件
this.$watch("include", val => {
pruneCache(this, name => matches(val, name));
});
this.$watch("exclude", val => {
pruneCache(this, name => !matches(val, name));
});
},
render() {
// 获取第一个子元素的 vnode
const slot = this.$slots.default;
const vnode: VNode = getFirstComponentChild(slot);
const componentOptions: ?VNodeComponentOptions =
vnode && vnode.componentOptions;
if (componentOptions) {
// name不在inlcude中或者在exlude中 直接返回vnode
// check pattern
const name: ?string = getComponentName(componentOptions);
const { include, exclude } = this;
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
// 获取键,优先获取组件的name字段,否则是组件的tag
const key: ?string =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
// 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
}
// 不命中缓存,把 vnode 设置进缓存
else {
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// keepAlive标记位
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
}
};
// src/core/components/keep-alive.js
// 删除缓存VNode还要对应执行组件实例的destory钩子函数。
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy() // 执行组件的destory钩子函数
}
cache[key] = null
remove(keys, key)
}
实现原理
- 获取 keep-alive 包裹着的第一个子组件对象及其组件名。
- 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例。
- 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
- 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
- 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,主要作用是使组件实例不再进入$mount过程,那mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。
LRU 缓存淘汰算法
LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。所以当需要存储的数量超过可以存储的最大值时,会将最久没被访问过的给剔除,再增加新数据。
keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]
组件之间是如何进行通信的
-
父子通信
-
父向子传递数据是通过 props
-
子向父是通过 events(
$emit)v-on:event -
通过父链 / 子链也可以通信(
$parent/ $children) -
ref 也可以访问组件实例;
-
provide / inject API;
-
$attrs/$listeners
-
-
兄弟通信
- Bus;
- Vuex
-
跨级通信
- Bus;
- Vuex;
- provide / inject API、
$attrs/$listeners
vuex原理 vuex实现了一个单向数据流,在全局拥有一个state存放数据,当组件要更改state中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取state数据的更新。当所有异步操作(常见于调用后端接口获取数据)或批量的同步任务(耗时长)就需要在Action中定义方法,但Action中的方法是无法修改state的,还是需要通过Mutation来修改state中的数据。最后,根据state中数据的变化,更新渲染到视图上。
参考阅读: segmentfault.com/a/119000001…
slot是什么?有什么作用?原理是什么?
插槽slot可以理解为在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填充(替换组件模板中slot位置),作为承载分发内容的出口,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理。
slot又分三类,默认插槽,具名插槽和作用域插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
template和jsx的有什么分别?
template和jsx的都是render函数的一种表现形式,不同的是:
JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
SSR是什么?有什么好处?
在客户端请求服务器的时候,服务器到数据库中获取到相关的数据,并且在服务器内部将Vue组件渲染成HTML,并且将数据、HTML一并返回给客户端,这个在服务器将数据和组件转化为HTML的过程,叫做服务端渲染SSR。
而当客户端拿到服务器渲染的HTML和数据之后,由于数据已经有了,客户端不需要再一次请求数据,而只需要将数据同步到组件或者Vuex内部即可。除了数据以外,HTML也结构已经有了,客户端在渲染组件的时候,只需要将HTML的DOM节点映射到Virtual DOM即可,不需要重新创建DOM节点,这个将数据和HTML同步的过程,又叫做客户端激活。
使用SSR的好处:
-
有利于SEO
其实就是有利于爬虫来爬你的页面,因为部分页面爬虫是不支持执行JavaScript的,这种不支持执行JavaScript的爬虫抓取到的非SSR的页面会是一个空的HTML页面,而有了SSR以后,这些爬虫就可以获取到完整的HTML结构的数据,进而收录到搜索引擎中。
-
白屏时间更短
相对于客户端渲染,服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。
使用js实现一个只读属性
我们可以参照vue中的实现方式。
$data和$props是两个vue中十分重要的属性,以下代码把它们设置为只读。
// $data 属性实际上代理的是 _data 这个实例属性
const dataDef = {}
dataDef.get = function () { return this._data }
// $props 代理的是 _props 这个实例属性。
const propsDef = {}
propsDef.get = function () { return this._props }
// 在生产环境下,为 $data 和 $props 这两个属性设置一下 set,当触发set时抛出警告,说明这两个属性只读
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function (newData: Object) {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}