文章目录
- 1. 谈谈对 MVVM 的理解及原理
- 2. 响应式数据原理(02:50)
- 3. Vue 如何检测数组的变化(08:40)
- 4. Vue 为何采用异步渲染(16:00)
- 5. nextTick 实现原理(22:10)
- 6. 计算属性(computed)与侦听属性(watch)区别(30:10)
- 7. watch 的 deep 如何实现的?(55:04)
- 8. vue 组件的生命周期(51:50)
- 9.ajax请求放在哪个生命周期
- 10.何时使用 beforeDestroy(03:54)
- 11. Vue 模版编译原理(04:35)
- 12.v-if 与 v-show 区别(19:40)
- 13. 为什么 v-for 与 v-if 不可以连用?(24:20)
- 14. 虚拟 DOM(VNode描述 DOM 结构)(26:00)
- 15. diff 算法事件复杂度 (35:25)
- 16.Vue 中 diff 算法原理(优化)
- 参考资料
1. 谈谈对 MVVM 的理解及原理
-
传统的 MVC 模式是用户请求服务端路由,路由调用控制器处理,控制器获取数据返回给前端,页面重新渲染
-
MVVM:不需要手动操作 DOM,将数据绑定到 viewModel 层,会自动将数据渲染到视图层,通过 DOM 事件监听,当视图层变化会通知 viewModel 层更新数据
2. 响应式数据原理(02:50)
理解:
- 核心点是:
Object.defineProperty
- Vue 在初始化数(initData)据时,使用一个 Observer来监测所有 data 数据,Observer 使用 defineReactive方法(使用的是
Object.defineProperty
)会对 data 对象中的所有属性重新定义,获取(get)属性时,会进行依赖(当前组件的所有 Watcher)收集(dep.depend),属性变化(set)时通知相关依赖进行更新操作(dep.notify)
原理:
3. Vue 如何检测数组的变化(08:40)
理解:
- 未使用
Object.defineProperty( )
方法 - 使用函数劫持的方式重写数组的方法
- Vue 将 data 中的数组进行了原型链重写,调用
push、pop、shift、unshift、splice、reverse、sort
这七个方法时,会指向自定义的数组原型方法,可通知依赖更新数据 - 对于
push、unshift、splice
三个会插入数据的方法会对插入数据(inserted)进行监听
原理:
4. Vue 为何采用异步渲染(16:00)
理解:
- Vue 组件级更新
- 防止一更新数据就更新视图造成的性能浪费,Vue 会在本轮数据更新完成后更新视图
- dep.notify( ) 通知 watcher 进行更新
- 依次调用 subs[i].update() (subs 为一个 watcher 数组)
- queueWatcher( ) 根据 id 对 watcher 去重
- nextTick( flushScheduleQueue ):flushScheduleQueue函数会把 queue 中所有的 watcher 取出来,并执行视图更新的操作
原理:
5. nextTick 实现原理(22:10)
理解:
-
首先要了解 js 运行机制
js 是单线程的,基于事件循环。步骤如下:
1主线程执行同步任务(形成执行栈)
2主线程外有一个任务队列,异步任务有运行结果就在任务队列放置一个事件
3一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
4主线程不断重复以上第三步- 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制
- 异步任务又分为两大类:macro task 和 micro task
- 每个 macro task
结束后开始前,都要清空当前微任务队列里所有的 micro task - 常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate
- 常见的 micro task 有 MutationObsever 和 Promise.then
- nextTick 源码理解
1.申明了 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数
2.对于macro task 优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现
3.nextTick(cb) :
- 将 cb 压入回调函数数组 callbacks => 根据 useMacroTask (默认false) 执行 macroTimerFunc 或 microTimerFunc => 下一 tick 执行 flushCallbacks
原理:
6. 计算属性(computed)与侦听属性(watch)区别(30:10)
理解:
-
watcher 有四种类型:deep、user、computed、sync
-
deep:深度监听,调用 traverse 函数对对象作深层递归遍历,可收集到所有子对象的依赖
-
user:调用 $watch 方法创建的 watcher,会做一个错误捕获
-
computed:为计算属性量身定制
-
sync:同步执行 watcher 的回调函数
(默认情况下,响应式数据变化后,触发update( ) 只是把该 watcher 推送到一个队列中,在 nextTick 才会执行回调)
-
-
computed 属性具有缓存的功能,只有所依赖的属性发生变化时才更新视图
- 1.initComputed:对 computed 对象做遍历,拿到userDef => 获取对应的 getter函数 => 为每个 getter 创建
watcher
(默认lazy: true,不执行)=> 判断 key 非 vm 的属性,则调用defineComputed(vm, key, userDef)
- 2.defineComputed:利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter(使用较少), getter 对应的是 createComputedGetter(key) 的返回值
- 3.createComputedGetter:返回一个函数 computedGetter,它就是计算属性对应的 getter
- 1.initComputed:对 computed 对象做遍历,拿到userDef => 获取对应的 getter函数 => 为每个 getter 创建
原理:
7. watch 的 deep 如何实现的?(55:04)
理解:
-
1.initWatch:对 watch 对象做遍历,拿到每一个 handler => handler == 数组 ?遍历数组,调用 createWatcher 方法 :直接调用 createWatcher
-
2.createWatcher:对 hanlder 的类型做判断,拿到它最终的回调函数 => 调用 vm.$watch(keyOrFn, handler, options) 函数
-
本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher
-
deep 实现
-
在 watcher 执行 get 求值的过程,若 deep: true,则会执行 traverse 函数
get() { let value = this.getter.call(vm, vm) // ... if (this.deep) { traverse(value) } }
-
traverse 的逻辑:对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher
-
8. vue 组件的生命周期(51:50)
理解:
9.ajax请求放在哪个生命周期
理解:
10.何时使用 beforeDestroy(03:54)
理解:
11. Vue 模版编译原理(04:35)
理解:
- **baseCompile **:把模板
template
编译成render
函数,三个逻辑:-
1.parse:解析模版字符串生成 AST
- 输入 template + options ,输出 AST 对象
- 实现:正则 + 字符串截取 + advance 函数
- 对于开始标签、闭合标签、文本会分别执行不同的回调
- 三种 AST 元素结点类型type:1-普通元素、2-表达式、3-纯文本
-
2.optimize:优化 AST
-
目的:首次渲染后不会改变的数据,
patch
过程会直接跳过比对 -
实现:markStatic(root) 标记静态节点
-
markStaticRoots(root, false) 标记静态根
- 首先需要是普通元素(type==1)静态节点
- 且满足至少拥有一个子节点,且不能只有一个文本节点
- 最后遍历所有子节点均为静态节点,才满足
'static': true
-
-
3.codegen:生成代码
- generate(ast, options) 函数通过 genElement(ast, state) 生成可执行的 render 代码串,并用 with(this){return ${code}}} 包裹起来
- genElement(ast, state) 判断当前 AST 元素节点的属性执行不同的代码生成函数
-
原理:
12.v-if 与 v-show 区别(19:40)
理解:
- v-if 根据条件决定是否渲染(编译后是一个返回一个三元表达式的函数)
- v-show 都会渲染,只是控制显示与隐藏(编译为指令,控制display属性)
13. 为什么 v-for 与 v-if 不可以连用?(24:20)
理解:
- v-for 优先级更高,若与 v-if 连用则会给每个元素都加上 v-if ,造成性能浪费
原理:
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if {
// ...
}
// ...
14. 虚拟 DOM(VNode描述 DOM 结构)(26:00)
理解:
- 虚拟节点就是用对象描述真实的 DOM 元素
- Vue 中使用 createElement函数创建VNode
- 每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree
- 最后通过 vm._update 把 VNode 渲染成一个真实的 DOM 并渲染出来
- vm._update 的核心是 patch 方法,首次渲染我们调用了 createElm 方法,这里传入的 parentElm 是 oldVnode.elm 的父元素
原理:
15. diff 算法事件复杂度 (35:25)
理解:
-
传统 diff 算法时间复杂度 O(n^3)
O(n^2) — 将两个DOM树的所有节点两两对比
+
O(n) — 进行树的编辑(插入、替换、删除)需要遍历一次 -
Vue 对 diff 算法进行了优化:只进行同层级对比,时间复杂度 O(n)
16.Vue 中 diff 算法原理(优化)
理解:
- 只比较同层级 Vnode
- 新旧节点不同则直接替换
- 若相同,则比较子节点
- 指向相同对象,直接return
- 都只有文本节点,则替换文本节点
- 只有一个有子节点,则删除或新增
- 都有子节点的情况,是执行updateChildren函数来更新子节点
- 采用首尾双指针遍历新旧子节点
- 四个指针分别记为:oS(oldStart)、oE(oldEnd)、S(Start) 、E(End)
- 出现匹配的情况:
- oS = S / oE = E,指针内移一位
- oS = E / oE = s,真实 DOM 节点移到尾部或首部
- 全部不匹配,遍历 oldChild 依次与 S 匹配,成功则将其移到首位,否则,将 S 对应节点插到 DOM 中 oS 位置,oS 与 S 指针内移一位
+ 图解
原理: