天天看點

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

學習目标

  • 了解Vue批量異步更新政策
  • 掌握虛拟DOM和Diff算法

一、異步更新隊列

Vue高效的秘訣是一套批量、異步的更新政策

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

異步任務的類型

JS 單線程基于事件循環:分為

異步和同步

同步執行完,在執行異步中的内容。

  • 宏任務macro task事件:setTimeout、setInterval、setImmediate、I/O、UI rendering、script(整體代碼)
  • 微任務micro task事件:Promises(浏覽器實作的原生Promise)、MutationObserver、process.nextTick

事件循環

  1. 進入腳本執行宏任務,自上而下運作
  2. 遇到同步代碼按順序執行,遇到宏任務放入宏任務隊列,遇到微任務放入微任務隊列
  3. 執行完目前宏任務,執行微任務中執行完并正在等待執行的任務
  4. 執行下一個宏任務,這樣周而複始的執行順序被稱為事件循環

JS 的執行順序和聲明以及引用的順序有關,先聲明的順序先執行,後聲明的順序後執行

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

簡而言之:一次事件循環隻執行處于 Macrotask 隊首的任務,執行完成後,立即執行 Microtask 隊列中的所有任務

體驗一下

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function () {
    console.log('promise1');
  })
  .then(function () {
    console.log('promise2');
  });

console.log('script end');
           
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
async function async1() {
	console.log( 'async1 start' )
	await async2()
	console.log( 'async1 end' )
}

async function async2() {
	console.log( 'async2' )
}

console.log( 'script start' )

setTimeout( function () {
	console.log( 'setTimeout' )
}, 0 )

async1();

new Promise( function ( resolve ) {
	console.log( 'promise1' )
	resolve();
} ).then( function () {
	console.log( 'promise2' )
} )

console.log( 'script end' )

           
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

二、Vue中的具體實作

  • 異步:隻要偵聽到資料變化,Vue将開啟一個隊列,并緩沖在同一個事件循環中發生的所有資料變更。
  • 批量:如果同一個watcher被多次觸發,隻會被推入到隊列中一次。去重對于避免不必要的計算和DOM操作非常重要的。然後,在下一個的事件循環"tick"中,Vue重新整理隊列執行實際工作。
  • 異步政策:Vue在内部隊列嘗試使用原生的

    Promise.then

    MutationObserver

    setImmediate

    ,如果執行環境不支援,則會采用setTimeout(fn,0)代替。

整體流程

update() src\core\observer\watcher.js

dep.js中的notify()之後watcher執行update(),執行入隊操作

src\core\observer\index.js
export function defineReactive (......) {
  ......
  Object.defineProperty(obj, key, {
    ......
    get: function reactiveGetter () {
      ......
      return value
    },
    set: function reactiveSetter (newVal) {
      ......
      //+ 通知更新
      dep.notify()
    }
  })
}
           
src\core\observer\dep.js
export default class Dep {
  ......

  notify () {
    ......
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

           
export default class Watcher {
	......
 	update () {
    	......
      	queueWatcher(this)
    	......
  	}
}
           

queueWatcher() src\core\observer\scheduler.js

執行watcher入隊操作

export function queueWatcher (watcher: Watcher) {
  ......

  + 去重,不存在才入隊
  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)
    }
  }
}

           

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

nextTick按照特定異步政策執行隊列操作

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    + 異步函數
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

           

timerFunc() src\core\util\next-tick.js

啟動微服務

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */


+ 是否支援promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    + 啟動一個微任務
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( + 特殊浏覽器
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else { + 最後的選擇 不得不使用宏任務 setTimeout
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
           

flushCallbacks() src\core\util\next-tick.js

循環所有要執行的回調

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() + 
  }
}
           

案例

03-timerFunc.html

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>異步更新</title>
  <script src="../../dist/vue.js"></script>
</head>

<body>
  <div id="demo">
    <h1>異步更新</h1>
    <p id="p1">{{foo}}</p>
  </div>
  <script>
    const app = new Vue({
      el: "#demo",
      data: {
        foo: "ready"
      },
      mounted() {
        setInterval(() => {
          this.foo = Math.random();//入隊一次
          this.foo = Math.random();//入隊不能入隊,隊列覆寫
          this.foo = Math.random();//不能入隊,已經存在了
          //+ 異步行為,此時内容沒變
          console.log("1111",p1.innerHTML);

          this.$nextTick(() => {
            //+ 這裡才是最新的值
            console.log("2222",p1.innerHTML);
          });
        }, 3000)
      }
    });

  </script>
