更多文章
前言
有段时间没用vue了,但这并不能减少我对vue的热爱,整理一下vue的知识,查漏补缺,内附原理、代码,篇幅可能过长
想换工作了,杭州有木有小伙伴推荐的
Vue、React异同
相同:
- 使用了虚拟DOM
- 提供响应式和组件化的视图组件
- 将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库
- 提供生命周期钩子函数,开发者按需定制需求
差异:
Vue
本质是MVVM框架,React
是前端组件化框架Vue
推崇模板,但也支持JSX
语法,React
支持JSX
语法Vue
提供了指令、过滤器等,方便操作,React
没有Vue
支持数据双向绑定Vue
会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。React
在应用的状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate
这个生命周期方法可以进行控制,但Vue
将此视为默认的优化- CSS作用域在
React
中是通过CSS-in-JS的方案实现的,Vue
通过scoped Vue
中子组件向父组件传递消息有两种方式:事件和回调函数,Vue
更倾向于使用事件。在React
中我们都是使用回调函数的
MVVM是什么?和MVC区别?
- MVC
MVC
是Model-View-Controller
缩写,Model
即数据模型,负责数据库中数据的存取,View
即视图,也就是页面上看到的,MVC
的思想就是既不能在Model
里面写View
相关代码,也不能在View
里面写Model
相关代码,所以将同步Model
和View
的工作就交给了Controller
,在数据简单的年代Controller
负担起数据解析的工作毫无压力,但随着数据结构变得越来越复杂,Controller
就变得非常臃肿,接下来引出了MVVM
,将数据解析的工作交给了VM
- MVVM
MVVM
是Model-View-ViewModle
缩写,最早由微软提出
Model
代表数据层,View
代表视图层,负责展示数据,ViewModle
则负责同步Model
和View
之间的关联,其实现同步关联的核心是DOMListeners
和 DataBindings
两个工具,DOMListeners
工具用于监听 View
中 DOM
的变化,并会选择性的传给 Model
;DataBindings
工具用于监听 Model
数据变化,并将其更新给 View
MVVM``设计思想就是将复杂的``DOM
中解脱出来,开发者从如何操作DOM
变成了如何更新JS对象状态
想要更详细的了解请看mvc和mvvm的区别
生命周期
- 创建阶段
- BeforeCreate(组件创建前)
实例初始化后,数据监测(data observer)、watch/event事件配置调用之前,此阶段$el和data为undefined
- Created(组件创建后)
数据观测、事件配置已完成。data对象可访问,但$el还无法访问,ref为undefined,常用来做数据初始化、ajax等操作
- 挂载阶段
- BeforeMount(组件挂载前)
该函数在组件挂载前调用,此时 HTML 模板编译已完成,虚拟 DOM 已存在,$el 为可用状态,但 ref 仍不可用
- Mounted(组件挂载后)
组件挂载完成,数据渲染完成。DOM可以访问,此阶段只执行一次
- 更新阶段
- BeforeUpdate(更新前)
数据更新、虚拟 DOM 打补丁前调用,此阶段可以访问到当前现有DOM
- Updated(更新后)
数据更新、虚拟 DOM 打补丁后调用
- 卸载阶段
- BeforeDestory(卸载前)
在实例销毁前调用,可通过this获取实例,ref 仍然存在,此阶段通常用来清除定时器和绑定的监听事件
- Destroyed(卸载后)
实例被销毁,所有指令均被解绑,所有事件监听器移除
- keep-alive钩子函数
- activated
在被keep-alive缓存的组件激活时调用
- deactivate
在被keep-alive缓存的组件停用时调用。
子父组件通信
- 父组件->子组件
- props
- provide/inject
// provide向所有后代组件注入依赖
// provide 和 inject 绑定并不是可响应的
// 如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的
// parent
export default {
data() {
return {
obj: {
a: 1
},
sex: "女"
}
},
provide(){
return {
obj: this.obj, // obj响应
sex: this.sex // sex非响应
}
}
}
// child
export default {
inject: ['obj', 'sex']
}
- $attrs
// 接收所有父组件除class和style且不作为prop识别的attribute绑定
// parent
<template>
<Child :sex="'女'" :name="'老王'" :style="{}" />
</template>
// Child
export default {
created() {
console.log(this.$attrs) // { sex: "女" }
}
}
- $listeners
// 包含了父作用域中的 (不含 .native 修饰器的)v-on事件监听器
// parent
<template>
<Child1 @click1="click1" @click2="click2" @click3="click3" />
</template>
// child1
<template>
<Child2 v-on="$listeners" /> // 向下传递Child1绑定的事件
</template>
export default {
created() {
console.log(this.$listeners) // { click1: f, click2: f, click3: f }
},
methods: {
click1() {
this.$emit('click1)
}
}
}
// child2
export default {
created() {
console.log(this.$listeners) // { click1: f, click2: f, click3: f }
},
methods: {
click2() {
this.$emit("click2")
},
click3() {
this.$emit("click3")
}
}
}
- 子组件->父组件
- 回调函数
- $emit
- 兄弟组件
- 状态提升到父组件
- 通用
- vuex
- bus事件总线
// main.js
Vue.prototype.$bus = new Vue()
// 发出
this.$bus.$emit('test', 666)
// 接收
this.$bus.$on('test', (e)=> console.log(e)) // 666
双向绑定原理
Vue
采用的是数据劫持 + 订阅者发布者模式,数据劫持是通过Object.defineProperty
为组件data
每个属性添加get
和set
方法,在数据发生变化时触发set
中的监听回调,通知给订阅者
第一步:递归遍历数据(data),通过Object.defineProperty为属性添加getter和setter
Observer.prototype = {
walk: function(data) {
var self = this;
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val); // 对象递归
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() { },
set: function(newVal) { }
});
}
}
function observe(value, vm) {
if (!value || typeof value !== 'object') return
return new Observer(value);
};
第二步:dep作为观察者(收集依赖,发布消息)会在调用getter时注册函数,在调用setter时通知执行注册的函数
Object.defineProperty(data, key, {
get: function() {
if (Dep.target) {
dep.addSub(Dep.target); // 注册函数,收集依赖
}
return val;
},
set: function(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 触发update更新,发布消息
}
});
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // watcher提供update方法
});
}
};
第三步:Compile解析模板指令,将其中的变量替换成数据。然后初始化渲染页面视图,并将每个指令对应的节点绑定上更新函数、监听函数。后续一旦数据发生变化,便会更新页面。页面发生变化时也会相应发布变动信息
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
console.log('Dom元素不存在');
}
},
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
compileElement: function (el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent;
if (self.isElementNode(node)) {
// 节点
self.compile(node);
} else if (self.isTextNode(node) && reg.test(text)) {
// 是否是符合这种形式{{}}的指令,解析模板
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
// 递归子节点
self.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes;
var self = this;
Array.prototype.forEach.call(nodeAttrs, function(attr) {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
if (self.isEventDirective(dir)) {
// 事件指令
self.compileEvent(node, self.vm, exp, dir);
} else {
// v-model 指令
self.compileModel(node, self.vm, exp, dir);
}
node.removeAttribute(attrName);
}
});
},
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp];
// 将初始化的数据初始化到视图中
this.updateText(node, initText);
// 生成订阅器并绑定更新函数,Watcher中updaue会触发该事件
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);
});
},
compileEvent: function (node, vm, exp, dir) {
var eventType = dir.split(':')[1];
var cb = vm.methods && vm.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false);
}
},
compileModel: function (node, vm, exp, dir) {
var self = this;
var val = this.vm[exp];
this.modelUpdater(node, val);
new Watcher(this.vm, exp, function (value) {
self.modelUpdater(node, value);
});
// 监听input事件,视图变化更新data数据
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[exp] = newValue;
val = newValue;
});
},
updateText: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
},
isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir) {
return dir.indexOf('on:') === 0;
},
isElementNode: function (node) {
return node.nodeType == 1;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
}
第四步:Watcher订阅者作为Observer和Compile之间的桥梁,实例化时将自己添加到dep中,提供一个update方法待数据变化时触发Compile中的相应函数完成更新
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器dep
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 触发Compile中的相应更新视图函数
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
computed、watch差异
- Computed:
- 计算属性
- 支持缓存,只有依赖的数据发生变化时才会重新计算
- 不支持异步操作
- 自动监听依赖值的变化,从而动态返回内容
通常一个值依赖于其他多个值计算而来的话我们会使用Computed,Computed默认具有getter,setter则需要手动写,如下:
computed: {
num: {
get() {
return this.num1 + this.num2
},
set(e) {
console.log('do somethings')
}
}
}
- Watch
- 监听
- 不支持缓存
- 可进行异步操作
- 监听是一个过程,在监听的值变化时,可以触发一个回调
Watch如果需要在组件加载立即出发需要将immediate设置为ture,如果需要监听深层次的属性需要将deep设置为true
需要注意的是一旦添加deep则所有属性将被监听,非常的耗性能,所以通常会优化一下只监听需要被监听的属性,如:obj.a,例外一个问题:不是添加了deep所有的操作都会被监听到,如对象新增属性obj.newAttr = 1、数组arr[0] = 1这样的操作,只有那些响应式的方式才能被监听到,如push、pop等
watch: {
obj: {
handler: function(newVal, oldVal) {
console.log(newVal, oldVal)
},
immediate: true,
deep: true
}
}
异步队列、$nextTick
Vue
中数据发生变化时,并不会立即更新DOM
,通常会使用一个for循环去测试,这样做是为了避免不必要的计算和DOM操作。
每个Watcher都有一个唯一id,当同一个Watcher被多次被触发,会根据id判断是否存在,避免重复推入队列,这是Vue做的优化,去重之后的队列中的Watcher则是需要被执行的,但这个队列是不会被立即执行的,会在下一个事件循环tick执行也就是我们说的nextTick,所以我们先看一下Vue中nextTick的实现,了解了nextTick,其实异步队列更新也就差不多了
// flushCallbacks,执行所有的任务
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 为了兼容,使用顺序是Promise->MutationObserver->setImmediate->setTimeout
// 将任务放到微任务或者宏任务中执行,所有flushCallbacks中同步任务执行完毕执行异步任务
// timerFunc
let timerFunc;
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() ===
"[object MutationObserverConstructor]")
) {
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (
typeof setImmediate !== "undefined" &&
isNative(setImmediate)
) {
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// nextTick
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 收集任务,等待执行
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // 防止后续的 nextTick 重复执行 timerFunc
pending = true
// 执行任务
timerFunc()
}
}
ok,接下来是异步任务队里
// update更新
update () {
// 各种情况的判断 ...
else {
queueWatcher(this) // this 为当前的实例 watcher
}
}
// queueWatcher
const queue = []
let has = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 判断id是否存在,去重操作
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
// 下一个tick执行任务,flushSchedulerQueue在nextTick中会添加到callbacks中
nextTick(flushSchedulerQueue)
}
}
}
// flushSchedulerQueue
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run() // 执行更新任务
}
}
可以看到不仅是我们日常开发会使用nextTick,对于Vue来说也是它的Api,要理解这些你要对js执行机制有一些了解
v-for中key的作用
为什么使用key呢?很简单,就是因为Vue追踪DOM的变化依赖一个标识(针对的是动态DOM),对于一个DOM的是新生的还是变换了位置,没有一个唯一标识去追踪,神仙也不可能知道这些变化,说白了就是给DOM一个身份,因为DOM中一样的标签如:div、p等他们看起来就像是克隆人,你无法识别,为啥要给个身份?其实是还是为了优化,优化的是diff算法,添加key和不添加key计算出来的补丁是完全不一样的,我把之前一篇文章的例子拿过来看一下(了解更多请看Virtual Dom 及 Diff 算法初探):
const oldData = createElement('ul', { key: 'ul' }, [ // 标识 ul: 0
createElement('li', {}, ['aaa']), // li: 1 aaa: 2
createElement('li', { }, ['bbb']), // li: 3 bbb: 4
createElement('li', { }, ['ccc']) // li: 5 ccc: 6
])
const newData = createElement('ul', { key: 'ul' }, [
createElement('li', {}, ['aaa']),
createElement('li', { }, ['aaa']),
createElement('li', { }, ['bbb']),
createElement('li', { }, ['ccc'])
])
const patches = diff(oldData, newData)
console.log('patches----------', patches)
根据patches
结果,我们可以看出相应标识节点发生的变化,再来看一组数据
const oldData = createElement('ul', { key: 'ul' }, [
createElement('li', { key: 1 }, ['aaa']),
createElement('li', { key: 2 }, ['bbb']),
createElement('li', { key: 3 }, ['ccc'])
])
const newData = createElement('ul', { key: 'ul' }, [
createElement('li', { key: 1 }, ['aaa']),
createElement('li', { key: 4 }, ['aaa']),
createElement('li', { key: 2 }, ['bbb']),
createElement('li', { key: 3 }, ['ccc'])
])
const patches = diff(oldData, newData)
console.log('patches----------', patches)
可以看出不加key的补丁确实复杂了很多,日常复杂操作中又不知道要复杂多少倍
另外一个问题是for循环中尽量避免自带的index索引,因为一个元素变化会导致后续所有DOM表示发生了变化,导致补丁变复杂,更新效率也变差
v-if、v-show差异
优化是无处不在的,分清v-if
、v-show
也是为了优化
- v-if
真正
的条件渲染,对DOM销毁或重建,耗性能,切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
另外v-for
具有比v-if
更高的优先级,同时使用也会造成性能问题,通常用computed
或v-show
解决
- v-show
简单的css切换(display),开销小,频繁切换常用
Vue组件中data必须是函数
其实这也是个js的基础问题,因为同一对象被复用地址同一个会互相影响,也即是一个组件被多次引用这个数据就会乱掉,所以用函数返回一个新对象
Vue初始化页面闪动问题
<!-- css -->
[v-cloak] { display: none; }
<!-- html -->
<div v-cloak>
{{ message }}
</div>
Vue、React路由模式
前端路由主要就是hash路由和history路由,vue-router
和react-router
亦是如此
- hash路由
hash路由的url是带#号的,通过监听hashchange
事件改变页面内容
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<div id="app"></div>
const app = document.getElementById('app');
window.addEventListener('onload',hashChange() )
window.addEventListener('hashchange', () => hashChange())
function hashChange() {
switch (window.location.hash) {
case '#/page1':
app.innerHTML = 'page1'
return
case '#/page2':
app.innerHTML = 'page2'
return
default:
app.innerHTML = 'page1'
return
}
}
- history
history新增了pushState
和replaceState
(pushState是将传入url压入历史记录栈,replaceState将传入url替换当前历史记录栈),在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求
<a href="/page1">page1</a>
<a href="/page2">page2</a>
<div id="app"></div>
window.addEventListener('DOMContentLoaded', Load)
window.addEventListener('popstate', PopChange)
var routeView = null
function Load() {
routeView = document.getElementById('app')
// 默认执行一次 popstate 的回调函数
PopChange()
var aList = document.querySelectorAll('a[href]')
aList.forEach(aNode => aNode.addEventListener('click', function (e) {
e.preventDefault()
var href = aNode.getAttribute('href')
history.pushState(null, '', href)
// popstate 是监听不到地址栏的变化,所以此处需要手动执行回调函数 PopChange
PopChange()
}))
}
function PopChange() {
switch (window.location.pathname) {
case '/page1':
routeView.innerHTML = 'page1'
return
case '/page2':
routeView.innerHTML = 'page2'
return
default:
routeView.innerHTML = 'page1'
return
}
}
history模式需要后台进行配置,当url无法匹配到静态资源时,返回同一个index.html,以nginx为例:
location / {
try_files $uri $uri/ /index.html;
}
后台配置
vue-router路由钩子函数
通常会将路由钩子函数称之为路由守卫,守卫就是"看大门的",谁要进来就要看这个"看大门的"让不让你进来了,如果你身份符合条件就会让你进来,也就是我们经常会做的一件事:身份验证,"大门"也要分很多种的,主大门,屋内小门等等,所以路由钩子自然也会做区分:
- 全局钩子
这是主大门的"看门的",这里如果过不去,你怕是哪都去不了
// 常用钩子:跳转前校验
router.beforeEach((to, from, next) => {
// ...
})
- 独享钩子
深宫大院总有一些特殊身份的人,他特殊啊,所以他自然也独享一些"看门的"
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => { }
}
]
})
- 各组件钩子
各屋没那么特殊,但也会有个门,想进来也要看有没有"看门的"、"看门的"心情如何
export default {
data() {},
beforeRouteEnter(to, from, next) {}, // 进门前你是不知道主人是谁的,所以此阶段this为undefined
beforeRouteLeave (to, from, next) {}
}
Vue如何动态路由
- router提供的动态路由
无论Vue
还是React
都会提供/:id
方式动态匹配,通常这种方式会用来实现匹配某一类相同结构的页面
- 动态权限路由
通常需要做权限控制就需要动态路由,有两种做法:
-
前端有一份全量的路由数据,然后根据请求后台的权限数据处理这份全量的路由的数据,将不符合条件的过滤掉不显示在菜单栏或页面其它地方,页面跳转的时候也通过路由守卫判断是否有权限,无权限则提示无权限
-
将前端路由分为静态和动态两份,静态的如登录页、404等可以直接写到路由中,而动态部分由后端提供,前端拿到后通过
router.addRoutes
添加到前端路由中,这里会问一个问题是如果退出登录时如果直接使用push
的话再次登录会遇到路由重复添加的问题,处理这个有两种方式:
// 一、简单粗暴的使用window.location.href
window.location.href = '/login'
// 二、每次添加路由前清空路由信息
// router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const createRouter = () => new Router({
mode: 'history',
routes: []
})
const router = createRouter()
export function resetRouter () {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
export default router
// 后续调用addRoutes前先调用resetRouter
import {resetRouter} from '@/router'
resetRouter()
router.addRoutes(routes)
keep-alive
缓存不活动的组件实例,而不是销毁它们,常见的例子就是tabs
来回切换时个tab
状态没有被初始化
// include:值可为字符串、正则
// 缓存组件名为a、b的组件,其余都不缓存
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
// exclude值和include相同,意思相反,匹配到的都不缓存
// max 最多缓存组件数量
// 配合meta
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
以上是keep-alive
的使用,接下来一起了解一下keep-alive
的实现,keep-alive
本身也是个组件,它是Vue
内部实现的一个组件,但它不会被渲染,keep-alive
内部有一个属性{abstract: true}
,abstract
为true
时会被忽略掉,初次渲染的时候keep-alive
组件内部会将不满足条件的则直接返回vnode
,满足缓存条件的组件通过对象cache
将vnode
缓存起来,已经缓存过的直接将缓存的vnode
的componentInstance
(组件实例)覆盖到目前的vnode
上面,下面是代码
// keep-alive.js
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null) // 存储缓存
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys) // 通过pruneCacheEntry清除缓存
}
},
// 观测缓存变化通过pruneCache更新缓存
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: VNode = getFirstComponentChild(slot) // 拿到第一个元素
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 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 // 无法命中缓存返回vnode
}
const { cache, keys } = this
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
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])
}
}
// pruneCache
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
// 无法匹配则清除缓存
if (name && !filter(name)) {
if (cachedNode !== current) {
pruneCacheEntry(cachedNode)
}
cache[key] = null
}
}
}
}
// pruneCacheEntry
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) {
vnode.componentInstance.$destroy()
}
}
第一次渲染时和普通渲染没有什么区别,但是后续渲染时,会将缓存的DOM插入父元素中
// patch 过程会执行 createComponent
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 首次渲染vnode.componentInstance为undefined,后续渲染 vnode.componentInstance === cache[key].componentInstance
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 这部将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
结语
持续更新...(感觉有些模块可以抽出来细究)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!