Vue预习

image.png
image.png
image.png

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都执行了。这个就是异步更新的策略

事件绑定是宏任务

每个事件循环会先执行同步代码,同步代码执行完成之后,清空微任务队列(即执行微任务队列),
事件循环的机制
image.png

事件循环的机制

  • 事件循环Event Loop:浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工作机制。

  • 宏任务Task:代表一个个离散的、独立的工作单元。 浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染 。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种事件如页面加载、输入、网络事件和定时器等。

  • 微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。 如果存在微任务,浏览器会清空微任务之后再重新渲染。 微任务的例子有 Promise 回调函数(async和await是Promise的语法糖)、DOM变化等。体验一下

vue中的具体实现

image.png

vue在堆放watcher,因为每次对数据的更新,不会立刻执行真正的更新,会让watcher进入到Queue的队列中。等到未来某个时刻同步代码都执行完成之后,用异步的方式。如何制造异步的方式就是使用nextTick的方法 执行nextTick的时候,它会将刷新任务队列的函数调用一次。是异步的方式将队列中的全部watcher都执行。这个时候里面的更新函数才是真正执行。这个是vue做的方式,理论上来说就是这样的写法 查看源码

  • 异步:只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

  • 批量:如果同一个 watcher 被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列执行实际工作。

  • 异步策略:Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境都不支持,则会采用 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或者泰罗这些框架其实就是利用这个机制,对于不同的平台做输出

image.png

体验虚拟DOM:

vue中虚拟dom基于snabbdom实现,安装snabbdom并体验

体验vdom可以看一下snabbdom这个库。在vue中底层使用的是snabbdom,在snabbdom基础上做了一些修改

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head></head>
  4. <body>
  5. <div id="app"></div>
  6. <!--安装并引入snabbdom-->
  7. <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom.min.js"></script>
  8. <script>
  9. // 之前编写的响应式函数
  10. function defineReactive(obj, key, val) {
  11. Object.defineProperty(obj, key, {
  12. get() {
  13. return val
  14. },
  15. set(newVal) {
  16. val = newVal
  17. // 通知更新
  18. update()
  19. }
  20. })
  21. }
  22. // 导入patch的工厂init,h是产生vnode的工厂
  23. const {
  24. init,
  25. h
  26. } = snabbdom
  27. // 获取patch函数
  28. const patch = init([])
  29. // 上次vnode,由patch()返回
  30. let vnode;
  31. // 更新函数,将数据操作转换为dom操作,返回新vnode
  32. function update() {
  33. if (!vnode) {
  34. // 初始化,没有上次vnode,传入宿主元素和vnode
  35. vnode = patch(app, render())
  36. } else {
  37. // 更新,传入新旧vnode对比并做更新
  38. vnode = patch(vnode, render())
  39. }
  40. }
  41. // 渲染函数,返回vnode描述dom结构
  42. function render() {
  43. return h('div', obj.foo)
  44. }
  45. // 数据
  46. const obj = {}
  47. // 定义响应式
  48. defineReactive(obj, 'foo', '')
  49. // 赋一个日期作为初始值
  50. obj.foo = new Date().toLocaleTimeString()
  51. // 定时改变数据,更新函数会重新执行
  52. setInterval(() => {
  53. obj.foo = new Date().toLocaleTimeString()
  54. }, 1000);
  55. </script>
  56. </body>
  57. </html>

优点

  • 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,配合异步更新策略减少刷新频率,从而提升性能

    1. patch(vnode, h('div', obj.foo))
  • 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台

    1. <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-
    2. style.js"></script>
    3. <script>
    4. // 增加style模块
    5. const patch = init([snabbdom_style.default])
    6. function render() {
    7. // 添加节点样式描述
    8. return h('div', {
    9. style: {
    10. color: 'red'
    11. }
    12. }, obj.foo)
    13. }
    14. </script>
  • 兼容性:还可以加入兼容性代码增强操作的兼容性(因为他最后输出的时候,可以输出兼容性的代码)

对于vue2来说虚拟dom是格外重要的。因为现在是一个组件一个watcher。在更新的时候,根本不知道hi谁发生变化了。为了解决这个问题,必须有一个vdom的这个概念出来,每次做diff算法,才能真正的得到变化的点

必要性

vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大
型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,
这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。

虚拟dom的生成以及完整的更新流程 重温上一讲的初始化和patch的流程