</body>

</html>
           

watcher中update執行三次,但run僅執行一次

相關API:vm.$nextTick(cb)

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

三、虛拟DOM

概念

虛拟DOM(Virtual DOM)是對DOM的JS抽象表示,他們是JS對象,能夠描述DOM結構和關系。應用的各種狀态變化會作用于虛拟DOM,最終映射到DOM上。

體驗虛拟DOM

kvue
<!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>
    <!-- 虛拟 DOM 庫 -->
    <script src="node_modules/snabbdom/dist/snabbdom.js"></script>
    <script>
        const obj = {};

        const {
            init,
            h
        } = snabbdom;
        //1.擷取patch函數
        const patch = init([]);
        let vnode; //儲存之前舊的虛拟dom

        function defineReactiove(obj, key, val) {
            //對傳入的obj進行通路的攔截
            Object.defineProperty(obj, key, {
                get() {
                    console.log('get ' + key);
                    return val
                },
                set(newValue) {
                    if (newValue !== val) {
                        console.log('set ' + key + ":" + newValue);
                        val = newValue;
                        //更新函數,更新界面
                        update();
                    }
                }
            });
        }

        //使用虛拟dom做更新
        function update() {
            // app.innerText = obj.foo --- dom操作

            //改為虛拟dom
            vnode = patch(vnode, h("div#app", obj.foo)); 
                   //patch參數1:老的vnode,參數2:新的vnode

        }

        defineReactiove(obj, 'foo', new Date().toLocaleTimeString());

        // obj.foo = new Date().toLocaleTimeString();

        //執行初始化
        vnode = patch(app, h("div#app", obj.foo)); //建立虛拟dom
        //參數1:
        //如果是真實dom,就不會去做比較,而是直接轉換為真實dom,替換宿主

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

</html>
           

優點

  • 虛拟DOM輕量、快速:當它們發生變化時通過新舊虛拟DOM比對可以得到最小DOM操作量,進而提升性能
  • 跨平台:将虛拟DOM更新轉換為不同操作平台時特殊操作實作跨平台
const patch = init([snabbdom_style.default]);
vnode = patch(vnode, h("div#app", {style:{color:'res'}}, obj.foo));
           
  • 相容性:還可以加入相容性代碼增強操作的相容性

必要性

vue1.0中有細粒度的資料變化偵測,它是不需要虛拟DOM的,但是細粒度造成了大量開銷,這對于大型項目來說是不可接受的。是以,vue2.0選擇了中等細粒度的解決方案,每一個元件一個watcher執行個體,這樣狀态變化時隻能通知到元件,在通過引入虛拟DOM去進行比對和渲染。

整體流程

資料發生變化:

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

mountComponent() src\core\instance\lifecycle.js

渲染、更新元件

+ 定義更新函數
updateComponent = () => {
	+ 實際調用是在lifecycleMixin中定義的_update和renderMixin中的_render
	vm._update(vm._render(), hydrating)
}

export function mountComponent (......): Component {
  ......

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    + 使用者 $mount() 時,定義updateComponent
    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
  
  + 建立watcher,傳入updateComponent
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ......
}

           

_render src\core\instance\render.js

生成虛拟dom

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

           

_update src\core\instance\lifecycle.js

update負責更新dom,轉換vnode為dom

Vue.prototype._update = function (......) {
	......
	if (!prevVnode) {
		// initial render  
		vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false )
	} else {
		// updates
		vm.$el = vm.__patch__(prevVnode, vnode)
	}
	......
}
           

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

__patch__是在平台特有代碼中指定的

//1+ 指定更新檔方法:傳入的虛拟dom轉換為真實dom
//+ 1.初始化,2.更新
Vue.prototype.__patch__ = inBrowser ? patch : noop
           

patch src\core\vdom\patch.js

patch實作

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

四、diff算法的整體流程

4.1 diff算法簡介

