关于vue的阅读与总结,这是一份深入思考后的关于vue的理解。触类旁通,多多学习。
举一个最近自己看到的例子: vue-router
插件中,借用vue.min
可以混入生命周期,在这里混入的生命周期在每个组件的这个生命周期的这个阶段都会调用:
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
复制代码
看到这个实现,对于以后想要实现vue插件并且绑定生命周期,提供了一种很好的思路和方法,往往可以触类旁通,有意想不到的收获。
MVVM 由以下三个内容组成
在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。
在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。
MVVM 到底是什么?与其专注于说明 MVVM 的来历,不如让我们看一个典型的应用是如何构建的,并从那里了解 MVVM:
这是一个典型的 MVC 设置。Model 呈现数据,View 呈现用户界面,而 View Controller 调节它两者之间的交互。
虽然 View 和 View Controller 是技术上不同的组件,但它们几乎总是手牵手在一起,成对的。
在典型的 MVC 应用里,许多逻辑被放在 View Controller 里。它们中的一些确实属于 View Controller
,但更多的是所谓的“表示逻辑(presentation logic)”
。
以 MVVM 属术语来说,就是那些将 Model 数据转换为 View 可以呈现的东西的事情,例如将一个 NSDate 转换为一个格式化过的 NSString
。
我们的图解里缺少某些东西,那些使我们可以把所有表示逻辑放
进去的东西。我们打算将其称为 “View Model”
—— 它位于 View/Controller 与 Model 之间:
这个图解准确地描述了什么是 MVVM:一个 MVC 的增强版,我们正式连接了视图和控制器,并将表示逻辑从 Controller 移出放到一个新的对象里,即 View Model。
v-show和v-if
v-if
: 真正的条件渲染。false,不在dom中。v-show
: 一直在dom中,只是用css的display属性进行切换(存在于html结构中,但是未用css进行渲染)。存在dom结构中display:none
时,不在render(渲染树)树中。visibility:hidden和display:none
display: none
: 标签不会出现在页面上(尽管你仍然可以通过dom与它进行交互)。其它标签不会为它分配空间。 visibility:hidden
: 标签会出现在页面上,只是看不见而已。其它标签会为它分配空间。
如果需要,可以通过将 vm.$data 传入 JSON.parse(JSON.stringify(...)) 得到深拷贝的原始数据对象。
父传子:props 子传父:emit
问题:多级嵌套组件
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
element-ui的button组件, 部分源码
// Button 组件核心源码
export default {
name: 'ElButton',
// 通过 inject 获取 elForm 以及 elFormItem 这两个组件
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
// ...
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
//...
},
// ...
};
复制代码
问题:不能够实现子组件向祖先组件传递数据
$attrs
$listeners
上述的provide
和inject
实现了多层级组件数据的传输,但是不能够实现子组件向祖先组件传递数据
,如果要实现子传祖,可以使用$ attrs和$ listeners
对于一些没有必要引进vuex的项目,可考虑
事件总线:EventBus
可以用来很方便的实现兄弟组件和跨级组件的通信,但是使用不当时也会带来很多问题(vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去
);所以适合逻辑并不复杂的小页面,逻辑复杂时还是建议使用vuex
class EventBus{
constructor(){
// 一个map,用于存储事件与回调之间的对应关系
this.event=Object.create(null);
};
//注册事件
on(name,fn){
if(!this.event[name]){
//一个事件可能有多个监听者
this.event[name]=[];
};
this.event[name].push(fn);
};
//触发事件
emit(name,...args){
//给回调函数传参
this.event[name]&&this.event[name].forEach(fn => {
fn(...args)
});
};
//只被触发一次的事件
once(name,fn){
//在这里同时完成了对该事件的注册、对该事件的触发,并在最后取消该事件。
const cb=(...args)=>{
//触发
fn(...args);
//取消
this.off(name,fn);
};
//监听
this.on(name,cb);
};
//取消事件
off(name,offcb){
if(this.event[name]){
let index=this.event[name].findIndex((fn)=>{
return offcb===fn;
})
this.event[name].splice(index,1);
if(!this.event[name].length){
delete this.event[name];
}
}
}
}
复制代码
状态管理,逻辑复杂时还是建议使用vuex
beforeCreate
、created
beforeCreate
、created
生命周期是在初始化的时候,在_init
中执行
具体代码在vue/src/core/instance/init.js
中
Vue.prototype._init = function() {
// expose real self
//...
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化props,methods,data,computed等
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
复制代码
beforeCreate
. 不能用props,methods,data,computed等。initState
. 初始化props,methods,data,computed等。created
. 此时已经有,props,methods,data,computed等,要用data属性则可以在这里调用。在beforeCreate
、created
这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props、data 等数据的话,就需要使用 created 钩子函数。
beforeMount
、mounted
挂载是指将编译完成的HTML模板挂载到对应虚拟dom
在挂载开始之前被调用:相关的 render 函数首次被调用。
该钩子在服务器端渲染期间不被调用。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
// ...
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ...
vm._update(vnode, hydrating)
// ...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch 到真实 DOM 后,执行 mounted
钩子。
beforeUpdate
、updated
beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// ...
}
复制代码
这里有个细节是_isMounted
, 表示要在mounted
之后才执行beforeUpdate
至于updated
则表示,当这个钩子被调用时, 组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作
beforeDestroy
、destroyed
beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
复制代码
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy
钩子函数。
在 $destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
actived
、deactivated
activated
和 deactivated
钩子函数是专门为 keep-alive 组件定制的钩子
activated
是keep-alive
组件激活时调用。deactivated
是keep-alive
组件销毁时调用。errorCaptured
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
Object.defineProperty
设置 setter
与 getter
函数,用来实现响应式以及依赖收集当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。
每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 逻辑会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。
总结就是: vue.js 采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调
dep.notify()
(遍历subs,调用每个Watcher的update()方法)通知各个Watcher实现observer
// 遍历对象
function observer(target) {
// target是对象,则遍历
if (target && typeof target === 'object') {
Object.keys(target).forEach(key => {
defineReactive(target, key, target[key])
})
}
}
// 用defineProperty监听当前属性
function defineReactive(target, key, val) {
const dep = new Dep()
// 递归
observer(val)
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function() {
return val
},
set: function(value) {
console.log(val, value)
val = value
}
})
}
复制代码
实现dep
订阅者
class Dep {
constructor() {
// 初始化订阅队列
this.subs = []
}
// 增加订阅
addSub(sub) {
this.subs.push(sub)
}
// 通知订阅者
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
复制代码
订阅者Dep里的subs数组是Watcher实例
实现Watcher类
class Watcher {
constructor() {}
update() {
// 更新视图
}
}
复制代码
改写 defineReactive 中的 setter 方法,在监听器里去通知订阅者了:
// 用defineProperty监听当前属性
function defineReactive(target, key, val) {
const dep = new Dep()
// 递归
observer(val)
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function() {
return val
},
set: function(value) {
console.log(val, value)
dep.notify()
}
})
}
复制代码
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者, dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。 其内部通过 this.dirty 属性标记计算属性是否需要重新求值
。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher, computed watcher 通过 this.dep.subs.length 判断有没有订阅者, 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
区别
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存
,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是「观察」的作用,无缓存性
,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
假如我在一个for循环中改变当前组件依赖的数据,改变一万次,会有什么效果?(涉及批量更新和 nextTick 原理
) 整体过程:
JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}}
复制代码
在浏览器环境中 :
常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate
常见的 micro task 有 MutationObsever 和 Promise.then
例题解答:number会被不停地进行
++操作,不断地触发它对应的
Dep中的
Watcher对象的
update方法。然后最终
queue中因为对相同
id的
Watcher对象进行了筛选(过滤),从而
queue中实际上只会存在一个
number对应的
Watcher对象。在下一个 tick 的时候(此时
number已经变成了 1000),触发
Watcher对象的
run方法来更新视图,将视图上的
number` 从 0 直接变成 1000。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout
vue 的 nextTick 方法的实现原理:
computed 和 watch 公用一个 Watcher 类,在 computed 的情况下有一个 deps 。 Vue 在二次收集依赖时用 cleanupDeps 在每次添加完新的订阅,会移除掉旧的订阅
为了解决这个问题, 对数组中所有能改变数组自身的方法,如 push、pop 等这些方法进行重写
。 然后手动调用 notify,通知 render watcher,执行 update
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
复制代码
我们需要对每个对象的每个属性进行遍历
。如果属性值也是对象那么需要深度遍历
, 显然如果能劫持一个完整的对象
是才是更好的选择。Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点
更准确
: 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速
: key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
- parse 函数解析 template,生成 ast(抽象语法树)
- optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
- generate 函数生成 render 函数字符串
调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素。
export default {
name: "keep-alive",
abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
props: {
include: patternTypes, // 被缓存组件
exclude: patternTypes, // 不被缓存组件
max: [String, Number] // 指定缓存大小
},
created() {
this.cache = Object.create(null); // 缓存
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]);
}
};
复制代码
获取 keep-alive 包裹着的第一个子组件对象及其组件名
根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说
LRU (Least Recently Used)缓存策略:从内存中找出最久未使用的数据置换新的数据。
核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
Vue 为什么要用虚拟 DOM (Virtual DOM)
翻开vue源代码的过程,发现里边有写多值得学习的优化过程,这里记录下来:
cache函数,利用闭包实现缓存
二次依赖收集时,cleanupDeps, 剔除上一次存在但本次渲染不存在的依赖
traverse,处理深度监听数据,解除循环引用
编译优化阶段,optimize
的主要作用是标记 static
静态节点
keep-alive组件利用lRU缓存淘汰
算法
异步组件,分两次渲染
先同级比较再比较子节点
先判断一方有子节点和一方没有子节点的情况。如果新的一方有子节点,旧的一方没有,相当于新的子节点替代了原来没有的节点;同理,如果新的一方没有子节点,旧的一方有,相当于要把老的节点删除。
再来比较都有子节点的情况,这里是diff的核心。首先会通过判断两个节点的key、tag、isComment、data同时定义或不定义以及当标签类型为input的时候type相不相同来确定两个节点是不是相同的节点,如果不是的话就将新节点替换旧节点。
如果是相同节点的话才会进入到patchVNode阶段。在这个阶段核心是采用双指针的算法,同时从新旧节点的两端进行比较,在这个过程中,会用到模版编译时的静态标记配合key来跳过对比静态节点,如果不是的话再进行其它的比较。
举例说明:
// old arr
["a", "b", "c", "d", "e", "f", "g", "h"]
// new arr
["a", "b", "d", "f", "c", "e", "x", "y", "g", "h"]
复制代码
从头到尾开始比较,[a,b]是sameVnode,进入patch,到 [c] 停止;
从尾到头开始比较,[h,g]是sameVnode,进入patch,到 [f] 停止;
判断旧数据是否已经比较完毕,多余的说明是新增的,需要mount(本例中没有)
判断新数据是否已经比较完毕,多余的说明是删除的,需要unmount(本例中没有)
patchVNode阶段。在这个阶段核心是采用双指针的算法,同时从新旧节点的两端进行比较,在这个过程中,会用到模版编译时的静态标记配合key来跳过对比静态节点,如果不是的话再进行其它的比较。
缺点:因为采用的是同级比较,所以如果发现本级的节点不同的话就会将新节点之间替换旧节点,不会再去比较其下的子节点是否有相同
Vue2、Vue3
Vue3.x借鉴了ivi算法和inferno算法。
它在创建VNode的时候就确定了其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升
vue 和 react
相同是都是用同层比较,不同是 vue使用双指针比较,react 是用 key 集合级比较
VueRouter对不同模式的实现大致是这样的:
首先根据mode来确定所选的模式,如果当前环境不支持history模式,会强制切换到hash模式;
如果当前环境不是浏览器环境,会切换到abstract模式下。然后再根据不同模式来生成不同的history操作对象。
new Router过程
hash和history的区别