05Vue源码剖析2

Vue 源码剖析2

异步更新队列

Vue 高效的秘诀是一套批量、异步的更新策略

概念解释

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

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

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

体验一下

Vue 中的具体实现

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

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

  • 异步策略:Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 或 setTmmediate,如果执行环境都不支持,则会采用 setTimeout 代替。

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)

$nextTick 把传入回调函数放入 callbacks 队尾

$nextTick 原理执行顺序:

Promise==>MutationObserver==>SetImmediate==>setTimeout

虚拟 DOM

概念

虚拟 DOM(Vitual DOM)是对 DOM 的 JS 抽象表示,他们是 JS 对象,能够描述 DOM 结构和关系。应用的各种状态变化会作用于虚拟 DOM,最终映射到 DOM 上。

image.png

体验虚拟 DOM

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

<!DOCTYPE html>
<html lang="en">

<head></head>

<body>
  <div id="app"></div>
  <!--安装并引⼊snabbdom-->
  <script src="../../node_modules/snabbdom/dist/snabbdom.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>

优点

  1. 虚拟 DOM 轻量、快速:当它们发生变化时通过新旧虚拟 DOM 比对可以得到最小 DOM 操作量,配合异步更新策略减少刷新频率,从而提升性能
patch(vnode, h('div', obj.foo))
  1. 跨平台:将虚拟 dom 更新转换为不同运行时特殊操作实现跨平台
<script src="../../node_modules/snabbdom/dist/snabbdom-style.js"></script>
<script>
    // 增加style模块
    const patch = init([snabbdom_style.default])
    function render() {
      // 添加节点样式描述
      return h('div', {style: {color: 'red' } }, obj.foo)
    }
</script>
  1. 兼容性:还可以加入兼容性代码增强操作的兼容性

必要性

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

整体流程

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

patch

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.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
// patchVnode过程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text ⽂本相同跳过
// 4.p updateChildren
// 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

总结&&思考

const app = new Vue({
  el: '#demo',
  data: { foo: 'ready~~' },
  mounted () {
    // 批量、异步
    // 每次赋值,watcher入队
    // $nextTick()把传入回调函数放入callbacks队尾
    this.foo = Math.random()
    console.log('1:' + this.foo);

    this.foo = Math.random()
    console.log('2:' + this.foo);

    this.foo = Math.random()
    console.log('3:' + this.foo);

    // 异步行为,此时内容没变
    console.log('p1.innerHTML:' + p1.innerHTML) // ready~~

    // [callbacks, fn]
    // Promise.resolve().then(() => {
    //     console.log('promise, p1.innerHTML:' + p1.innerHTML)
    // })

    this.$nextTick(() => {
      // 这里才是最新的值
      console.log('p1.innerHTML:' + p1.innerHTML)
    })
  }
});

面试官:在 Vue 里面执行 mounted 里面的内容,输出结果如何?

面试官:如果把 $nextTick 放在中间位置呢?

面试官:如果把 $nextTick 放在最上面位置呢?

面试官:如果再加上 Promise 呢?

如果...

如果没有如果...

附思维导图:

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270