diff算法是一種通過同層的樹節點進行比較的高效算法,避免了對樹進行逐層周遊,是以時間複雜度隻有O(n)。diff算法的很多場景下都有應用,例如在vue虛拟dom渲染成真實dom的新舊VNode節點更新時,就用到了該算法。diff算法有兩個比較顯著的特點:

  1. 比較隻會在同層級進行,不會跨層級比較。
    【 web進階 01vue 】 vue直播課05 Vue源碼剖析02
  2. 在diff比較過程中,循環從兩邊向中間收攏。
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

4.2 diff流程

通過對vue源碼的解讀和執行個體分析來理清楚diff算法的整個流程。下面将整個diff流程分成三步來具體分析:

第一步

vue的虛拟 dom 渲染真實 dom 的過程中首先會對新老VNode的開始和結束位置進行标記:

oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。

//+ 4個指針
let oldStartIdx = 0 //舊節點開始下标
let newStartIdx = 0 //新節點開始下标
let oldEndIdx = oldCh.length - 1 //舊節點結束下标
let newEndIdx = newCh.length - 1 //新節點結束下标

//+ 4個節點
let oldStartVnode = oldCh[0] //舊節點開始vnode
let oldEndVnode = oldCh[oldEndIdx] //舊節點結束vnode
let newStartVnode = newCh[0] //新節點開始vnode
let newEndVnode = newCh[newEndIdx] //新節點結束vnode
           

經過第一步之後,我們初始的新舊 VNode 節點如下圖所示:

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

第二步

标記好節點位置之後,就開始進入到while循環進行中,這裡是diff算法的核心流程,分情況進行了新老節點的比較并移動對應的 VNode 節點。while 循環的退出條件是直到老節點或者新節點的開始位置大于結束位置。

//+ 循環條件 :開始索引不能大于結束索引
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
	......//+ 處理邏輯
}
           

接下來具體介紹 while 循環彙總的處理邏輯

//+ 頭尾指針調整  isUndef 不存在
if (isUndef(oldStartVnode)) {
	oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
	oldEndVnode = oldCh[--oldEndIdx]
	//+ 接下來是頭尾比較的4種情況
}
           

循環過程中對新老 VNode 節點的頭尾進行比較,尋找相同節點,如果有相同節點滿足 sameVnode (可以複用的相同節點)則直接進行 patchVnode (該方法進行節點複用處理),并且根據具體情形,移動新老節點的 VNode 索引,以便進入下一次循環處理,一共有 2 * 2 = 4 中情形。下面根據代碼展開分析:

情形一:當新老 VNode 節點的 start 滿足 sameVnode 時,直接 patchVnode 即可,同時新老 VNode 節點的 開始索引 都加1。

//+ 接下來是頭尾比較的4種情況
else if (sameVnode(oldStartVnode, newStartVnode)) {
	//+ 兩個開頭相同
	patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
	
	//+ 新老開始索引+1
	oldStartVnode = oldCh[++oldStartIdx]
	newStartVnode = newCh[++newStartIdx]
} 
           

情形二:當新老 VNode 節點的 end 滿足 sameVnode 時,直接 patchVnode 即可,同時新老 VNode 節點的 結束索引 都減1。

else if (sameVnode(oldEndVnode, newEndVnode)) {
	//+ 兩個結尾相同
	patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
	
	//+ 新老結束索引-1
	oldEndVnode = oldCh[--oldEndIdx]
	newEndVnode = newCh[--newEndIdx]
}
           

情形三:當老 VNode 節點的 start 和新 VNode 節點的 end 滿足 sameVnode 時,這說明這次資料更新後的 oldStartVnode 已經跑到 oldEndVnode 後面去了。這時候在 patchVnode 後,還需要将目前真實 dom 節點移動到oldEndVnode 的後面,同時 老 VNode節點開始索引 加1 , 新 VNode 節點的結束索引 減1。

else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
	//+ 老的開始和新的結束相同,除了打更新檔之外,還要移動到隊尾
	patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
	canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
	
	//+ 老節點索引+1,新節點索引-1
	oldStartVnode = oldCh[++oldStartIdx]
	newEndVnode = newCh[--newEndIdx]
} 
           

