7天深入Vue-批量异步更新策略与虚拟Dom(五)

批量异步更新策略

  • 由于Vue一个组件渲染对应一个Watcher,这个Wathcer 被很多属性绑定,每个属性更改都会触发Watcher的更新函数,所以为了避免重复的更新Wathcher ,Vue使用了批量异步的更新;简单来说,每次属性发生变化会判单异步更新队列中是否存在需要触发的Watcher,不存在才会将Wathcer 追加到队列中。通知在下一次渲染之前,将队列中所有Watcher都触发一次(一般是在微任务当中)。从而实现Vue高效的渲染

1.src\core\observer\watcher.js

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  • 从响应试原理中,我们知道当依赖项发生变化时,会执行Dep.notify,从而会执行Watcher.update.此时并没有直接进行更新,然后将自己放进了queueWatcher队列中

2.src\core\observer\scheduler.js

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
  • 此处将watcher去重后放入队列中,并在nextTick(flushSchedulerQueue) 中调用,一般nextTick会在微任务中,也就是在Js Task清空后,第二次渲染之前调用.

3.src\core\observer\scheduler.js

watcher.run()
  • flushSchedulerQueue中会执行所有Watcher里面的run,从而会调用当初,定义Watcher时,传入的更新方法

4.src\core\util\next-tick.js 看下nextTick的实现

typeof Promise !== 'undefined'

typeof MutationObserver !== 'undefined' 

typeof setImmediate !== 'undefined'

setTimeout(flushCallbacks, 0)

从源码可以看出,依次判断Promise ,MutationObserver , setImmediate ,setTimeout 谁存在就是用谁,Promise 任务调用在下一次渲染之前,setTimeout 发生在下一次渲染之后


虚拟Dom

  • 虚拟Dom本质就是Js对象,他是对DOM的抽象表示

体验虚拟Dom

  • npm i snabbdom
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>
    <script type="module">

        import { init } from './node_modules/snabbdom/build/package/init.js'
        import { h } from './node_modules/snabbdom/build/package/h.js'

        const patch = init([])

        let vnode; // 保存旧的vnode

        const app = document.querySelector("#app")
        const obj = {};

        function defineReactive(obj, key, val) {
            Object.defineProperty(obj, key, {
                get() {
                    console.log('get' + key);
                    return val;
                },
                set(newVal) {
                    if (newVal !== val) {
                        console.log('set' + key + ':' + newVal);
                        val = newVal;
                    }

                    update()
                }
            })
        }


        function update() {
            // app.innerText = obj.foo;
            vnode = patch(vnode, h('div#app', obj.foo))
        }

        defineReactive(obj, 'foo', '')

        // 初始化
        vnode = h('app', { on: { click: () => console.log('click') } }, [
            h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
            ' and this is just normal text',
            h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
        ])
        patch(app, vnode)


        setInterval(() => {
            obj.foo = new Date().toLocaleTimeString()
        }, 1000)
    </script>
</body>

</html>

源码


查看Vue使用虚拟Dom流程

1.src\core\instance\lifecycle.js 已知组件是在实例化Watcher,传入更新函数updateComponent ,从而刷新页面,以及后续响应化页面

  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  • vm._update(vm._render(), hydrating) 是实现组件更新,但是_update_render 不知什么时候挂载的,但是从初始化流程中可知Vue构造函数
    的文件

3. src\core\instance\index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
  • renderMixin 应该就是 _render 的实现

2.src\core\instance\render.js


export function initRender(vm: Component) {
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
export function renderMixin(Vue: Class<Component>) {
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    vnode = render.call(vm._renderProxy, vm.$createElement)
  }
}
  • 内部拿到运行传入的render函数,执行并返回虚拟Dom,也就是我们平时调用的render(h){ return h('div',children) } 里面的h 就是 createElement
  • 步骤1中的 vm._update(vm._render(), hydrating) ,_render已经弄清楚了,返回的是一个虚拟Dom,传入_update

3.继续看src\core\instance\lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
      if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
  }
}

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
}
  • _update 通过传入虚拟Dom,判断是否有上一次的虚拟Dom运行+__patch__,patch就是将虚拟Dom转换为真实Dom
  • 根据上述注释Vue.prototype.__patch__ 全局搜索,找到src\platforms\web\runtime\index.jspatch位置

4.src\platforms\web\runtime\index.js

import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

5.src\platforms\web\runtime\patch.js

import { createPatchFunction } from 'core/vdom/patch'

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

6.src\core\vdom\patch.js

export function createPatchFunction (backend) {
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(oldVnode)) {
        createElm(vnode, insertedVnodeQueue)
    }else{
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
          patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        }else{
            if (isDef(parentElm)) {
                removeVnodes([oldVnode], 0, 0)
            }
        }
    }
    return vnode.elm
  }
}
  • createPatchFunction 会返回一个patch函数
  • 不存在,则createElm
  • 存在虚拟Dom, patchVnode
  • 最后旧节点不存在,就removeVnodes
  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
  }
  • 新老节点均有children子节点,则对子节点进行diff操作,调用upateChildren
  • 如果老节点没有子节点,而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点
  • 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点
  • 当新老节点都无子节点的时候,只是文本替换
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

updateChildren 就是网上写的虚拟Dom,Diff 算法

  • 同层比较,深度优先
  • 主要作用是用一种高效的方式对比新旧两个VNode的children得出最小得操作补丁。头部四个比较,尾部四个比较,头尾四个比较,尾头四个比较,都没找到会执行一个双循环.

Vue虚拟Dom调试

1.src\core\vdom\patch.js

QQ图片20210217175411.png

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

推荐阅读更多精彩内容