Skip to content

一些原理汇总

整体流程

  1. 创建Vue构造函数:在构造函数上添加一些静态属性方法,并子啊原型上添加一些方法属性。
  2. 实例化Vue形成组件实例:首先会将Vue.options与传入的组件配置进行合并,得到最终的组件options
  3. 继续在原型上添加一些属性方法,如事件方法,生命周期方法等等。与此同时,还会将options里的data,method,props等等全部添加到vue实例上,并对他们进行响应式处理。
  4. 随后开始对模板进行编译。
    1. 首先解析template字符串,形成ast树。
    2. 其次对ast进行优化,将一些静态节点做标记。
    3. ast生成新的代码,这些代码执行后可以生成Vnode
  5. 实例化renderWatcher,开始更新:
    1. 首先执行生成的代码,得到vnode
    2. 然后将vnode与老的vm._vnode进行diff
    3. 最后根据diff结果进行增删节点、更新属性等操作。

diff算法

diff算法处于patch阶段,此时已经生成了新的Vnode。如果新老Vnode处于同层级,并且Vnode都存在,那么需要将新老Vnode进行对比。过程如下:

  • 新节点第一个节点 vs 老节点第一个节点:

    • 如果节点类型相同,调用patchVnode递归更新。新老节点index移动(后续也会移动)。
  • 新节点最后一个节点 vs 老节点最后一个节点:

    • 如果节点类型相同,调用patchVnode递归更新。
  • 新节点最后一个节点 vs 老节点第一个节点:

    • 如果节点类型相同,调用patchVnode递归更新。并且将老Vnode对应的真实节点插入到最后一个新节点的下一个兄弟节点之前。
  • 新节点第一个节点 vs 老节点最后一个节点:

    • 如果节点类型相同,调用patchVnode递归更新。并且将老Vnode对应的真实节点插入到第一个老节点的下一个兄弟节点之前。
  • 如果以上都不符合:

    • 将老节点的key与节点的index形成键值对形式{ key: index }

    • 通过新节点的key去查找是否存在对应的老节点:

      • 如果存在,且类型相同,将对应的老节点(真实节点)插入到第一个老节点之前。
      • 否则直接创建新的真实节点。
    • 最后新节点index向后移动一步。

  • 对比完成后,如果老节点还有剩余,删除对应的真实节点。如果新节点还有没有复用的,创建新的真实节点。

Vnode之所以提升性能,是因为它的计算相较于直接对DOM的操作的开销要小得多:

javascript
不使用虚拟DOM的开销 = 所有真实节点的操作
使用虚拟DOM的开销 = Vnode的创建 + diff开销 + 必要的真实节点操作

$nextTick原理

$nextTick原理比较简单,实际上是将当前的callback收集起来,然后利用微任务/宏任务在下一轮事件循环时执行。

javascript
// timerFunc 其实是 微任务 or 宏任务,这里做了兼容处理
// Promise =》 MutationObserver =》 setImmediate =》 setTimeout
const p = Promise.resolve()
timerFunc = () => {
  // 在下一轮事件循环时,执行所有的 callback
  p.then(flushCallbacks)
  if (isIOS) setTimeout(noop)
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 收集 callback
  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
    })
  }
}

批量更新(queueWatcher)原理

javascript
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  // 当前已经在下一轮事件循环了
  flushing = true
  let watcher, id

  // 1. 父组件 优先于 子组件
  // 2. $watch 优先于 renderWatcher
  // 3. 如果子组件被销毁了,它的watcher会被跳过
  queue.sort((a, b) => a.id - b.id)

  // 遍历执行
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行 watcher
    watcher.run()
  }
}

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果 watcher 已经存在,那么不再添加
  if (has[id] == null) {
    has[id] = true
    // 如果还未启动执行,直接追加 watcher
    if (!flushing) {
      queue.push(watcher)
    } else {
      // 如果在下一轮事件循环中,此时watcher正在执行。
      // 如果过了这个id应该早就执行,但是错过了,
      // 此时会通过 while 循环相当于插队,将它置于最前面了。
      // 下一个就会执行它
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    
    // 如果没有开始执行,那么启动执行
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

批量更新的实质与$nextTick一致,首先收集watchers,然后在下一轮事件循环时批量run