情形四:當老的 VNode 節點的 end 和新 VNode 節點的 start 滿足 sameVnode時,這說明這次資料更新後的oldEndVnode 跑到了 oldStartVnode 的前面去了。這時候在 patchVnode 後,還需要将目前真實 dom 節點移動到oldStartVnode 的前面,同時 老 VNode節點結束索引 減1 , 新 VNode 節點的開始索引 加1。

else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
	//+ 老的結束和新的開始相同,除了patchVnode之外,還要移動到隊首
	patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
	canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
	
	//+ 老的結束索引-1,新的開始索引+1
	oldEndVnode = oldCh[--oldEndIdx]
	newStartVnode = newCh[++newStartIdx]
}  
           

如果都不滿足以上四種情形,那說明沒有相同的節點可以複用,于是則通過查找事先建立好的以舊的 VNode 為key 值,對應 index 序列為 value 值的哈希表。從這個哈希表中找到與 newStartVnode 一緻 key 的舊的 VNode 節點,如果兩者滿足 sameVnode 的條件,在進行 patchVnode的同時會将這個真實 dom 移動到 oldStartVnode 對應的真實 dom 的前面;如果沒有找到,這說明目前索引下的新的 VNode 節點在舊的 VNode 隊列不存在,無法進行節點的複用,name就隻能調用 createElm 建立一個新的 dom 節點放到 目前 newStartIdx 的位置。

else {
	//+ 4種猜想之後沒有找到相同的,不得不開始循環查找
	if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
	//+ 查找在老的孩子數組中的索引
	//+ idxInOld :如果newStartVnode新的VNode節點存在key并且這個key在oldVnode中能找到則傳回這個及诶單的idxInOld(即第幾個節點,下标)
	 
	idxInOld = isDef(newStartVnode.key) 
	? oldKeyToIdx[newStartVnode.key] 
	:findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
	
	if (isUndef(idxInOld)) { // New element
	//+ 沒找到則建立新元素
	//+ newStartVnode沒有key或者是該key沒有在老節點中找到則建立一個新節點
	 
	createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
	} else {
		//+ 找到除了打更新檔,還要移動到對首。
		//+ 擷取同key的老節點
		vnodeToMove = oldCh[idxInOld]
		//+ 如果新VNode與得到的有相同key的節點是同一個VNode則進行patchVnode
		if (sameVnode(vnodeToMove, newStartVnode)) {
			patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
			//+  因為已經patchVnode進去了,是以将這個老節點指派undefined
			oldCh[idxInOld] = undefined
			//+  當有辨別位canMove實可以直接插入oldStartVnode對應的真實Dom節點前面
			canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
		} else {
			// same key but different element. treat as new element
			//+ 當新的VNode與找到的同樣key的VNode不是sameVNode的時候(比如說tag不一樣或者是有不一樣type的input标簽),建立一個新的節點
			createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
		}
	}
	newStartVnode = newCh[++newStartIdx]
}
           

再來看我們的執行個體,第一次循環後,找到舊節點的末尾和新節點的開頭(都是D),于是直接複用 D 節點作為 diff 後建立的第一個真實節點。同時舊節點的 endIndex 移動到 C ,新節點的 startIndex 移動到了 C。

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

緊接着第二次循環,第二次循環後,同樣是舊節點的末尾和新節點的開頭 (都是C),同理,diff後建立 C 的真實節點插入到第一次建立的 B 節點後面。同時舊節點的 endIndex 移動到了 B,新節點的 startIndex 移動到了 E。

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

接下來第三次循環,發現 patchVnode 的4中情形都不符合,于是在舊節點隊列查找目前新節點E,結果發現沒找到,這時候隻能建立新的真實節點 E,插入到第二次建立的 C 節點之後。同時新節點的 startIndex 移動到了 A。舊節點的 startIndex 和 endIndex 都保持不變。

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

第四次循環中,發現新舊節點的開頭(都是A),于是 diff 後建立了 A 的真實節點,插入到前一次建立的 E 節點後面。同時舊節點的 startIndex 移動到了 B ,新節點的 startIndex 移動到了 B。

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

第五次循環中,情形同第四次循環一樣,是以 diif 後建立了 B 真實節點 插入到前一次建立的 A 節點後面。同時舊節點的 startIndex 移動到了 C ,新節點的 startIndex 移動到了 F。

【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

