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

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

作者: 申_9a33 | 来源:发表于2021-02-17 15:54 被阅读0次

    批量异步更新策略

    • 由于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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/kuppxltx.html