vue的一些重要知识点
从vue源码里学到的一些知识
- 函数科里化:一个函数原本有多个参数,但是只传入一个参数,生成一个新函数,由新函数接收剩下的参数来运行
- 偏函数:一个函数原本有多个参数,但是只传入部分参数,生成一个新函数,由新函数接收剩下的参数来运行
- 高阶函数:一个函数的参数是一个函数,该函数对参数进行加工从而得到了一个新函数,这个加工过后的函数就是高阶函数
vue响应式系统
vue初始化的时候,会用Object.defineProperty函数给data中每一个属性添加getter,setter,同时创建dep和watcher进行
依赖收集和派发更新,最后通过diff算法对比新老vnode差异,通过patch及时更新DOM
vue的数据频繁变化,为何只会更新一次
- 检测到数据变化
- 开启一个队列
- 再同一事件循环里缓冲所有的数据改变
- 如果同一个
watcher被多次触发,那么只会被推入队列一次,所以就只会执行一次
Object.defineProperty的缺陷
数组的length属性被初始化configurable:false,就变成不可枚举了,然后就没法通过 get/set 来监听length了。
vue2 里就重写了 7 个能够改变原数组的方式来实现监听
而普通对象还是使用Object.defineProperty添加set/get来监听
vue.nextTick的原理
在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM
源码实现是通过微/宏任务来实现的。Promise > Mutation Observer > setImmediate > setTimeout
vue diff 算法
- 只对比父节点相同的新旧节点(比较的是
vNode),时间复杂度是O(n) - 在
diff的过程中,循环从两边向中间靠拢
新旧节点对比过程
- 先找到不需要移动的相同节点,借助
key值找到可复用的节点,这是消耗最小的 - 再找相同但是需要移动的节点,消耗第二小
- 最后找不到,才会去新建删除节点,保底处理
注意:新旧节点对比过程,不会对这两棵Vnode树进行修改,而是以比较的结果直接对 真实DOM 进行修改
Vue的patch是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理)
具体简介:0.5 virtualDom 和 diff 算法
vue渲染过程
- 调用
compile函数,生成render函数字符串,编译过程如下:parse函数使用大量的正则表达式对template字符串进行解析,将标签,指令,属性等转换为抽象语法树AST。模板到AST非常消耗性能optimise函数遍历AST,找到其中的一些静态节点并进行标记,方便在页面重新渲染的时候进行diff比较,直接跳过这一节点generate将最终的的AST转换为render函数字符串
- 调用
new watcher函数,监听数据的变化,当数据发生变化时,render函数执行生成vNode对象 - 调用
patch方法,对比新旧vNode对象,通过DOM diff算法来添加,修改删除真正的DOM元素
vue生命周期
见官网
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)
可选值:hash | history | abstract
配置路由模式:
hash使用 URLhash来作为路由。支持所有浏览器,包括不支持HTML5 History Api的浏览器history依赖HTML5 History Api和服务器配置abstract支持所有JavaScript运行环境,如nodejs服务端。如果发现没有浏览器Api,路由会强制进入这个模式
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);
}
}
}
vue中访问data属性为啥不需要带data
/** 将某一个对象的属性访问映射到对象的某一个属性成员上 */
function proxy(target, prop, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return target[prop][key];
},
set(newVal) {
target[prop][key] = newVal;
},
});
}
为什么this.$nextTick里能获取到更新后的dom
调用this.$nextTick方法其实就是调用vue里的nextTick函数,再异步队列里执行回调函数。根据先进先出的原则,修改data触发的更新异步队列会先执行,执行完后dom会更新渲染。接下来再执行this.$nextTick里的回调函数,所以就能获取更新后的dom元素了。
// 我们使用 this.$nextTick 其实就是调用 nextTick 方法
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this);
};
总结vue异步更新原理
- 修改
vue里的data时,就会触发所有和这个data相关的watcher进行更新 - 首先,会将所有的
watcher加入到队列Queue - 然后,调用
nextTick方法,执行异步任务 - 再异步任务的回调中,对
Queue中的watcher进行排序,然后执行DOM更新
为何data是一个函数而不是一个对象
因为JavaScript里对象是引用类型的数据,当多个实例引用同一个对象的时候,只要一个实例对数据进行了操作,那么其他实例中的数据也会发生变化。而在vue中,这显然是不可行的,我们需要组件有自己单独的数据。所以数据必须是一个函数,而数据以函数返回值的形式定义,这样我们组件就有了自己的数据源,而不是共用的了。
vue2中数组没有及时更新的解决办法
- 可以用
this.$set去设置 - 使用
push,pop,shift等数组方法去更新数组 - 使用官方的
this.$forceUpdate()去更新 - 可以用
hack的写法,比如[...Array],JSON.parse(JSON.stringify)
请写一个mini版本的vue router
const Vue; // 先定义一个Vue
class MyVueRouter {
constructor(options) {
this.$options = options;
this.routeMap = {}; // 存储路由信息
// 路由响应式绑定
this.app = new Vue({
data: {
current: '/'
}
})
}
init() {
this.bindEvents(); // 监听url变化,绑定事件
this.createRouteMap(this.$options); // 解析路由配置
this.initComponent(); // 实现router-view
}
bindEvents() {
// 监听刚打开页面的时候
window.addEventListener('load', this.onHashChange.bind(this));
// 监听URL变化
window.addEventListener('hashchange', this.onHashChange.bind(this));
}
onHashChange() {
/*
比如这个网站:https://xumeng.life/#test
window.location.hash返回的是#test
slice接受2个参数start和end,传1表示把test获取到
*/
this.app.current = window.location.hash.slice(1) || '/';
}
createRouteMap(options) {
options.routes.forEach(item => {
this.routeMap[item.path] = item.component;
})
}
initComponent() {
Vue.component('router-link', {
props: {
to: String
},
render(h) {
/*
h函数全称hyperscript,意思是能够创建html的js,许多虚拟dom都会用这个来实现,约定俗成的
比如你可以这样使用:
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })
// 像 `.prop` 和 `.attr` 这样的的属性修饰符
// 可以分别通过 `.` 和 `^` 前缀来添加
h('div', { '.name': 'some-name', '^width': '100' })
// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
*/
return h('a', {
attrs: {
href: '#' + this.to
}},
[this.$slots.default])
}
});
Vue.component('router-view', {
render: h => {
const com = this.routeMap[this.app.current];
return h(com);
}
})
}
};
const router = new MyVueRouter({
routes: [
{
path: '/',
name: 'index',
component: a
},
{
path: '/',
name: 'about',
component: b
}
]
});