学习目标

异步的批量更新策略概念
  1. 异步:侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
  2. 批量:如果同一个 watcher 被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算 和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列执行实际工作。
  3. 异步策略:Vue 在内部对异步队列尝试使用原生的 Promise.then 、 MutationObserver或 setImmediate ,如果执行环境都不支持,则会采用 setTimeout 代替。
虚拟DOM概念

概念:虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
diff算法的更新,是边diff算法,边更新;一旦发现不一样了就进行更新;而不是计算后统一个更新

体验虚拟DOM

vue中虚拟dom基于snabbdom实现,安装snabbdom并体验
snabbdom的实现与讲解,因为不是重点所以直接写好了,在git上,有详细的注释:https://github.com/speak44/sn...

源码环境

 "name": "vue",  "version": "2.6.11",

异步的批量更新策略源码分析

要了解更新,就需要从defineReactive的方法开始分析,因为每次更新都在里面进行处理

路径:src/core/observer/index.js

export function defineReactive (){   Object.defineProperty(obj, key, {   get(....){......},   set(....){.....   // 通知更新,大小管家都会去执行  dep.notify()   },   }  }
dep.notify() 方法

路径:src/core/observer/dep.js

notify () { .....    // 循环遍历,sunbs 就是所有的watcher实例,   for (let i = 0, l = subs.length; i < l; i++) {   subs\[i\].update() // 去访问所有的watcher实例的update方法  }   }
watcher的update方法

路径:src/core/observer/watcher.js

update () {  /\* istanbul ignore else \*/  //给计算属性用的   if (this.lazy) { // compueted 会定义一个lazy  this.dirty = true   } else if (this.sync) { // 强制同步更新时会配置sync this.run()   } else {   // watcher入队,值发生变化的是时候不是直接更新,而是放在队列里面 queueWatcher(this) // 把自己作为参数传进去   }  }
queueWatcher(this) 方法

路径:src/core/observer/scheduler.js

export function queueWatcher (watcher: Watcher) {   // 去重,如果连续修改三遍,比如   // this.conner=1;   // this.conner=2;    // this.conner=3;   // 连续修改三遍,不会全都都放在watcher队列里面,而是进行一个去重。  // 只会进入队列一次;所以需要把id拿出来,进行判断 。一个组建一个watcher;当前的watcher放在队列一次就可以,不关心怎么改值,只用最后一个的值;值会用最后一次的值,但是队列的id只有一个,上面的更改对应的一个id;过程随便改,用最后一次更新的值。   const id = watcher.id   // 如果队列里面不存在。在放到队列里面  if (has\[id\] == null) {   has\[id\] = true   if (!flushing) {   queue.push(watcher)   } else {.......}   // queue the flush   if (!waiting) { // 如果没有正在执行的watcher就去执   waiting = true   ..........   //异步执行flushSchedulerQueue   nextTick(flushSchedulerQueue)   }  }
nextTick
  • 概念:下一时刻执行的回调,通常用来访问最新的dome状态,当执行cb的时候,所有的watcher都更新完了。
  • nextTick的核心:放到微任务队列里面。
  • 源码解释: 利用微任务机制;就是将cb函数 利用promise放到微任务的队列后面;所有的watcher都更新完了。早去执行
  • 使用: this.$nextTick(()=>{console.log(p.innerHTML)}) // 事件处理机制 同步任务-> 微任务 ->异步任务

路径:src/core/util/next-tick.js

// 将cb函数放回调队列队尾 export function nextTick (cb?: Function, ctx?: Object) {   let _resolve   callbacks.push(() => {   // 回调函数错误处理。 try...catch   // if ...else 的处理只是将回调函数入队,而没有进行执行   if (cb) {   try {   cb.call(ctx)   } catch (e) {   handleError(e, ctx, 'nextTick')   }   } else if (_resolve) {   _resolve(ctx)   }   })   if (!pending) {   pending = true   //异步执行的函数 timerFunc()   }   .......   }
timerFunc()

路径:src/core/util/next-tick.js

// 上个文件的回调函数数组  const callbacks = []    let timerFunc    // 判断 如果当前的环境支持promise,就用promise的方法去安排异步方法去执行 if (typeof Promise !== 'undefined' && isNative(Promise)) {   const p = Promise.resolve() //微任务   timerFunc = () => {   p.then(flushCallbacks)    .......   }   isUsingMicroTask = true  } else if (!isIE && typeof MutationObserver !== 'undefined' && (   .......   timerFunc = () => {   counter = (counter + 1) % 2   textNode.data = String(counter)   }   // 如果不支持promise 用setImmediate  } 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 {   // Fallback to setTimeout.   // 最后的选择是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]()   }  }
flushSchedulerQueue()

路径:src/core/util/next-tick.js

// 将watcher放在一个数组里面返回  const queue: Array<Watcher> = [];  function flushSchedulerQueue () {  currentFlushTimestamp = getNow()  flushing = true  let watcher, id    // Sort queue before flush.  //刷新前对队列排序。  // This ensures that:  //这确保:  // 1. Components are updated from parent to child. (because parent is always  //一。组件从父级更新到子级。(因为父母总是  // created before the child)  //在子对象之前创建)  // 2. A component's user watchers are run before its render watcher (because  // 2.组件的用户观察程序在其呈现观察程序之前运行(因为  // user watchers are created before the render watcher)  //在渲染观察程序之前创建用户观察程序)  // 3. If a component is destroyed during a parent component's watcher run,  //三。如果组件在父组件的监视程序运行期间被破坏,  // its watchers can be skipped.  //它的观察者可以被跳过。  // queue的来源是,上面定义的 const queue: Array<Watcher> = [];由watcher所组成的数组  queue.sort((a, b) => a.id - b.id)    // do not cache length because more watchers might be pushed  // as we run existing watchers  for (index = 0; index < queue.length; index++) {      // 每次拿一个watcher      watcher = queue[index]      if (watcher.before) {          watcher.before()      }      id = watcher.id      has[id] = null      //watcher的操作方法是run方法执行的     watcher.run()     .......  }
run()

路径:src/core/observer/watcher.js

//Scheduler job interface.  // 调度程序作业接口。  // Will be called by the scheduler.  // 将由调度程序调用。 run () {      if (this.active) {      // 核心是执行了get方法。如果当前awtcher是render watcher      // 此get 会是updateConment()     // 由此见的wathcer 来帮助组建更新的,watcher让updateConment重新执行了,之后render函数先执行,然后是update**      const value = this.get() //value是updateConment的返回值          if(.....){......}else(....){....}      }  }

虚拟DOM源码分析

首先了解

patch的实现

首先进行树级别比较,首先是整棵树的根节点开始比较;原则:同层比较,深度优先;可能有三种情况:增删改。
new VNode不存在就删: 将old vnode进行一个删除操作
old VNode不存在就增: 老的不存在就是新增
都存在就执行diff执行更新: 都存在就是更新

patch

路径: src/core/vdom/patch.js

\\ Virtual DOM patching algorithm based on Snabbdom by  基于Snabbdom的虚拟DOM修补算法  .........  \\modified by Evan You (@yyx990803)  \\由Evan You修改(@yyx990803)  .........      return function patch (oldVnode, vnode, hydrating, removeOnly) {   //新的不存在,删除   if (isUndef(vnode)) {   if (isDef(oldVnode)) invokeDestroyHook(oldVnode)   return   }   let isInitialPatch = false   const insertedVnodeQueue = []   // 老的不存在,新增   if (isUndef(oldVnode)) {   // empty mount (likely as component), create new root element   isInitialPatch = true   createElm(vnode, insertedVnodeQueue)   } else {   //两者都存在,进行diff算法 const isRealElement = isDef(oldVnode.nodeType)   if (!isRealElement && sameVnode(oldVnode, vnode)) {   // patch existing root node   //diff算法发生的位置,比较两棵树   //从 patchVnode的传入参数就可以看到,oldVnode, vnode .... ;   // 为什么组件要提供一个根节点,不能并排写;从算法层面来说,不希望出现多根的情况;只能有一个根节点。必须是单根的,才能往下进行递归。 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)   } else {.....}   }  }
patchVnode

比较两个VNode,
包括三种类型操作:

  • 属性更新(style,或者class发生变化;)
  • 文本更新(innerText发生变化)
  • 子节点更新(Children的增删改变化)

具体规则如下:

  1. 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
  2. 如果新节点有子节点而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点。
  3. 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点。
  4. 当新老节点都无子节点的时候,只是文本的替换。
patchVnode()

路径:src/core/vdom/patch.js

// 单节点比较  function patchVnode (   oldVnode,   vnode,   insertedVnodeQueue,   ownerArray,   index,   removeOnly  ) {// 一个节点一个节点的比较   // 获取两个节点孩子节点数组   const oldCh = oldVnode.children // 老-old 孩子节点   const ch = vnode.children // 新-new 孩子节点   // 属性更新   if (isDef(data) && isPatchable(vnode)) {   for (i = 0; i < cbs.update.length; ++i) cbs.update\[i\](oldVnode, vnode)   if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)   }   // 内容比较   // 新节点没文本,可以理解为,有text文本内容 或者有子节点  if (isUndef(vnode.text)) { //没有定义文本,就会有children节点 // 都有孩子   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)   }   }
updateChildren:重排操作

updateChildren主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化

updateChildren

路径:src/core/vdom/patch.js

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {   // 前后四个游标 四个节点 oldStartVnode oldEndVnode newStartVnode newEndVnode    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   ............   // 循环条件: 开始游标必须小于等于结束游标     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]              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                      // 很少情况,就是key相同,元素不同                     createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, 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)   }  }
sameVnode():用来对比两个节点是否相同

路径:src/core/vdom/patch.js

function sameVnode (a, b) {      return (      // key的作用:判断两个vnode是否相同的判断条件之一    // key的工作方式:如果不设置呢? undefined===undefined; 那么就为true;vue就会很做很无用的工作      //当前key是否一样  key相同也是需要比较的          a.key === b.key && (              (                  // 当前标签是否一样                  // 一般情况下,tag  key都是保持一致的,因为是for循环便利出来的                  a.tag === b.tag &&                  // 不能是注释                 a.isComment === b.isComment &&                  // 标签不能变 id class等                isDef(a.data) === isDef(b.data) &&                  // input类型 type还需要一致                 sameInputType(a, b)              ) || (                  isTrue(a.isAsyncPlaceholder) &&                  a.asyncFactory === b.asyncFactory &&                  isUndef(b.asyncFactory.error)              )          )      )  }

学习资料:

  • 思维导图:https://www.processon.com/vie...

总结

关异步更新这块,可以重点看下 nextTick 这部分,因为面试的时候会用到,其实就是将创建的任务放在微任务的队尾,所以不在这里去访问innerHtML可以拿到数据。面试的时候也会问到

关于虚拟dom,可以重点看下patch的实现,以及这个key,我们总是被问到这个key是干嘛的,源码中也有介绍,上面的文档也有说明。其实就是去
判断两个vnode是否相同的判断条件之一,并不是唯一条件,这个要明确了,只有是要加key,一是为了减少vnode在比较时候的无用功,消耗性能;二是在存在倒序的情况的比较来判断。