vue源码解析(中)异步更新与虚拟dom

异步更新队列

Vue⾼效的秘诀是⼀套批量异步的更新策略。

概念解释

vue源码解析(中)异步更新与虚拟dom_第1张图片

  • 事件循环Event Loop:浏览器为了协调事件处理、脚本执⾏、⽹络请求和渲染等任务⽽制定的⼯作机制。
  • 微任务:微任务是更⼩的任务,是在当前宏任务执⾏结束后⽴即执⾏的任务。如果存在微任务,浏览器会清空微任务之后再重新渲染。 微任务的例⼦有 Promise 回调函数、DOM变化等。
  • 宏任务Task:代表⼀个个离散的、独⽴的⼯作单元。**浏览器完成⼀个宏任务,在下⼀个宏任务执⾏开始前,会对⻚⾯进⾏重新渲染。**主要包括创建⽂档对象、解析HTML、执⾏主线JS代码以及各种事件如⻚⾯加载、输⼊、⽹络事件和定时器等。

提供一个网站可以体验一下微任务和宏任务:传送门

vue中的具体实现

vue源码解析(中)异步更新与虚拟dom_第2张图片

  • 异步:只要侦听到数据变化,Vue 将开启⼀个队列,并缓冲在同⼀事件循环中发⽣的所有数据变更。
  • 批量:如果同⼀个 watcher 被多次触发,只会被推⼊到队列中⼀次。去重对于避免不必要的计算和 DOM 操作是⾮常重要的。然后,在下⼀个的事件循环“tick”中,Vue 刷新队列执⾏实际⼯作。
  • 异步策略:Vue 在内部对异步队列尝试使⽤原⽣的 Promise.thenMutationObserversetImmediate,如果执⾏环境都不⽀持,则会采⽤ setTimeout 代替。

update() core\observer\watcher.js

dep.notify()之后watcher执⾏更新,执⾏⼊队操作

queueWatcher(watcher) core\observer\scheduler.js

执⾏watcher⼊队操作

nextTick(flushSchedulerQueue) core\util\next-tick.js

nextTick按照特定异步策略执⾏队列操作

虚拟DOM

概念

虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应⽤的各种状态变化会作⽤于虚拟DOM,最终映射到DOM上。
vue源码解析(中)异步更新与虚拟dom_第3张图片

体验虚拟DOM:

vue中虚拟dom基于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="../../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>
    
  • 兼容性:还可以加⼊兼容性代码增强操作的兼容性

必要性

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__是在平台特有代码中指定的

// install platform patch function
// 安装了平台特有的 patch 函数, diff发生的地方
Vue.prototype.__patch__ = inBrowser ? patch : noop

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执⾏更新
    同层比较, 深度优先
    vue源码解析(中)异步更新与虚拟dom_第4张图片
    patchVnode
    ⽐较两个VNode,包括三种类型操作:属性更新、⽂本更新、⼦节点更新
    具体规则如下:

  • 新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren

  • 如果新节点有⼦节点⽽⽼节点没有⼦节点,先清空⽼节点的⽂本内容,然后为其新增⼦节点。

  • 新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该节点的所有⼦节点。

  • 新⽼节点都⽆⼦节点的时候,只是⽂本的替换。

测试

<div id="demo">
   <h1>虚拟DOM</h1>
   <p>{
     {
     foo}}</p>
</div>
// patchVnode过程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text ⽂本相同跳过
// 4.p updateChildren
// 5.text setTextContent

updateChildren

updateChildren主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个VNode的children得出最⼩操作补丁。执⾏⼀个双循环是传统⽅式,vue中针对web场景特点做了特别的算法优化,我们先看图
vue源码解析(中)异步更新与虚拟dom_第5张图片
在新⽼两组VNode节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。
oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。

下⾯是遍历规则:
⾸先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉⽐较,共有4种⽐较⽅法。

当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满⾜sameVnode,直接将该VNode节点进⾏patchVnode即可,不需再遍历就完成了⼀次循环。如下图,
vue源码解析(中)异步更新与虚拟dom_第6张图片
如果oldStartVnode与newEndVnode满⾜sameVnode。说明oldStartVnode已经跑到了oldEndVnode后⾯去了,进⾏patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后⾯。
vue源码解析(中)异步更新与虚拟dom_第7张图片
如果oldEndVnode与newStartVnode满⾜sameVnode,说明oldEndVnode跑到了oldStartVnode的前⾯,进⾏patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前⾯。
vue源码解析(中)异步更新与虚拟dom_第8张图片
如果以上情况均不符合,则在old VNode中找与newStartVnode相同的节点,若存在执⾏patchVnode,同时将elmToMove移动到oldStartIdx对应的DOM的前⾯。
vue源码解析(中)异步更新与虚拟dom_第9张图片
当然也有可能newStartVnode在old VNode节点中找不到⼀致的sameVnode,这个时候会调⽤createElm创建⼀个新的DOM节点。
vue源码解析(中)异步更新与虚拟dom_第10张图片
⾄此循环结束,但是我们还需要处理剩下的节点。

当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上⽐⽼的VNode节点多,需要将剩下的VNode对应的DOM插⼊到真实DOM中,此时调⽤addVnodes(批量调⽤createElm接⼝)。
vue源码解析(中)异步更新与虚拟dom_第11张图片
但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是⽼的节点还有剩余,需要从⽂档中删 的节点删除。
vue源码解析(中)异步更新与虚拟dom_第12张图片
比的是vnode,操作的是真实dom
以上就是diff的全部过程

你可能感兴趣的:(vue源码,vue.js,javascript)