整体流程

mountComponent() core/instance/lifecycle.js
渲染、更新组件

  1. // 定义更新函数
  2. const updateComponent = () => {
  3. // 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
  4. vm._update(vm._render(), hydrating)
  5. }

_render core/instance/render.js
生成虚拟dom

_update core\instance\lifecycle.js
update负责更新dom,转换vnode为dom

patch() platforms/web/runtime/index.js
patch是在平台特有代码中指定的

  1. Vue.prototype.__patch__ = inBrowser? patch : noop

测试代码,examples\test\04-vdom.html

04文件测试 查看md文档

patch获取

patch是createPatchFunction的返回值,传递nodeOps和modules是web平台特别实现

  1. export const patch: Function = createPatchFunction({ nodeOps, modules })

platforms\web\runtime\node-ops.js
定义各种原生dom基础操作方法

platforms\web\runtime\modules\index.js
modules 定义了属性更新实现

watcher.run() => componentUpdate() => render() => update() => patch()

patch实现

同层比较,深度优先(遍历树的特点)
patch core\vdom\patch.js
首先进行树级别比较,可能有三种情况:增删改。

  • new VNode不存在就删;
  • old VNode不存在就增;
  • 都存在就执行diff执行更新

image.png

patchVnode(基本工作思路)
比较两个VNode,包括三种类型操作: 属性更新、文本更新、子节点更新
具体规则如下:
1. 新老节点 均有children 子节点,则对子节点进行diff操作,调用 updateChildren
2. 如果 新节点有子节点而老节点没有子节点 ,先清空老节点的文本内容,然后为其新增子节点。
3. 当 新节点没有子节点而老节点有子节点 的时候,则移除该节点的所有子节点。
4. 当 新老节点都无子节点 的时候,只是文本的替换。

测试,04-vdom.html

image.png

当foo发生变化时,整个都开始diff算法 先比较#demo节点,向下找孩子节点,找到h1,由于h1还有孩子,所以会比较“虚拟dom”,这个是静态节点,不需要比较,跳出来,开始比较p标签,最后才会到{{foo}}的文本节点,然后直到看到文本节点的更新,这个是预测过程

  1. // patchVnode过程分解
  2. // 1.div#demo updateChildren
  3. // 2.h1 updateChildren
  4. // 3.text 文本相同跳过
  5. // 4.p updateChildren
  6. // 5.text setTextContent

updateChildren
updateChildren主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化,我们看图说话:

image.png

在新老两组VNode节点的左右头尾两侧都有一个变量标记,在 遍历过程中这几个变量都会向中间靠拢
oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。

下面是遍历规则:
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode 两两交叉比较 ,共有 4 种比较方法。

当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满足sameVnode,直接将该VNode节点进行patchVnode即可,不需再遍历就完成了一次循环。如下图,
image.png

如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
image.png

如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前面。
image.png

如果以上情况均不符合,则在old VNode中找与newStartVnode相同的节点,若存在执行patchVnode,同时将elmToMove移动到oldStartIdx对应的DOM的前面。

image.png

当然也有可能newStartVnode在old VNode节点中找不到一致的sameVnode,这个时候会调用createElm创建一个新的DOM节点。
image.png

至此循环结束,但是我们还需要处理剩下的节点。

当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes(批量调用createElm接口)。
image.png

但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有
剩余,需要从文档中删 的节点删除。
image.png

key的作用

  • 判断两个vnode是否相同节点,必要条件之一
  • 工作方式,不添加会怎样
  1. ABCDE
  2. AFBCDE
  3. 4 次更新 1 次创建追加
  4. ABCDE
  5. AFBCDE
  6. BCDE
  7. FBCDE
  8. BCD
  9. FBCD
  10. BC
  11. FBC
  12. B
  13. FB
  14. F
  15. 只剩下F,创建,插入到B前面

key的作用是什么 diff是如何工作的

作业

将vue异步更新过程绘制为思维导图

思考拓展

  • 节点属性是如何更新的
  • key是怎么起作用的

下节课讲组件机制和事件机制或者双向绑定

作业解析

  • patch函数是怎么获取的

    出发点是看一下跨平台这件事在vue中是如何实现的。 入口文件:查看md文档

  • 节点属性是如何更新的

  • 组件化机制是如何实现的
  • 口述diff