這時候發現新節點的 startIndex 已經大于 endIndex了。不在滿足循環條件。是以結束循環,接下來走後面邏輯。

第三步

當while循環結束後,根據新老節點的數目不同,做響應的節點删除或者添加。若新節點數目大于老節點則需要把多出來的節點建立出來加入到真實 dom 中,反之若老節點資料大于新節點則需要吧多出來的老節點從真實 dom 中删除。至此整個diff過程就已經全部完成了。

//+ 整理工作:必定有數組還有剩下的元素未處理
if (oldStartIdx > oldEndIdx) {
	//+ 老的結束了,這種情況說明新的數組還有剩下的節點
	//+ 全部比較完成以後,發現oldStartIdx > oldEndIdx的話,說明老節點已經周遊完了,新節點比老節點多, 是以這時候多出來的新節點需要一個一個建立出來加入到真實Dom中
	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)
}
           
【 web進階 01vue 】 vue直播課05 Vue源碼剖析02

五、作業

  • patch函數式怎麼擷取的?
  • 節點屬性是如何更新的
  • 元件化機制是如何實作的
  • 口述diff

補充:

async和await

async和await是es6新增的關鍵字,用于把異步變成同步

async在函數定義時使用,用async定義的函數預設傳回一個promise執行個體,可以直接.then。

如果async定義的函數執行傳回的不是一個promise對象,name就會給傳回值包裝成一個promise對象(将傳回值放進promise執行個體的resolve方法中當做參數)

await:要和async一起使用

await 會等待,等它右側的代碼執行完

用法:

1.如果await右側是同步代碼,就會讓右側代碼執行;如果執行的是一個函數,還會把函數的傳回值給到await左邊的變量

let p;
async function f3() {
    p = await 18;
    console.log(p);
}

f3();
console.log(1);
console.log(p);

執行結果是1, undefined , 18

------
f3()
js執行的時候是從右往左執行的,先執行18,然後将await左邊連同下面的代碼都放進微任務

console.log(1); 列印1

console.log(p); 列印undefined

清空微任務

給p指派
console.log(p);  列印18


           

2.如果await右側是一個promise執行個體,或者傳回了promise執行個體,await會等着promise執行個體reslove,并且在執行個體reslove之前,await後面的代碼不執行;并且還會拿到promise在reslove時傳入的值,并且指派給等号左邊變量

變量聲明
let p;

函數聲明
async function f(){
    console.log("f",p)  第三個列印
    return 10;
}

函數聲明
async function f3() {
    console.log("f3--1",p)  第二個列印
     p = await f();  執行右邊函數,await之後的代碼放在微任務
    console.log("f3---2",p);
}

console.log("66666")  第一個列印
f3();
console.log("77777",p) 第四個列印

---------------
同步執行完,清空微任務
p = 10   指派
console.log("f3---2",p); 第五個列印

--------------------------
結果
66666
f3--1 undefined
f     undefined
77777 undefined
f3---2 10
           

3.await會把await下面的代碼程式設計為任務

4.應用

如果我們有a,b,c 三個異步操作,要求 b 依賴 a 的傳回值,c 依賴 b 的傳回值

使用promise鍊式調用

function a(){
	return new Promise((resolve,reject)=>{
		setTimeout(()=>{
			reslove(1)
		},1000);
	});
}
function b(result){
	return new Promise((resolve,reject)=>{
		setTimeout(()=>{
			console.log("b",result)
			reslove(2)
		},2000);
	});
}
function c(){
	return new Promise((resolve,reject)=>{
		setTimeout(()=>{
			console.log("c",result)
			reslove(3)
		},3000);
	});
}

let ap = a();

ap.then(b).then(c);

           

使用async和await

function a() {
    return new Promise(((resolve, reject) => {
        setTimeout(()=>{
            resolve(1);
        },1000)
    }))
}
function b(result) {
    return new Promise(((resolve, reject) => {
        setTimeout(()=>{
            console.log(result);
            resolve(2);
        },2000)
    }))
}
function c(result) {
    return new Promise(((resolve, reject) => {
        setTimeout(()=>{
            console.log(result);
            resolve(3);
        },3000)
    }))
}

async function f(){
	let aP = await a();
	let bP = await b(aP);
	let cP = await c(bP);
}

f();