Vue预习
vue源码剖析(二)
复习
vue的初始化的思维导图
https://www.processon.com/view/link/5da6c108e4b002a6448895c3#map
学习目标
响应式之后如何通知更新
- 理解Vue批量异步更新策略
- 掌握虚拟DOM和Diff算法
异步更新队列
Vue高效的秘诀是一套 批量、异步 的更新策略。(vue主要利用了微任务这个概念)
概念解释
xhr是表示ajax请求
平常写的同步代码是放在Call Stack(调用栈)中的。谁放在前面谁先执行
同步代码中可能会制造一些异步任务。异步任务是有回调函数的,会在未来的某个时刻会被调用。所以要分优先级。这就是事件循环机制的工作方式
宏任务(下图的setTimeout这一行)大而独立
执行一段脚本,非常完整的代码执行,就是一段宏任务。他和setTimeout都是独立的东西。意味着写的同步代码,在写完之后,在另外一个定时器执行之前的间隙中,其实浏览器就会重新刷新了,那么vue就是不想利用这个机制,不想用setTimeout这种宏任务的方式。因为如果使用这种机制的话,会发现如果有很多Dom更新的任务用宏任务去做的话,会导致将任务隔离到下一个task中去了,导致浏览器渲染完成之后,结果没有出来,它还要再等待下一次的间隔,才能看到效果。所以宏任务不是vue首选的方式。今天学习会发现:
vue首选使用微任务来进行异步任务(即使用Promise/mutation observer来制造微任务)
在事件循环中,当从宏任务队列中拿出一个任务时,每一个宏任务执行完成之后,在间隙之前,会清空微任务的队列。有了这个机制,可以利用这个在浏览器渲染之前将DOM都执行了。这个就是异步更新的策略
事件循环中,每一个宏任务执行完成之后,会清空微任务的队列。有了这个机制,可以利用这个在浏览器渲染之前将DOM都执行了。这个就是异步更新的策略
事件绑定是宏任务
每个事件循环会先执行同步代码,同步代码执行完成之后,清空微任务队列(即执行微任务队列),
事件循环的机制
事件循环的机制
事件循环Event Loop:浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工作机制。
宏任务Task:代表一个个离散的、独立的工作单元。 浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染 。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种事件如页面加载、输入、网络事件和定时器等。
微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。 如果存在微任务,浏览器会清空微任务之后再重新渲染。 微任务的例子有 Promise 回调函数(async和await是Promise的语法糖)、DOM变化等。体验一下
vue中的具体实现
vue在堆放watcher,因为每次对数据的更新,不会立刻执行真正的更新,会让watcher进入到Queue的队列中。等到未来某个时刻同步代码都执行完成之后,用异步的方式。如何制造异步的方式就是使用nextTick的方法 执行nextTick的时候,它会将刷新任务队列的函数调用一次。是异步的方式将队列中的全部watcher都执行。这个时候里面的更新函数才是真正执行。这个是vue做的方式,理论上来说就是这样的写法 查看源码
异步:只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
批量:如果同一个 watcher 被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列执行实际工作。
异步策略:Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
或setImmediate
,如果执行环境都不支持,则会采用setTimeout
代替。
执行步骤:dep.notify (通知更新)=> watcher.update() => queueWatcher() (watcher入队操作) => nextTick(flushSchedulerQueue) (排队,flushSchedulerQueue放入callbacks中)=> timerFunc() 启动异步任务 => Promise.resolve(flushCallbacks) (flushCallbacks存放到微任务队列中)
将来真正在刷新的时候,被执行的函数是flushCallbacks
flushSchedulerQueue执行的时候执行的是 =》 watcher.run方法的内部执行了 =》 watcher.getter方法,这个方法是创建watcher时传递入Watcher中的 =》 updateComponent() 组件更新函数,这个函数内部执行 =》 update(_render())(先执行render函数,再执行update函数)
update() core\observer\watcher.js
dep.notify()之后watcher执行更新,执行入队操作
queueWatcher(watcher) core\observer\scheduler.js
执行watcher入队操作
nextTick(flushSchedulerQueue) core\util\next-tick.js
nextTick按照特定异步策略执行队列操作
测试代码:03-timerFunc.html
watcher中update执行三次,但run仅执行一次,且数值变化对dom的影响也不是立竿见影的。
相关API:vm.$nextTick(cb)
虚拟DOM
概念
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们 是JS对象 ,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
好处:轻量,属性较少,只将dom中最核心的值保存了。其他都没有做, 由于虚拟dom会制造中间层的机会,对于JS的操作,最终会直接响应在虚拟dom上的变更,最后通过diff/patch算法将vdom变成真实dom 在这个过程中可以解决很多问题。有了中间层之后,其实是可以跨平台的,未来可以实现不同的patch函数,这些patch函数可以针对不同的平台去做操作。比如:uniapp或者泰罗这些框架其实就是利用这个机制,对于不同的平台做输出
体验虚拟DOM:
vue中虚拟dom基于snabbdom实现,安装snabbdom并体验
体验vdom可以看一下snabbdom这个库。在vue中底层使用的是snabbdom,在snabbdom基础上做了一些修改
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div id="app"></div>
<!--安装并引入snabbdom-->
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom.min.js"></script>
<script>
// 之前编写的响应式函数
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
val = newVal
// 通知更新
update()
}
})
}
// 导入patch的工厂init,h是产生vnode的工厂
const {
init,
h
} = snabbdom
// 获取patch函数
const patch = init([])
// 上次vnode,由patch()返回
let vnode;
// 更新函数,将数据操作转换为dom操作,返回新vnode
function update() {
if (!vnode) {
// 初始化,没有上次vnode,传入宿主元素和vnode
vnode = patch(app, render())
} else {
// 更新,传入新旧vnode对比并做更新
vnode = patch(vnode, render())
}
}
// 渲染函数,返回vnode描述dom结构
function render() {
return h('div', obj.foo)
}
// 数据
const obj = {}
// 定义响应式
defineReactive(obj, 'foo', '')
// 赋一个日期作为初始值
obj.foo = new Date().toLocaleTimeString()
// 定时改变数据,更新函数会重新执行
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
</body>
</html>
优点
虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,配合异步更新策略减少刷新频率,从而提升性能
patch(vnode, h('div', obj.foo))
跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-
style.js"></script>
<script>
// 增加style模块
const patch = init([snabbdom_style.default])
function render() {
// 添加节点样式描述
return h('div', {
style: {
color: 'red'
}
}, obj.foo)
}
</script>
兼容性:还可以加入兼容性代码增强操作的兼容性(因为他最后输出的时候,可以输出兼容性的代码)
对于vue2来说虚拟dom是格外重要的。因为现在是一个组件一个watcher。在更新的时候,根本不知道hi谁发生变化了。为了解决这个问题,必须有一个vdom的这个概念出来,每次做diff算法,才能真正的得到变化的点
必要性
vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大
型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,
这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
虚拟dom的生成以及完整的更新流程 重温上一讲的初始化和patch的流程
整体流程
mountComponent() core/instance/lifecycle.js
渲染、更新组件
// 定义更新函数
const updateComponent = () => {
// 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
_render core/instance/render.js
生成虚拟dom
_update core\instance\lifecycle.js
update负责更新dom,转换vnode为dom
patch() platforms/web/runtime/index.js
patch是在平台特有代码中指定的
Vue.prototype.__patch__ = inBrowser? patch : noop
测试代码,examples\test\04-vdom.html
04文件测试 查看md文档
patch获取
patch是createPatchFunction的返回值,传递nodeOps和modules是web平台特别实现
export const patch: Function = createPatchFunction({ nodeOps, modules })
platforms\web\runtime\node-ops.js
定义各种原生dom基础操作方法
platforms\web\runtime\modules\index.jsmodules
定义了属性更新实现
watcher.run() => componentUpdate() => render() => update() => patch()
patch实现
同层比较,深度优先(遍历树的特点)
patch core\vdom\patch.js
首先进行树级别比较,可能有三种情况:增删改。
- new VNode不存在就删;
- old VNode不存在就增;
- 都存在就执行diff执行更新
patchVnode(基本工作思路)
比较两个VNode,包括三种类型操作: 属性更新、文本更新、子节点更新
具体规则如下:
1. 新老节点 均有children 子节点,则对子节点进行diff操作,调用 updateChildren
2. 如果 新节点有子节点而老节点没有子节点 ,先清空老节点的文本内容,然后为其新增子节点。
3. 当 新节点没有子节点而老节点有子节点 的时候,则移除该节点的所有子节点。
4. 当 新老节点都无子节点 的时候,只是文本的替换。
测试,04-vdom.html
当foo发生变化时,整个都开始diff算法 先比较#demo节点,向下找孩子节点,找到h1,由于h1还有孩子,所以会比较“虚拟dom”,这个是静态节点,不需要比较,跳出来,开始比较p标签,最后才会到{{foo}}的文本节点,然后直到看到文本节点的更新,这个是预测过程
// patchVnode过程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text 文本相同跳过
// 4.p updateChildren
// 5.text setTextContent
updateChildren
updateChildren主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化,我们看图说话:
在新老两组VNode节点的左右头尾两侧都有一个变量标记,在 遍历过程中这几个变量都会向中间靠拢 。
当 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。
下面是遍历规则:
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode 两两交叉比较 ,共有 4 种比较方法。
当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满足sameVnode,直接将该VNode节点进行patchVnode即可,不需再遍历就完成了一次循环。如下图,
如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前面。
如果以上情况均不符合,则在old VNode中找与newStartVnode相同的节点,若存在执行patchVnode,同时将elmToMove移动到oldStartIdx对应的DOM的前面。
当然也有可能newStartVnode在old VNode节点中找不到一致的sameVnode,这个时候会调用createElm创建一个新的DOM节点。
至此循环结束,但是我们还需要处理剩下的节点。
当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes(批量调用createElm接口)。
但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有
剩余,需要从文档中删 的节点删除。
key的作用
- 判断两个vnode是否相同节点,必要条件之一
- 工作方式,不添加会怎样
ABCDE
AFBCDE
4 次更新 1 次创建追加
ABCDE
AFBCDE
BCDE
FBCDE
BCD
FBCD
BC
FBC
B
FB
F
只剩下F,创建,插入到B前面
key的作用是什么 diff是如何工作的
作业
将vue异步更新过程绘制为思维导图
思考拓展
- 节点属性是如何更新的
- key是怎么起作用的
下节课讲组件机制和事件机制或者双向绑定
作业解析
patch函数是怎么获取的
出发点是看一下跨平台这件事在vue中是如何实现的。 入口文件:查看md文档
节点属性是如何更新的
- 组件化机制是如何实现的
- 口述diff