有句老话说,在父母那里我们永远是孩子,同样在各位大佬这里,我永远是菜鸡🐣🐣🐣。不管怎样,学习的激情永远不可磨灭。答案如有错误,感谢指教🌚
# 首发个人博客
种一棵树,最好的时机是十年前,其次是现在 (opens new window)
# vue源码中值得学习的点
科里化
: 一个函数原本有多个参数, 只传入一个
参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构偏函数
: 一个函数原本有多个参数, 只传入一部分
参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构高阶函数
: 一个函数参数是一个函数
, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数- ...
# vue 响应式系统
简述:
vue 初始化时会用Object.defineProperty()
给data中每一个属性添加getter
和setter
,同时创建dep
和watcher
进行依赖收集
与派发更新
,最后通过diff
算法对比新老vnode差异,通过patch
即时更新DOM
# 简易图解:
# 详细版本
可以参考下图片引用地址: 图解 Vue 响应式原理 (opens new window)
# Vue的数据为什么频繁变化但只会更新一次
- 检测到数据变化
- 开启一个队列
- 在同一事件循环中缓冲所有数据改变
- 如果同一个
watcher (watcherId相同)
被多次触发,只会被推入到队列中一次
不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run
优化后:执行顺序update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM)
# vue使用Object.defineProperty() 的缺陷
数组的length属性被初始化configurable false
,所以想要通过get/set方法来监听length属性是不可行的。
vue中通过重写了七个
能改变原数组的方法来进行数据监听
对象还是使用Object.defineProperty()添加get和set来监听
参考
# Vue.nextTick()原理
在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
源码实现:Promise > MutationObserver > setImmediate > setTimeout
参考文章:浅析Vue.nextTick()原理 (opens new window)
# computed 的实现原理
computed 本质是一个惰性求值的观察者computed watcher
。其内部通过 this.dirty
属性标记计算属性是否需要重新求值。
- 当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher
通过this.dep.subs.length
判断有没有订阅者, - 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性
最终计算的值
发生变化时才会触发渲染 watcher
重新渲染,本质上是一种优化。) - 没有的话,仅仅把
this.dirty = true
(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
# watch 的理解
watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中
的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听
注意:Watcher : 观察者对象 , 实例分为渲染 watcher
(render watcher),计算属性 watcher
(computed watcher),侦听器 watcher
(user watcher)三种
# vue diff 算法
- 只对比父节点相同的新旧子节点(比较的是Vnode),时间复杂度只有O(n)
- 在 diff 比较的过程中,循环从两边向中间收拢
新旧节点对比过程
1、先找
到 不需要移动的相同节点,借助key值找到可复用的节点是,消耗最小
2、再找相同但是需要移动
的节点,消耗第二小
3、最后找不到,才会去新建删除
节点,保底处理
注意:新旧节点对比过程,不会对这两棵Vnode树进行修改,而是以比较的结果直接对 真实DOM 进行修改
Vue的patch是即时的
,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理)
参考文章:Vue 虚拟dom diff原理详解 (opens new window)
# vue 渲染过程
- 调用
compile
函数,生成 render 函数字符串 ,编译过程如下:
- parse 使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
模板 -> AST (最消耗性能)
- optimize 遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,
优化runtime的性能
- generate 将最终的AST转化为render函数字符串
- 调用
new Watcher
函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象 - 调用
patch
方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
# 结合源码,谈一谈vue生命周期
vue 生命周期官方图解 (opens new window)
# Vue 中的 key 到底有什么用?
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速 : key 的唯一性
可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
,源码如下:
function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key;
const map = {};
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key;
if (isDef(key)) map[key] = i;
}
return map;
}
# vue-router 路由模式有几种
默认值: "hash"
(浏览器环境) | "abstract"
(Node.js 环境)
可选值: "hash"
| "history"
| "abstract"
配置路由模式:
hash
: 使用 URL hash 值来作路由。支持所有浏览器
,包括不支持 HTML5 History Api 的浏览器。history
: 依赖HTML5 History API
和服务器配置。abstract
: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
# 说一说keep-alive实现原理
# 定义
keep-alive
组件接受三个属性参数:include
、exclude
、max
include
指定需要缓存的组件name
集合,参数格式支持String, RegExp, Array。
当为字符串的时候,多个组件名称以逗号隔开。exclude
指定不需要缓存的组件name
集合,参数格式和include一样。max
指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String、Number。
# 原理
keep-alive
实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode
LRU(Least recently used)
算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)
# 对对象属性访问的解析方法
eg:访问 a.b.c.d
函数柯里化 + 闭包 + 递归
function createGetValueByPath( path ) {
let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
return function getValueByPath( obj ) {
let res = obj;
let prop;
while( prop = paths.shift() ) {
res = res[ prop ];
}
return res;
}
}
let getValueByPath = createGetValueByPath( 'a.b.c.d' );
var o = {
a: {
b: {
c: {
d: {
e: '正确了'
}
}
}
}
};
var res = getValueByPath( o );
console.log( res );
# vue中针对7个数组方法的重写
Vue 通过原型拦截
的方式重写了数组的 7 个方法,首先获取到这个数组的Observer
。如果有新的值,就调用 observeArray
对新的值进行监听,然后调用 notify
,通知 render watcher
,执行 update
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
methodsToPatch.forEach(function(method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
# vue处理响应式 defineReactive 实现
// 简化后的版本
function defineReactive( target, key, value, enumerable ) {
// 折中处理后, this 就是 Vue 实例
let that = this;
// 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
if ( typeof value === 'object' && value != null && !Array.isArray( value ) ) {
// 是非数组的引用类型
reactify( value ); // 递归
}
Object.defineProperty( target, key, {
configurable: true,
enumerable: !!enumerable,
get () {
console.log( `读取 ${key} 属性` ); // 额外
return value;
},
set ( newVal ) {
console.log( `设置 ${key} 属性为: ${newVal}` ); // 额外
value = reactify( newVal );
}
} );
}
# vue响应式 reactify 实现
// 将对象 o 响应式化
function reactify( o, vm ) {
let keys = Object.keys( o );
for ( let i = 0; i < keys.length; i++ ) {
let key = keys[ i ]; // 属性名
let value = o[ key ];
if ( Array.isArray( value ) ) {
// 数组
value.__proto__ = array_methods; // 数组就响应式了
for ( let j = 0; j < value.length; j++ ) {
reactify( value[ j ], vm ); // 递归
}
} else {
// 对象或值类型
defineReactive.call( vm, o, key, value, true );
}
}
}
# 为什么访问data属性不需要带data
vue中访问属性代理this.data.xxx 转换 this.xxx的实现
/** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
function proxy( target, prop, key ) {
Object.defineProperty( target, key, {
enumerable: true,
configurable: true,
get () {
return target[ prop ][ key ];
},
set ( newVal ) {
target[ prop ][ key ] = newVal;
}
} );
}