Virtual DOM Tree
虚拟DOM为前端框架带来了跨平台的能力,是一层对真实DOM的抽象
在JS中,虚拟DOM是一个Object对象,并且至少包含tag,attrs和children三个属性,不同的框架对这三个属性的命名可能会有差别
通过VNode,Vue可以对抽象树进行创建节点,删除节点以及修改节点,经过diff算法得出一些需要修改的最小单位,再更新视图,减少了DOM操作,提高了性能
性能问题
一个真实的DOM节点,哪怕是一个最简单的div也包含很多属性,操作DOM的代价是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验
举个例子
用传统的原生API或jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程
当你在一次操作时,需要更新10个DOM节点,浏览器收到第一个更新DOM请求后,并不知道后续还有9个过呢更新操作,因此会马上执行流程,最终执行10次流程
而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免了大量的无谓计算
双向数据绑定
采用数据劫持,发布者订阅者模式,通过Proxy来劫持元素各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调
- Observer观测需要被观测的对象,其数据对象被Vue递归遍历,包括子属性对象的属性,都加上setter和getter。那么对这个对象的某个值赋值,就会触发setter,就能监听到数据变化
- Compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher,订阅者,是Observer和Compile之间的通信桥梁,主要工作:
- 在自身实例化时,往属性订阅
dep
中添加自己 - 自身需要有一个update()方法
- 等到属性发生变动,dep.notice()通知时,能调用自己的update()方法,并触发Compile中的回调
- 在自身实例化时,往属性订阅
MVVM作为数据绑定的入口,整合Observer,Compile和Watcher,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令(whoseJam:相当于SD中的Rules),利用Watcher搭建起Observer和Compile的通信,最终实现:数据变化->视图更新,视图交互变化->数据更新
模板编译
- 解析:Vue的模板编译器将模板字符串解析成抽象语法树,树中的每个节点描述了模板中的各种元素、指令、属性等信息
- 静态分析:对AST进行静态分析,收集模板中的指令、属性等静态信息
- 优化
- 代码生成:将优化后的AST转换为可执行的渲染函数。渲染函数是一个普通的js函数,它接受数据作为参数,返回虚拟DOM树
slot
默认插槽
//子组件 (假设名为ebutton)
<template>
<div class= 'button'>
<button></button>
<slot>SOME TEXT会显示在此处</slot>
</div>
</template>
//父组件 (引用子组件ebutton)
<template>
<div class= 'app'>
<ebutton>SOME TEXT</ebutton>
</div>
</template>
具名插槽
//子组件 (假设名为ebutton)
<template>
<div class= 'button'>
<button> </button>
<slot name= 'one'> 这就是默认值1</slot>
<slot name='two'> 这就是默认值2 </slot>
<slot name='three'> 这就是默认值3 </slot>
</div>
</template>
//父组件 (引用子组件ebutton)
<template>
<div class= 'app'>
<ebutton>
<template v-slot:one> 这是插入到one插槽的内容 </template>
<template v-slot:two> 这是插入到two插槽的内容 </template>
<template v-slot:three> 这是插入到three插槽的内容 </template>
</ebutton>
</div>
</template>
$nextTick
在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM
- Vue更新DOM是有策略的,不是同步更新
- nextTick以一个函数作为入参
- nextTick后能拿到最新的数据
JS执行机制
同步与异步
- 同步:在主线程上排队执行的任务,只有一个任务执行完毕,才能执行后一个任务
- 异步:不进入主线程,而进入任务队列,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程中执行
机制
- 所有同步任务都在主线程上执行,形成一个执行栈(Execution Context Stack)
- 主线程之外,还存在一个任务队列(Task Queue)。只要异步任务有了运行结果,就在任务队列里面放置一个事件
- 一旦执行栈中的所有同步任务都执行完毕,系统就会读取任务队列,看看里面有哪些事件,那些对应的异步任务,就会结束等待状态,进入执行栈,开始执行
nextTick实现
实现很简单,完全基于语言执行机制实现,直接创建一个异步任务,那么nextTick自然就达到在同步任务后执行的目的
const p = Promise.resolve();
export function nextTick(fn?: () => void): Promise<void> {
return fn ? p.then(fn) : p;
}
数据修改过后不会立即更新的原因:有一个queueJob维护了所有effect,之后还需要对这些effect进行去重,减少计算量,等所有同步任务完成之后,再用nextTick来施加影响
单页面应用和多页面应用
SPA,SinglePage Aplication,只有一个主页,一开始只需要加载一次js,css等相关资源,所有内容都包含在主页面里,对每一个功能模块组件化。单页面应用跳转,就是切换相关组件,仅仅刷新局部资源
MPA,MultiPage Application,指有多个独立页面的应用,每个页面必须重复加载js,css等相关资源,多页面应用的跳转,需要整页资源刷新