前言
本文内容:
- Vue响应式
- 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
- Vue模板编译
- Vue 中 computed
- vue 是如何对数组方法进行变异的 ?
- vm.$set()
- Vue 中 computed
- Vue模板编译
- Vue中的Key有什么作用
- 用VNode来描述一个DOM结构
- diff算法
- Vue 组件 data 为什么必须是函数
- Vue异步更新机制& nextTicket
- 为什么Vue采用异步渲染呢
- Vue 事件机制,手写off,once
- Vue的事件绑定
- Vue的生命周期
- 父子组件生命周期调用顺序
- Vue组件通信
- v-if 和 v-show 区别
- v-html 会导致哪些问题
- keep-alive组件缓存
- 双向绑定
- Vue3新特性
1.Vue响应式
Vue响应式原理的核心就是Observer、Dep、Watcher
1.1 Observer 「响应式」
vue将data初始化为一个Observer并对对象中的每个值,重写了其中的get、set,data中的每个key,都有一个独立的依赖收集器。- 在
get中,向依赖收集器添加了监听
1.1.1 Obejct.definedProperty()
Vue的响应式原理是通过Object.defineProperty实现的。被Object.defineProperty绑定过的对象,会变成「响应式」化。也就是改变这个对象的时候会触发get和set事件。进而触发一些视图更新。
# 简易Vue响应式
const defineReactive = function(obj, key) {
// 局部变量dep,用于get set内部调用
const dep = new Dep();
// 获取当前值
let val = obj[key];
Object.defineProperty(obj, key, {
// 设置当前描述属性为可被循环
enumerable: true,
// 设置当前描述属性可被修改
configurable: true,
get() {
console.log('in get');
// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
dep.depend();
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
// 这里每个需要更新通过什么断定?dep.subs
dep.notify();
}
});
}
3.x的与2.x的核心思想一致,只不过数据的劫持使用Proxy而不是Object.defineProperty,只不过Proxy相比Object.defineProperty在处理数组和新增属性的响应式处理上更加方便。
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
1.1.2 Observer 「响应式」
Vue中用Observer类来管理上述响应式化Object.defineProperty的过程。我们可以用如下代码来描述,将this.data也就是我们在Vue代码中定义的data属性全部进行「响应式」绑定,在数据被读的时候,触发get方法,执行Dep来收集依赖,也就是收集Watcher。在data值发生变更时,触发set,触发了依赖收集器中的所有监听的更新,来触发Watcher.update
# 简易Vue响应式
const Observer = function(data) {
// 循环修改为每个属性添加get set
for (let key in data) {
defineReactive(data, key);
}
}
const observe = function(data) {
return new Observer(data);
}
const Vue = function(options) {
const self = this;
// 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
if (options && typeof options.data === 'function') {
this._data = options.data.apply(this);
}
// 挂载函数
this.mount = function() {
new Watcher(self, self.render);
}
// 渲染函数
this.render = function() {
with(self) { _data.text; }
}
// 监听this._data
observe(this._data);
}
1.2 Dep 依赖(Watcher)收集器
1.2.1 有哪些依赖(Watcher)
我们通过defineReactive方法将data中的数据进行响应式后,虽然可以监听到数据的变化了,那我们怎么处理通知视图就更新呢?
Dep就是帮我们收集【究竟要通知到哪里的】。比如下面的代码案例,我们发现,虽然data中有text和message属性,但是只有message被渲染到页面上,至于text无论怎么变化都影响不到视图的展示,因此我们仅仅对message进行收集即可,可以避免一些无用的工作。
那这个时候message的Dep就收集到了一个依赖,这个依赖就是用来管理data中message变化的。
<div>
<p>{{message}}</p>
</div>
data: {
text: 'hello world',
message: 'hello vue',
}
当使用watch属性时,也就是开发者自定义的监听某个data中属性的变化。比如监听message的变化,message变化时我们就要通知到watch这个钩子,让它去执行回调函数。
这个时候message的Dep就收集到了两个依赖,第二个依赖就是用来管理watch中message变化的。
watch: {
message: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
}
当开发者自定义computed计算属性时,如下messageT属性,是依赖message的变化的。因此message变化时我们也要通知到computed,让它去执行回调函数。 这个时候message的Dep就收集到了三个依赖,这个依赖就是用来管理computed中message变化的。
computed: {
messageT() {
return this.message + '!';
}
}
图示如下:一个属性可能有多个依赖,每个响应式数据都有一个Dep来管理它的依赖。
1.2.2 如何收集依赖
我们如何知道data中的某个属性被使用了,答案就是Object.defineProperty,因为读取某个属性就会触发get方法。代码实例 如1.1。
# 简易Vue响应式
const Dep = function() {
const self = this;
// 收集目标
this.target = null;
// 存储收集器中需要通知的Watcher
this.subs = [];
// 当有目标时,绑定Dep与Wathcer的关系
this.depend = function() {
if (Dep.target) {
// 这里其实可以直接写 self.addSub(Dep.target),
// 没有这么写因为想还原源码的过程。
Dep.target.addDep(self); // 相当于 self.addSub(Dep.target),
}
}
// 通知收集器中所的所有Wathcer,调用其update方法
this.notify = function() {
for (let i = 0; i < self.subs.length; i += 1) {
self.subs[i].update();
}
}
// 为当前收集器添加Watcher
this.addSub = function(watcher) {
self.subs.push(watcher);
}
}
依赖收集
- initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
- initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
- render()的过程,触发 render watcher 依赖收集
- re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。
派发更新
- 组件中对响应的数据进行了修改,触发 setter 的逻辑
- 调用 dep.notify()
- 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。
1.3 依赖Watcher
- 在mount时,实例了一个
Watcher,将收集器的目标指向了当前Watcher - 在
data值发生变更时,触发set,触发了依赖收集器中的所有监听的更新,来触发Watcher.update
1.3.1 Watcher 「中介」
Watcher就是类似中介的角色,比如message就有三个中介,当message变化,就通知这三个中介,他们就去执行各自需要做的变化。
Watcher能够控制自己属于哪个,是data中的属性的还是watch,或者是computed,Watcher自己有统一的更新入口,只要你通知它,就会执行对应的更新方法。
因此我们可以推测出,Watcher必须要有的2个方法。一个就是通知变化,另一个就是被收集起来到Dep中去。
class Watcher {
addDep() {
// 我这个Watcher要被塞到Dep里去了~~
},
update() {
// Dep通知我更新呢~~
},
}
# 简易Vue响应式
const Watcher = function(vm, fn) {
const self = this;
this.vm = vm;
// 将当前Dep.target指向自己
Dep.target = this;
// 向Dep方法添加当前Wathcer
this.addDep = function(dep) {
dep.addSub(self);
}
// 更新方法,用于触发vm._render
this.update = function() {
console.log('in watcher update');
fn();
}
// 这里会首次调用vm._render,从而触发text的get
// 从而将当前的Wathcer与Dep关联起来
this.value = fn();
// 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
// 造成代码死循环
Dep.target = null;
}
综上:
Observer负责将数据转换成getter/setter形式,用于依赖收集和派发更新
Dep负责管理数据的依赖列表;是一个发布订阅模式,上游对接Observer,下游对接Watcher,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher
Watcher观察者对象,是实际上的数据依赖,负责将数据的变化转发到外界(渲染、回调),实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种;
首先将data传入Observer转成getter/setter形式;当Watcher实例读取数据时,会触发getter,被收集到Dep仓库中;当数据更新时,触发setter,通知Dep仓库中的所有Watcher实例更新,Watcher实例负责通知外界
# 简易Vue响应式
const vue = new Vue({
data() {
return { text: 'hello world' };
}
})
vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get
2. 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
另外,Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
Vue为什么不能检测数组变动?
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组的索引也是属性,所以我们是可以监听到数组元素的变化的。
var arr = [1,2,3,4]
arr.forEach((item,index)=>{
Object.defineProperty(arr,index,{
set:function(val){
console.log('set')
item = val
},
get:function(val){
console.log('get')
return item
}
})
})
arr[1]; // get 2
arr[1] = 1; // set 1
但是我们新增一个元素,就不会触发监听事件,因为这个新属性我们并没有监听,删除一个属性也是。
既然数组是可以被监听的,那为什么vue不能检测vm.items[indexOfItem] = newValue导致的数组元素改变呢,哪怕这个下标所对应的元素是存在的,且被监听了的?
可以看到,当数据是数组时,会停止对数据属性的监测。
这是为什么呢?其实是defineProperty是可以检测到数组操作的, 只是,结合数组的api以及根据序号来处理,涉及到的处理量有可能非常巨大。
其实本质上就是性能的取舍。
对于在原有数组上的修改读取没有问题,push和pop是操作尾部的,O(1)复杂度,问题不大。\
但'shift', 'unshift', 'splice', 'sort', 'reverse',
这些大概率会触发数组索引的移动或变动,触发很多次的get和set。
如果数组长度为1000呢,挨个`defineReactive`么?
性能消耗太大,vue的做法是修改原生操作数组的方法,
并且跟用户约定修改数组要用这些方法去操作
3. vue 是如何对数组方法进行变异的 ?
Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 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"
];
/**
* Intercept mutating methods and emit events
*/
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;
});
});
/**
* Observe a list of Array items.
*/
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
Vue 不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue - 当你修改数组的长度时,例如:
vm.items.length = newLength
为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)
4. vm.$set()
export function set(target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splice()执行有误
target.length = Math.max(target.length, key);
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val);
return val;
}
// target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 以上都不成立, 即开始给target创建一个全新的属性
// 获取Observer实例
const ob = (target: any).__ob__;
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val;
return val;
}
// 进行响应式处理
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
- 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
- 如果目标是对象,判断属性存在,即为响应式,直接赋值
- 如果 target 本身就不是响应式,直接赋值
- 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理
5. Vue 中 computed
computed 本质是一个惰性求值的观察者。
5.1 为什么要computed
一个最基本的例子如下:
<div id="app">
<p>{{fullName}}</p>
</div>
new Vue({
data: {
firstName: 'Xiao',
lastName: 'Ming'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
Vue 中我们不需要在 template 里面直接计算 {{this.firstName + ' ' + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。
5.2 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 watcher 通过 this.dep.subs.length 判断有没有订阅者,
5.3 怎么computed
-
当组件初始化的时候,
computed和data会分别建立各自的响应系统,Observer遍历data中每个属性设置get/set数据拦截 -
初始化
computed会调用initComputed函数- 注册一个
watcher实例,并在内实例化一个Dep消息订阅器用作后续收集依赖(比如渲染函数的watcher或者其他观察该计算属性变化的watcher) - 调用计算属性时会触发其
Object.defineProperty的get访问器函数 - 调用
watcher.depend()方法向自身的消息订阅器dep的subs中添加其他属性的watcher - 调用
watcher的evaluate方法(进而调用watcher的get方法)让自身成为其他watcher的消息订阅器的订阅者,首先将watcher赋给Dep.target,然后执行getter求值函数,当访问求值函数里面的属性(比如来自data、props或其他computed)时,会同样触发它们的get访问器函数从而将该计算属性的watcher添加到求值函数中属性的watcher的消息订阅器dep中,当这些操作完成,最后关闭Dep.target赋为null并返回求值函数结果。
- 注册一个
-
当某个属性发生变化,触发
set拦截函数,然后调用自身消息订阅器dep的notify方法,遍历当前dep中保存着所有订阅者wathcer的subs数组,并逐个调用watcher的update方法,完成响应更新。
5.4 对比侦听器 watch
都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
5.4.1 区别
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
5.4.2 运用场景
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
6. Vue模板编译
6.1 .vue 文件的模板到底是怎么转换成虚拟 DOM 的呢
从入口文件开始看:
// 省略了部分代码,只保留了关键部分
import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el) {
const options = this.$options
// 如果没有 render 方法,则进行 template 编译
if (!options.render) {
let template = options.template
if (template) {
// 调用 compileToFunctions,编译 template,得到 render 方法
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 这里的 render 方法就是生成生成虚拟 DOM 的方法
options.render = render
}
}
return mount.call(this, el, hydrating)
}
再看看 ./compiler/index 文件的 compileToFunctions 方法从何而来。
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
// 通过 createCompiler 方法生成编译函数
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
后续的主要逻辑都在 compiler 模块中,简单看看这一段的逻辑是怎么样的。
export function createCompiler(baseOptions) {
const baseCompile = (template, options) => {
// 解析 html,转化为 ast
const ast = parse(template.trim(), options)
// 优化 ast,标记静态节点
optimize(ast, options)
// 将 ast 转化为可执行代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
const compile = (template, options) => {
const tips = []
const errors = []
// 收集编译过程中的错误信息
options.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}
// 编译
const compiled = baseCompile(template, options)
compiled.errors = errors
compiled.tips = tips
return compiled
}
const createCompileToFunctionFn = () => {
// 编译缓存
const cache = Object.create(null)
return (template, options, vm) => {
// 已编译模板直接走缓存
if (cache[template]) {
return cache[template]
}
const compiled = compile(template, options)
return (cache[key] = compiled)
}
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
6.2 baseCompile
主要的编译逻辑基本都在 baseCompile 方法内。
主要分为三个步骤:
- 模板编译,解析 html,将模板代码转化为 AST;
- 优化 AST,标记静态节点,方便后续虚拟 DOM 更新;
- 由于 Vue 是响应式设计,所以拿到 AST 之后还需要进行一系列优化,确保静态的数据不会进入虚拟 DOM 的更新阶段,以此来优化性能。简单来说,就是把所以静态节点的 static 属性设置为 true。
function isStatic (node) { if (node.type === 2) { // 表达式,返回 false return false } if (node.type === 3) { // 静态文本,返回 true return true } // 此处省略了部分条件 return !!( !node.hasBindings && // 没有动态绑定 !node.if && !node.for && // 没有 v-if/v-for !isBuiltInTag(node.tag) && // 不是内置组件 slot/component !isDirectChildOfTemplateFor(node) && // 不在 template for 循环内 Object.keys(node).every(isStaticKey) // 非静态节点 ) } function markStatic (node) { node.static = isStatic(node) if (node.type === 1) { // 如果是元素节点,需要遍历所有子节点 for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { // 如果有一个子节点不是静态节点,则该节点也必须是动态的 node.static = false } } } }
- 由于 Vue 是响应式设计,所以拿到 AST 之后还需要进行一系列优化,确保静态的数据不会进入虚拟 DOM 的更新阶段,以此来优化性能。简单来说,就是把所以静态节点的 static 属性设置为 true。
- 生成代码,将 AST 转化为可执行的代码;
- 需要将 AST 转化为 render 方法
<div> <h2 v-if="message">{{message}}</h2> <button @click="showName">showName</button> </div> { render: "with(this){return _c('div',[(message)?_c('h2',[_v(_s(message))]):_e(),_v(" "),_c('button',{on:{"click":showName}},[_v("showName")])])}" } 将生成的代码展开: with (this) { return _c( 'div', [ (message) ? _c('h2', [_v(_s(message))]) : _e(), _v(' '), _c('button', { on: { click: showName } }, [_v('showName')]) ]) ; } # 这里的 `_c` 对应的是虚拟 DOM 中的 `createElement` 方法
- 需要将 AST 转化为 render 方法
6.3 执行变化&渲染
- ($mount)调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
- (更新)调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
6.4 Vue实例挂载的过程中发生了什么?
挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化和建立更新机制
- 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
- 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
7. Vue中的Key有什么作用
key 是给每一个 vnode 的唯一 id, 依靠 key, 我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快, 但会产生一些隐藏的副作用, 比如可能不会产生过渡效果, 或者在某些节点有绑定数据(表单)状态,会出现状态错位。)
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
更准确 : 因为带 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;
}
8. 用VNode来描述一个DOM结构
优点:
- 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
- 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
- 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
9. diff算法
diff算法是干什么的?
Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。
diff算法的必要性:
最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。
## 既然vue通过数据劫持可以精准的探测数据变化,为什么还要进行diff检测差异?
- 响应式数据变化,Vue确实可以在数据变化的时候,响应式系统可以立刻得知。
但是如何每个属性都添加watcher的话,性能会非常的差。
- 粒度过细,会导致更新不精准
所以采用watcher + Diff算法来检测差异。
diff算法何时执行?
vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作
diff算法的具体执行方式 patch过程是一个递归过程,遵循深度优先、同层比较的策略;
-
只进行同层比较,不会进行跨层比较。
-
只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的
-
首先判断两个节点是否为相同同类节点,不同则删除重新创建
-
如果双方都是文本则更新文本内容
-
如果双方都是元素节点则递归更新子元素,<同时更新元素属性>
-
更新子节点时又分了几种情况:
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
-
-
最小量更新,
key很重要。这个可以是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点- 扩展
v-for为什么要有key,没有key会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改DOM),加key只会移动减少操作DOM。
- 扩展
diff算法的优化策略:四种命中查找,四个指针
- 旧前与新前(先比开头,后插入和删除节点的这种情况)
- 旧后与新后(比结尾,前插入或删除的情况)
- 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
- 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)
vue2与vue3之间diff算法的区别:
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。
10. Vue 组件 data 为什么必须是函数 ?
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。
所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
11. Vue异步更新机制
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
主线程的执行过程就是一个 tick,借助js事件循环来理解nextTicket,强制进入下次主线程。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。
只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 - 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是
结束等待状态,进入执行栈,开始执行。 - 主线程不断重复上面的第三步。
nextTicket nextTick是一个微任务。
-
nextTick是等待下一次 DOM 更新刷新的工具方法。
-
Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在
同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。 -
开发时,有两个场景我们会用到nextTick:
- created中想要获取DOM时;
- 响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度。
-
nextTick签名如下:
function nextTick(callback?: () => void): Promise<void>所以我们
只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事。 -
在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。
12. 为什么Vue采用异步渲染呢?
13. Vue 事件机制,手写off,once
Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
14. Vue的事件绑定
原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。
15. Vue的生命周期
1.每个Vue组件实例被创建后都会经过一系列初始化步骤,比如,它需要数据观测,模板编译,挂载实例到dom上,以及数据变化时更新dom。这个过程中会运行叫做生命周期钩子的函数,以便用户在特定阶段有机会添加他们自己的代码。
2.Vue生命周期总共可以分为8个阶段:创建前后, 载入前后, 更新前后, 销毁前后,以及一些特殊场景的生命周期。vue3中新增了三个用于调试和服务端渲染场景。
| 生命周期v2 | 生命周期v3 | 描述 |
|---|---|---|
| beforeCreate | beforeCreate | 组件实例被创建之初,在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问 |
| created | created | 组件实例已经完全创建,在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom |
| beforeMount | beforeMount | 组件挂载之前 ,相关的 render 函数首次被调用 |
| mounted | mounted | 组件挂载到实例上去之后 ,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点 |
| beforeUpdate | beforeUpdate | 组件数据发生变化,更新之前,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程 |
| updated | updated | 数据数据更新之后, 当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。 |
| beforeDestroy | beforeUnmount | 组件实例销毁之前,在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。 |
| destroyed | unmounted | 组件实例销毁之后 ,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。 |
| 生命周期v2 | 生命周期v3 | 描述 |
|---|---|---|
| activated | activated | keep-alive 缓存的组件激活时 |
| deactivated | deactivated | keep-alive 缓存的组件停用时调用 |
| errorCaptured | errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
| - | renderTracked | 调试钩子,响应式依赖被收集时调用 |
| - | renderTriggered | 调试钩子,响应式依赖被触发时调用 |
| - | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用 |
3.Vue生命周期流程图:
4.结合实践:
beforeCreate:通常用于插件开发中执行一些初始化任务
created:组件初始化完毕,可以访问各种数据,获取接口数据等
mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。
beforeUpdate:此时view层还未更新,可用于获取更新前各种状态
updated:完成view层的更新,更新后,所有状态已是最新
beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
5.追问
-
setup和created谁先执行?
-
setup中为什么没有beforeCreate和created?
16. 父子组件生命周期调用顺序
渲染顺序:先父后子,完成顺序:先子后父
- 创建过程自上而下,挂载过程自下而上;即:
父 beforeCreate -> 父 created -> 父 beforeMount
-> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted
-> 父 mounted
- 之所以会这样是因为Vue创建过程是一个递归过程,
先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;
子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,
可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。
更新顺序:父更新导致子更新,子更新完成后父完成更新
- 子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程
父 beforeUpdate -> 父 updated
销毁顺序:先父后子,完成顺序:先子后父
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
17. Vue组件通信
- 父子间通信:父亲提供数据通过属性
props传给儿子;儿子通过$on绑父亲的事件,再通过$emit触发自己的事件(发布订阅)父子组件通信 - 利用父子关系
$parent、$children, 获取父子组件实例的方法。父子组件通信 ref获取组件实例,调用组件的属性、方法 父子组件通信- 祖先组件提供数据,后代组件注入。
provide、inject,插件用得多。隔代组件通信 - 跨组件通信
EventBus(Vue.prototype.bus=newVue)其实基于bus = new Vue)其实基于bus=newVue)其实基于on与$emit 父子、隔代、兄弟组件通信 Vuex状态管理实现通信,父子、隔代、兄弟通信- listeners A->B->C。Vue 2.4 开始提供了listeners 来解决这个问题
- props
- \$emit/ ~~$on~~
- ~~\$children~~/$parent
- ref
- eventbus
- vuex
注意vue3中废弃的几个API
18. v-if 和 v-show 区别
v-if如果条件不成立不会渲染当前指令所在节点的DOM元素v-show只是切换当前DOM的显示与隐藏
19. v-html 会导致哪些问题
XSS攻击v-html会替换标签内部的元素
20. keep-alive组件缓存
- 缓存用keep-alive,它的作用与用法
- 使用细节,例如缓存指定/排除、结合router和transition
- 组件缓存后更新可以利用activated或者beforeRouteEnter
- 原理阐述
1.开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
```
<keep-alive>
<component :is="view"></component>
</keep-alive>
```
2.结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive:
```
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
```
3.缓存后如果要获取数据,解决方案可以有以下两种:
- beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行`beforeRouteEnter`
```
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
```
- actived:在`keep-alive`缓存的组件被激活的时候,都会执行`actived`钩子
```
activated(){
this.getData() // 获取数据
},
```
4.keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。
21. 双向绑定
v-model是语法糖,默认情况下相当于:value和@input。使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。
v-model是一个指令,它的神奇魔法实际上是vue的编译器完成的。我做过测试,包含v-model的模板,转换为渲染函数之后,实际上还是是value属性的绑定以及input事件监听,事件回调函数中会做相应变量更新操作。
观察输出的渲染函数:
// <input type="text" v-model="foo">
相当于
<input v-bind:value="foo" v-on:input="foo = $event.target.value">
解析
_c('input', {
directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }],
attrs: { "type": "text" },
domProps: { "value": (foo) },
on: {
"input": function ($event) {
if ($event.target.composing) return;
foo = $event.target.value
}
}
})
22. vue3新特性
也就是下面这些:
- Composition API
- SFC Composition API语法糖
- Teleport传送门
- Fragments片段
- Emits选项
- 自定义渲染器
- SFC CSS变量
- Suspense
- api层面Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense
- 另外,Vue3.0在框架层面也有很多亮眼的改进:
-
更快
- 虚拟DOM重写
- 编译器优化:静态提升、patchFlags、block等
- 基于Proxy的响应式系统
-
更小:更好的TreeShaking优化
-
更容易维护:TypeScript + 模块化
-
更容易扩展
- 独立的响应化模块
- 自定义渲染器