Vue源码解析(2)

学习内容:

异步更新

异步更新的核心在于Wather类,我们打开Wather所在的文件:
src/core/observer/watcher.js

我们需要格外注意的是,将来有人需要调入我们的更新方法的时候,我们需要做什么事情,在这个文件里有一个updata方法。

这个方法什么时候会调用?
是不是我们上一讲的响应化机制的时候,set被执行的时候会执行notify,notify会通知所有的watcher执行updata方法,所以整个的入口是这个方法,我们实验一下。

我们在examples/todomvc/下新建一个test.html文件,文件内容如下

  <script src="../../dist/vue.js"></script>
    <div id="demo">
      <p v-for = "(item,i) of arr" :key='i'>{{item}}</p>
    </div>
    <script>
      new Vue({
        el:'#demo',
        data:{
          arr:['foo','bar']
        },
        mounted(){
          setTimeout(()=> {
            this.arr.push('banba')
          },1000) 
        }
      })
    </script>

用谷歌浏览器打开test.html文件,并打开调试模式,找到src/core/observer/watcher.js
`的文件并打开,在166行代码打上断点,我们可以非常清晰的看到数组更新后的执行流程。

在这里插入图片描述

接下来看看,Wather里的关键性代码:

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)   //{1}
    }
  }

{1}、我们找到了一个非常关键的代码queueWatcher(this),我们在前面调试更改的时候,我们发现我们进行程序更新的时候,它不会正常的改,而是会进入队列机制里头,也就是会走代码queueWatcher这个方法。

接下来我们进入queueWatcher所在的文件:
src/core/observer/scheduler.js代码如下:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  //{1-2} 判断重复
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
    //{2}入队操作
      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)
    }
    // {3} queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //{4}
      nextTick(flushSchedulerQueue)
    }
  }
}

它的核心作用就是Wather的入队操作,我们该怎么理解呢?
我们在一次更新周期中,可能会有很多次更新。那么我们可不可以在这么多的更新的情况下一次刷新到页面中,而不是做页面的数次刷新,浏览器的效率就提升了,用户体验就就会好很多。

我们来看一下它的运行流程:
{1-2}、判断重复,意思如果已经在队列里头了,就不需要进来了。
我们知道data多个属性在一个方法里更新,其实它们只更新一次,因为这些属性关联wather就一个。

{2}入队操作;

{3}接下来尝试执行队列;

{4}执行队列里面的所有任务;

接下来我们来研究nextTick参数里的flushSchedulerQueue函数

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
  //    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.sort((a, b) => a.id - b.id)    //{5}

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {    //{6}
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()     //{7}
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

{5}、对队列进行排序;

{6}、开始尝试执行队列;

{7}、尝试走当前的watcher;
我们需要关注当前的watcher的异步是怎么实现的,接下来我们切换到src/core/observer/watcher.js
里边有一代码:

run () {
    if (this.active) {
      const value = this.get()  //{8}
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)   //{9}
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

{8}、通过get方法可以得到当前关联watcher的值;
接下来就尝试做更新操作,核心是执行{9}的cb函数,这个cbwather一开始创建的时候传递进来的;

接下来看一下整个程序异步的实现机制,我们打开src/core/util/next-tick.js文件,其实整个程序的异步还是依赖与浏览器的异步,看代码:

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
// {9} start
// 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).
// {9} end
//首先会尝试使用微任务去启动我们的队列
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 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {   //{10}
  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' && ( //{11}
  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)) {  //{12}
  // 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.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)  //{13}
  }
}

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
    })
  }
}

{9}、这段注释的意思是,在vue里面,这个异步机制的实现依赖于微任务宏任务,vue首先会尝试微任务的实现方式去启动我们的队列;

我们来看,微任务是如何来启动的?
{10}、通过这个if条件判断,它首选用的是Promise,Promise用的是微任务;
{11}、如果浏览器不支持Promise,就用MutationObserver,因为MutationObserver也是一种微任务;
{12}、如果MutationObserver也不支持,就是尝试用setImmediate,而setImmediate是宏任务;
{13}、最后一种选择就用setTimeout;

所以在Vue的整个周期里面,队列的执行机制其实依赖于这个的,也就是我们的刷新周期是依赖于这个控制的,我们怎么确定一系列的任务是在一个刷新周期呢?就是利用微任务和宏任务;

虚拟DOM
概念

DOM抽象,实际是JS对象,也就是可以利用这个对象去描述真实DOM的所有属性,以及它的结构。

优点
  • 轻量、快速
    因为它是js,它在执行的时候,它的属性的数量比起真实的DOM要少得多,而且它的操作不会造成浏览器的刷新,所以相对来说会快得多,它是通过花费cpu 额外的计算以及内存的空间来换取这样的一个结果。

  • 跨平台
    这个虚拟DOM可以描述界面的具体信息,但是将来我们将它渲染到见面中的时候,其实我们可以有不同的渲染器,这个渲染器的不同,将来直接决定平台的不同。

  • 兼容性
    我们在把虚拟DOM转换成真实DOM的过程中,我们可以进行兼容性的优化,我们可以把一些兼容性的问题消化在这个层级上。

  • 性能改进
    基与vue来说,虚拟DOM又有特殊的意义,在vue早期版本中,是没有虚拟DOM的,wather特别多,页面有多少个绑定就有多少个wather,最后程序大的时候,程序就会受不了,那能不能一个组件一个wather呢?也可以,当一个组件有多个属性,当任何一个属性发生变化的时候,都需要页面去渲染,去刷新,但我们需要知道到底是那个属性发生变化了,这个时候我们需要去比对,这个时候就需要虚拟DOMdiff算法,把新旧DOM进行一个比对,从而得道一个真实的操作,到底变化发生在什么地方。所以说对于vue2来说说是一个性能上的优化。

虚拟DOM的实现过程

虚拟DOM的入口是那里呢?
src/platforms/web/runtime/index.js这是最佳入口点,文件一下代码:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)  //{14}
}

{14}、$mount只做了一件事,就是调了mountComponent方法,我们找到此方法对应的文件src/core/instance/lifecycle.js,找到对应方法的代码:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  //组件更新函数
  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 {
    updateComponent = () => {        //{15}
      //获得一个全新的虚拟dom,并且做更新
      vm._update(vm._render(), hydrating)  //{16}
    }
  }

  // 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 */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

{15}、组件更新函数,它在执行的时候调了两个实例更新的方法,分别是_update()_render(),由于_render()_update()的参数,所以我们先研究_render()函数,因为这个函数是真正用来渲染虚拟DOM的方法。

{16}、这段代码的意思是,我们得到一个虚拟DOM ,并且呢,我们再做更新。

接下啦我们着重来看一下_render()方法,它对应的文件为:
src/core/instance/render.js,我们找到renderMixin这个方法:

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  //返回虚拟dom
  Vue.prototype._render = function (): VNode {    //{17}
    const vm: Component = this
    const { render, _parentVnode } = vm.$options  //{18}

    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
      //{19} 执行render函数湖区vnode
      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
  }
}

{17}、这里核心的方法就是实现了渲染函数,它的核心功能就是返回虚拟DOMVNode;
{18}、首先解构当前选项中,拿到的了render函数;
{19}、执行render函数获取vnode,render函数里的一个参数vm.$createElement,它是当前实例的$createElement方法。

接下来我们来看看$createElement这个方法到底是干嘛的,打开此方法所在的文件:

src/core/instance/render.js,在这里面我们找到initRender方法。

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)      //{20}
  // normalization is always applied for the public version, used in
  // user-written render functions.

  //render函数中h就是它
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)   //{21}

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

{20}、这行代码里,我们看到了_c,这个方法专门是给内部使用的;
{21}、这个方法是专门给用户使用的,我们进入createElement所在方法;
src/core/vdom/create-element.js,以后我们想要研究组件化相关的代码,都要在vdom这个文件里找。

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(                  //{22}
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

{22}、首先我们先拿普通的标签举例。

说了这么多我们来总结一下:

入口mountComponent, src/core/instance/lifecyle.js
做了两件事,一、创建组件更新函数,创建Watcher实例;

 //组件更新函数
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {    //{24}
      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 {
    updateComponent = () => {

      //获得一个全新的虚拟dom,并且做更新
      vm._update(vm._render(), hydrating)   //{25}
    }
  }
  
new Watcher(vm, updateComponent, noop, {       //{23}
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }

{23}、我们发现这个Watcher就是和当前主见挂钩一对一的Watcher,我们会把updateComponent做为参数传入Watcher,将来Watcher更新的时候就是将来我们调updateComponent传入的方法;

{24}、updateComponent方法中,有一个{25} _update方法,此方法还是在src/core/instance/lifecyle.js里:

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {         //{25}
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // {26}
    if (!prevVnode) {    //{27}
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {               //{28}
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)      
     }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

{25}、这个方法的核心思想是什么呢?是一个虚拟节点;
{26}、而虚拟节点掉了一个__patch__方法,这个方法给我们返回一个真实的DOM元素,这个过程分成两个可能性,一是初始化时的新创建,二是组件更新的时候;

接下来我们来研究__patch__,打开它所在的文件:
src/platforms/web/runtime/index.js,从文件目录我们可得知,它已经不是核心代码了,而是和平台相关的代码。

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

这个名字起得这么奇怪,说明它不是让用户调的,而是内部用的,我们进入这个涵数,所在的文件src/platforms/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

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

{27}、这个方法的核心就是调了一个工厂函数createPatchFunction,它的作用就是传入一个配置项,真正的生成一个patch方法;
这时我们又好奇了,createPatchFunction方法里的配置项{ nodeOps, modules }是干嘛的呢?

我们先来看看nodeOps,打开它所在的文件:
src/platforms/web/runtime/node-ops.js

/* @flow */

import { namespaceMap } from 'web/util/index'

export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

export function parentNode (node: Node): ?Node {
  return node.parentNode
}

export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}

export function tagName (node: Element): string {
  return node.tagName
}

export function setTextContent (node: Node, text: string) {
  node.textContent = text
}

export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

通篇下来,我们发现此文件全都的DOM操作,也就是节点操作,它的核心就是让path真实DOM核心的的操作。

我们再来看看modules,有是节点操作还不够,还缺少了一样,那就是属性操作,modules里包含了所有关于节点的属性操作的方法。

接下来我们看看createPatchFunction的代码:
src/core/vdom/patch.js 而次文件是我们研究虚拟DOM的核心

export function createPatchFunction (backend) {  
	 let i, j
    const cbs = {}
    const { modules, nodeOps } = backend   //{28}

   return function patch (oldVnode, vnode, hydrating, removeOnly) {             //{29}
    if (isUndef(vnode)) {            //{30}
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)    //{31}
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {  //{32}
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)      //{33}
    } else {
      // {34}
      const isRealElement = isDef(oldVnode.nodeType)    //{35}
      if (!isRealElement && sameVnode(oldVnode, vnode)) {    //{36}
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)   //{37}
      } else {
        if (isRealElement) {      //{38}
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)    //{39}
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)   //{40}

        // create new node
        createElm(                       //{41}
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
  
  }
  

{28}、createPatchFunction参数里的backend,正式我们传进来的modules,nodeOps的属性和节点操作模块;
{29}、我们看到700行,path方法,这正是我们在前面打补丁时的返回方法,这个打补丁的函数的作用是什么呢?两个做用:

  • 初始化的时候直接生成DOM元素;
  • 如果是在做更新周期时把新就两个Vnode做比对,从而知道怎么做DOM操作;

说白了,path就是把vdom变成dom;

我们来看下张图片pic1,来方便理解:

pic1
 pic1
1、path的同层比较
由于两颗树的比较,算法的复杂度极高,为了降低复杂度,虚拟DOM优化成同层比较,而不会跨层比较。

我们来看看同层比较在代码中是如何表现出来的?
其实核心就做3件事:

  • 节点的增删改;
    我们一pic1为例,新节点有而老节点没有,它就会做一个新增操作,如果老节点有,新节点没有,那它就会做删除。具体的我们来看看代码是如何实现的。

{30}、说明Vnode没有定义,新的不存在;
{31}、老的存在;
从{30}和{31}结合起来说明是删除操作,调invokeDestroyHook(oldVnode)删除;

{32}、老的不存在,就说明就有可能是新增了;
{33}、老的不存在,createElm就开始创建新增了;
{34}、否则就是更新操作了;
{35}、判断老节点的类型,这里有可能是DOM;
{36}、如果不是真实的节点就开始做比对了,运用diff算法;
{37}、执行补丁算法,转换成真实的DOM操作;
{38}、这种情况就是我们初始化的过程,也就是真实DOM
{39}、创建一个空节点;
{40}、拿到真实的DOM,然后删掉;
{41}、拿到老节点后,创建最新的节点;
{42}、用它老爹把老节点删掉,用它最新的节点,追加进去;

pathVnode

什么时候需要path呢?
是在diff发生的地方,我们再来查看src/core/vdom/patch.js文件里的path方法。

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 {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {     //{43}
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

{43}、diff发生的地方,怎么发生比较呢?现在既有老节点又有新节点,而且呢当前的节点还是相同的节点,sameVnode的比较方式很简单,主要有一下几种:

  • 我先看一下这两个标签的名字是否一样,比如说,之前的是"p",现在是"div",不管里面内容是否相同,我都认为是发生了变化,直接做了替换;
  • 在便签上加一个key属性,目的就是让它快速的知道这是一个相同的节点还是不同的节点,如果是相同的节点我们才有必要去比较他们的虚拟DOM

接下来我们来看看它的比较方式

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&    //{44}
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
   //{45}
    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {     //属性更新 {46}
      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)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {   //新老孩子都有   {47}
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        //{48}
        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)    //{49}
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)   //清空 {50}
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
patchVnode

patchVnode方法主要是三种类型的操作:属性更新PROPS、文本更新TEXT、子节点更新REORDER(重排)

那么他们的具体原则是什么呢?

  • 先去判断静态节点 {44};
    因为我们的程序有些节点是静态的,是不会变的,不比较,直接跳过;

  • 新老节点都有children {47};
    新老节点都有children,我们只需比较孩子就可以了

  • 老节点没有子节点(children),新的存在,清空老节点dom的文本,把新的children加进去 {48};

  • 把3反过来,老的不存在,新的存在,删掉所有的孩子{49};

  • 都没children,文本替换{50};

{45}、获取新旧两个节点;

{46}、属性更新,那属性是如何更新的呢?

{47}、新老孩子都存在,

总结

_update的核心作用就是吧虚拟DOM编程真实的DOM;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值