1.vue插件机制及其原理
一、通常用来为Vue添加全局功能
- 添加全局方法或者属性。如vue-custom-element
- 添加全局资源:指令、过滤器、过度等。如vue-touch
- 通过全局混入来添加一些组件选项。如vue-router
- 添加Vue实例方法,通过把他们添加到Vue.prototype上实现
- 一个库,提供自己的API。如vue-router
二、使用插件方法
通过全局方法Vue.use()使用方法,它需要你调用new Vue()时启动应用之前完成。
三、插件原理
本质上插件就是一个对象,在对象里面调用 install 方法
- Vue.js的插件应该暴露一个 install 方法,这个方法的第一个参数是Vue构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function(Vue, options) {
// 1 、添加全局方法或属性
Vue.myGLobalMethod = function(){ };
// 2、添加全局资源
Vue.directive('my-directive', {
bind(el, binding, vnode, oldVnode) { }
});
// 3、注入组件选项
Vue.mixin({
created: function(){ }
});
// 4、添加实例方法
Vue.prototype.$myMethod = function(methodOptions) { }
}
1.vue插件机制及其原理
在vue中,当我们对某项数据进行频繁的更新时会有很严重的性能问题。比如我们对上述的num属性进行修改:
for(let i=0;i<100;i++){
data.num=i;//每次的data数据的变化都会调用Watcher的update去更新DOM
}
上面的代码会导致num对应的Watcher的回调频繁执行(100次),其对应的就是100次的DOM更新,我们知道,DOM更新的性能成本是昂贵的,我们开发中应当尽量减少Dom操作。
so
优秀Vue作者肯定也是不允许这种情况发生的,vue就是使用nextTick来优化这个问题的。
简单的说就是每次数据变化之后不是立刻去执行DOM更新,而是要把数据变化的动作缓存起来,在合适的时机只执行一次的dom更新操作。这里就需要要设置一个合适的时间间隔,通过下面要介绍的事件循环机制可以很完美的解决。
我们的数据变化缓存可以依赖事件循环eventloop来完成;因为每次事件循环之间都有一次视图渲染,我们只需要在render之前完成对dom的更新即可,因此我们为了避免无效的DOM操作,需要将数据变更缓存起来,只保存最后一次数据最终的变更结果。
这里简单给出两种实现方法:setTimeout和Promise,我们常用的setTimeout会创建一个宏任务,而Promise.then创建一个微任务。
如果使用setTimeout宏任务实现异步更新队列,那么就是本次同步代码执行完成不执行视图更新,而是在下一次宏任务开始清空异步更新队列,处理缓存的DOM更新和开发者添加的nextTick回调。
使用Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。
so,在vue中我们可以
由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick()方法。
Watcher新updata函数
update(newValue){
//值发生变化才变更
if(this.value!==newValue){
this.value=newValue;
//在异步更新队列中添加Watcher,用于后续更新
updateQueue.push(this);
}
}
//执行DOM更新等操作
run(){
this.cb(this.value);
}
现在我们有了一个处理更新队列的函数,但是现在还缺少一个很重要的元素,就是执行此函数的时机,这时我们回忆一下我们的更新队列是异步更新队列,这里的异步即使用setTimeout或者Promise实现异步更新,这个实现过程就是nextTick的代码实现了,下面是简化版nextTick函数
let callbacks=[];//事件队列,包含异步dom更新队列和用户添加的异步事件
let pending=false;//控制变量,每次宏任务期间执行一次flushCallbacks清空callbacks
funciton nextTick(cb){
callbacks.push(cb);
if(!pending){
pending=true;
//这里也可以使用Promise,Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。
if(Promise){
Promise.resovle().then(()=>{
flushCallbacks();
})
}
setTimeout(()=>{
flushCallbacks();
})
}
}
function flushCallbacks(){
pending=false;//状态重置
callbacks.forEach(cb=>{
callbacks.shift()();
})
}
3.vue computed设计原理
在initState的时候会初始化initComputed,默认初始化计算属性initComputed的时候会获取用户定义的方法,内部会创建一个watcher(new Watcher),将用户定义的方法传入,这个watcher有一个computedWatcherOptions标识lazy:true,默认创建出来watcher并不会去执行,watcher内部有个属性叫dirty,如果是计算属性默认dirty就是true。watcher的第二次传入的是一个getter用户定义的属性,在watcher对传入的属性方法进行判断,如果是function就把它存到一个getter上,当lazy是true就undefind的什么也不做,false的话就执行this.get()进行依赖追加表示不是计算属性是watch。
在state文件中有一个defineComputed方法,用来定义计算属性,它的底层也使用的是Object.defineProperty,在这个方法中又定义了一个createComputedGetter方法用来创建getter,在取值的时候会执行createComputedGetter方法返回一个computedGetter函数来,里面会对计算属性的watcher中dirty进行判断,当为true时,watcher才会去执行计算属性进行求值(watcher.evaluate),在evaluate中会执行get方法,get方法就是让当前的getter执行,在求值的过程中进行依赖收集。在取值之前pushTarget(this)就是将watcher放到全局,进行依赖收集,会把当前的计算属性的watcher收集起来,等数据发生变化,就会触发watcher的update执行,在watcher的update中,如果lazy为true是计算属性,就会将dirty改为true在取值时重新进行求值,在求值完成后就会把dirty改为false,如果在取值时dirty为false就会直接将值返回。
4.scoped原理
为组件实例生成一个唯一标识,给组件中的每个标签对应的dom元素添加一个标签属性,data-v-xxxx 给中的每个选择器的最后一个选择器添加一个属性选择器,原选择器[data-v-xxxx],如:原选择器为.container #id div,则更改后选择器为.container #id div[data-v-xxxx]
5.vue-router原理
1.hash模式,主要是hashHistory:hash(“#”)符号的本来作用是加在URL中指示网页中的位置,hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面,每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录,改变hash可以监听到hashchange事件:
window.addEventListener(“hashchange”, funcRef, false) 底层处理路由更新(push)的流程: 1)push方法 对window的hash直接赋值 window.location.hash = route.fullPath 2)router是怎么做到在每一个Vue组件中都能使用的呢? 在插件加载的地方,即VueRouter的install方法中通过Vue.mixin()全局注册一个混合,影响到每一个组件。
export function install (Vue) {
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
registerInstance(this, this)
},
})
}
看代码我们知道Vue.mixin()在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的route属性,所谓响应式其实就是,当route值改变时,调用Vue的render方法,更新视图
2.history模式,主要利用HTML5History 我们主要利用这个HTML5History来操作浏览器历史记录栈,主要方法有back(), forward(), go()来读取浏览器路由历史并控制跳转,HTML5新增pushState(), replaceState()2个方法来修改历史信息,调用这两个方法修改历史信息后,虽然当前URL改变了,但浏览器不会立即发送请求该URL,这就满足单页面应用”更新视图但不重新请求页面“的需求,修改浏览器历史记录后会触发popstate事件,我们可以通过监听popstate事件
5.keep-alive原理
1.用法:我们想要缓存某个组件,只要用<[keep-alive]>组件将其包裹就行。常用的用法是包裹组件缓存动态组件,或者包裹缓存路由页面。 2.组件可以接收三个属性:
include- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max- 数字。最多可以缓存多少组件实例。
3.原理
export default {
name: 'keep-alive',
abstract: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
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() {
/* 获取默认插槽中的第一个组件节点 */
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)
const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key = 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
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
它有created,destroyed,mounted,render四个钩子。接下来我们就说说这四个钩子分别干了什么,最后再总结下整体流程。
created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。
destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。
render钩子作用:【
-
先获取到插槽里的内容
-
调用getFirstComponentChild方法获取第一个子组件,获取到该组件的name,如果有name属性就用name,没有就用tag名。
-
用获取到的name和传入的include,exclude属性进行匹配,如果匹配不成功,则表示不缓存该组件,直接返回这个组件的
vnode,否则的话走下一步缓存: -
缓存机制:用拿到的name去
this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存: 命中缓存时会直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个。
如果没有命中缓存,即该组件还没被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中。此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉。
】
mounted钩子
在这个钩子函数里,调用了pruneCache方法,以观测 include 和 exclude 的变化。
** 6.vue编译原理**
第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
7.vuex`的核心原理
vuex`的核心原理,通过全局混入`beforeCreate`,将`store`实例注入到每个`Vue`组件中,因此每个组件都能通过`this.$store`来获取到唯一的`store`实例。通过借助`vue`的`data`和`computed`来实现`state`的响应式和`getters`缓存特性。严格模式通过`vue`的`watch`来监听,如果`state`的修改不是通过`commit`方法则会抛出警告。`action`方法都会包裹一层`promise`,所以`action`返回的是`promise`。这也就是为什么说要在`mutation`里面处理同步,在`action`里面处理异步
** 8.vue的render函数**
在vue中我们使用模板HTML语法来组建页面的,使用render函数我们可以用js语言来构建DOM。因为vue是虚拟DOM,所以在拿到template模板时也要转译成VNode的函数,而用render函数构建DOM,vue就免去了转译的过程。
当使用render函数描述虚拟DOM时,vue提供一个函数,这个函数是就构建虚拟DOM所需要的工具。官网上给他起了个名字叫createElement。还有约定它的简写叫h
render 函数即渲染函数,它是个函数,render 函数的返回值是VNode(即:虚拟节点,也就是我们要渲染的节点)
createElement 是 render 函数的参数,它本身也是个函数,并且有三个参数。接来下我们重点介绍这三个参数
-
String,表示的是HTML 标签名
-
Object ,一个含有数据的组件选项对象
-
Function ,返回了一个含有标签名或者组件选项对象的async 函数