从源码角度了解vue执行流程(二)

上篇博文已经讲到了执行完import Vue from “vue”,Vue构造函数和实例已经拥有了很多的属性和方法,那么我们接下来继续进行。

import App from “./App.vue”;

这个是引入App.vue作为模版导入。
在这里插入图片描述请记住_Ctor这个对象。

import router from “./router”;

执行src/router/index.js,代码如下

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue")
  }
];

const router = new VueRouter({
  routes
});

export default router;

引入VueRouter依赖,使用Vue.use()方法注册使用VueRouter插件。传入配置的路由数组生成一个路由实例。将路由实例对象对外暴露。

import store from “./store”;

执行src/store/index.js,代码如下

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
});

引入Vuex,使用Vue.use()方法注册使用Vuex插件。同样对外暴露出实例。

Vue.config.productionTip = false;

还记得之前的Vue构造函数嘛,其自身拥有的config属性下的一个属性,大概意思就是,开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句却没有用,反而会增加应用的体积。此外,有些警告检查还有一些小的运行时开销,这在生产环境模式下是可以避免的。

new Vue({ … })

终于开始初始化Vue构造函数了,我们找到Vue构造函数的位置所在。

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

代码很简单,执行原型上面的_init方法,并且把我们的参数传递进去。

紧接着找到定义_init的代码

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

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

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    console.log('--beforeCreate--')
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    console.log('--created--')
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      console.log('开始$mount')
      vm.$mount(vm.$options.el)
    }
  }
}

我们顺序执行代码,首先是合并配置。

if (options && options._isComponent) {
   // optimize internal component instantiation
   // since dynamic options merging is pretty slow, and none of the
   // internal component options needs special treatment.
   initInternalComponent(vm, options)
 } else {
   vm.$options = mergeOptions(
     resolveConstructorOptions(vm.constructor),
     options || {},
     vm
   )
 }

这块的作用就是如果是组件初始化,那么就走

initInternalComponent(vm, options)

否则走

vm.$options = mergeOptions(
   resolveConstructorOptions(vm.constructor),
   options || {},
   vm
 )

执行initLifecycle(vm)

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

让人一眼觉得很难的就是一串if判断了,就是这块。

if (parent && !options.abstract) {
  while (parent.$options.abstract && parent.$parent) {
    parent = parent.$parent
  }
  parent.$children.push(vm)
}

这块的作用是如果当前组件不是抽象组件并且存在父级,那么就通过while循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级,直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent,同时把该实例自身添加进找到的父级的$children属性中。这样就确保了在子组件的$parent属性上能访问到父组件实例,在父组件的$children属性上也能访问子组件的实例,这样就自上而下将父子组件串联了起来。

紧接着往实例身上初始化一些属性。

执行initEvents(vm)

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

实例上新增了_events属性并将其赋值为空对象,用来存储事件。接着,获取当前组件父组件注册的事件赋给listeners,如果listeners不为空,最后通过调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件实例中的_events对象里。

父组件既可以给子组件上绑定自定义事件,也可以绑定浏览器原生事件。这两种事件有着不同的处理时机,浏览器原生事件是由父组件处理,而自定义事件是在子组件初始化的时候由父组件传给子组件,再由子组件注册到实例的事件系统中。

执行initRender(vm)

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)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $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
  console.log('--render--');
  /* 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)
  }
}

这块代码最主要的就是给实例上挂载_c$createElement这两个方法,这两个方法是干嘛的呢?还记得入口文件的render函数嘛,我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要render方法,无论我们是用单文件.vue方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法。

而这里的两个方法,第一种它是被模板编译成的render函数使用,而vm.$createElement是用户手写render方法使用的, 这俩个方法支持的参数相同,并且内部都调用了createElement方法。

最后将$attrs$listeners设置成响应对象。

执行callHook(vm, ‘beforeCreate’)

执行完上述的初始过程之后就开始了beforeCreate钩子。

执行initInjections(vm)

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

这段代码主要是用来初始化inject的,细心的同学可能会产生疑问,provideinject不应该是一起的嘛。在Vue事件系统中,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。那么为什么要先初始化inject呢?设想一下这个情况

使用一个注入的值作为数据入口:

const Child = {
  inject: ['foo'],
  data () {
    return {
      bar: this.foo
    }
  }
}

使用一个注入的值作为一个 property 的默认值:

const Child = {
  inject: ['foo'],
  props: {
    bar: {
      default () {
        return this.foo
      }
    }
  }
}

初始化的dataprops可能会使用到inject,那么理所应当,初始化inject要在初始化initState前。而数据初始化完毕后,再由provide进行数据注入。

provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

执行initState(vm)

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState函数则进行了数据的一系列初始化工作,比如初始化propsmethodsdatacomputedwatch等,并对数据进行劫持,绑定gettersetter,并通过proxy将初始化的property进行代理,让你能通过this关键字进行访问,所以这也就是为什么propsdata等属性不能重名的原因。

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

执行initProvide(vm)

上面也已经解释了为什么初始化provide会在初始化inject和初始化数据之后

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

如果是函数的话,更改其执行的内部this指向,让你在函数内部也能使用this,如果是对象的话,直接是对象传递给实例_provided

执行callHook(vm, ‘created’)

执行完上述的初始化之后,开始执行created钩子函数。这也解释了为什么能在created钩子函数里能获取propsmethodsdata等的原因。

$mount("#app")

执行完上述的代码之后我们来到了这一步,这也是在_init(options)里面。

if (vm.$options.el) {
  console.log('开始$mount')
  vm.$mount(vm.$options.el)
}

这个就是开始挂载了,如果你在options里面写了el属性,那么就是Vue帮你挂载,又或者是你手动挂载,最后的结果都是调用$mount方法。

开始调用$mount,我们来到src/platforms/web/entry-runtime-with-compiler.js这个文件。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      console.log('编译render')
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

这里首先对$mount进行缓存,接着进行了重写。我们大致看下,执行了什么逻辑。

首先,它对 el 做了限制,Vue 不能挂载在 bodyhtml 这样的根节点上。接下来的是很关键的逻辑 —— 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,重要的实情说三遍。无论我们是用单文件.vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,而这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的。也就是说在挂载dom之前,render函数已经生成完毕。

而恰巧,我们初次初始化Vue的时候,传入的就有render这个属性。所以我们继续往下进行。调用mount方法。hydrating这个属性是服务器渲染相关,我们这里不必理会它。

继续执行下去它最终会调用位于src/core/instance/lifecycle.jsmountComponent函数。

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 = () => {
      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 */)
  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
}

代码最核心的部分就是这

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

初始化一个渲染watcher,要了解一点的是Vue主要分四种watcher,负责视图更新的渲染watcher,负责计算属性的computed watcher,以及用户自定义的watch watcher,还有一种就是负责响应式数据更新的普通watcher,而初始化渲染就属于第一种。

watcher

我们来到定义Watcher类的地方,src/core/observer/watcher.js文件。

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      console.log('watcher--lazy')
      this.dirty = true
    } else if (this.sync) {
      console.log('watcher--sync')
      this.run()
    } else {
      console.log('watcher--queueWatcher')
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    console.log('watcher--run', this.cb)
    if (this.active) {
      const value = this.get()
      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)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

大致看下执行逻辑,注意这个属性_watcher。由于v2版本的观测建立在了组件的颗粒度上,所以每个组件的实例都会有这个属性,这里保存着所有视图更新的Dep实例。

if (isRenderWatcher) {
  vm._watcher = this
}

还记得Vue提供的一个方法$forceUpdate嘛,强制视图更新,其内部实现就是遍历_watcher,来达到视图的再次渲染。

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

非常让你容易混淆的是这个

vm._watchers.push(this)

这个_watchers存放着组件实例的所有watcher,包括渲染watcheruser watchercomputed watcher以及普通watcher。这个属性的作用就是为了移除watcher用的。

我们继续往下执行,最后会执行

this.get()

我们看下get函数的逻辑

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

首先我们先看这两个函数pushTargetpopTarget

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

设置了一个唯一全局变量Dep.target,以及存放watcher的栈数组。这个也就保证了在同一时间只有一个watcher被计算。为什么要压栈和出栈呢?设想一个这么个情况,当在父组件进行watcher收集时,这个时候执行到了子组件内部,这个时候就应该开始子组件的依赖收集工作,所以更改Dep.target的指向,使其指向子组件的watcher,在收集完成之后,进行出栈操作,这样也就能保证回退到父组件也能继续进行依赖收集。

紧接着执行

value = this.getter.call(vm, vm);

我们看下这个getter函数,发现其竟然就是updateComponent,为什么呢?我们看这里

new Watcher(vm, updateComponent, noop, { // 这里被当作参数传入
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

// watcher
constructor (
  vm: Component,
   expOrFn: string | Function, // 这里进行接收
   cb: Function,
   options?: ?Object,
   isRenderWatcher?: boolean
) 

// 这里又进行了计算
if (typeof expOrFn === 'function') {
  this.getter = expOrFn
 } else {
   this.getter = parsePath(expOrFn)
   if (!this.getter) {
     this.getter = noop
     process.env.NODE_ENV !== 'production' && warn(
       `Failed watching path: "${expOrFn}" ` +
       'Watcher only accepts simple dot-delimited paths. ' +
       'For full control, use a function instead.',
       vm
     )
   }
 }

所以上面的getter函数就是updateComponent函数。也就是执行这句

vm._update(vm._render(), hydrating)

执行了这个vm._render(),这个方法最终最调用_createElement方法生成虚拟dom,这个时候就会对数据进行访问,触发每个响应数据的getter,我们康康defineReactive定义响应式数据的地方。

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val;
  if (Dep.target) {
    console.log('getter', value)
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
},

存在全局唯一的Dep.target,就会执行dep.depend(),我们再来看看定义Dep类的地方

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

这个时候,执行这句Dep.target.addDep(this),就相当于执行watcher类里面的这句,为什么呢?因为这个时候Dep.target就是当前的实例watcher,所以就相当于实例watcher对象,调用了自身的方法addDep,把当前的this也就是Dep类的实例对象传进去,最后执行dep.addSub(this)

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

而这句话也就是执行了Dep类里面的这个函数

addSub (sub: Watcher) {
  this.subs.push(sub)
}

把当前的watcher实例放进了subs数组里。随着组件内的响应式数据的触发,这样也就完成了依赖收集工作,每个响应式数据都维护自身的Dep类实例,而Dep类实例里面的subs数组放着对这个响应式数据的订阅。而执行这句traverse(value)就是为了进行子属性递归收集,最后收集完成之后进行依赖清空

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}

至于为什么会被清空,就是为了在用户使用不到的时候进行了派发更新,什么是使用不到呢?比如你在组件内使用v-if指令隐藏了一部分dom,而这部分dom恰巧有响应式数据,如果不去清除,随着组件进行重新渲染,毫无疑问是一种性能浪费,这属于Vue的一种性能优化。

执行完渲染虚拟dom之后,开始执行vm._update()进行dom更新工作。而_update实际上调用的是这块,位于src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  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.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 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.
}

这里会执行__patch__进行diff,也就是常说的diff算法进行比对。由于我们是初始化渲染,所以oldVnode为空,所以它会走这行代码

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

然后执行patch函数时,也就是位于src/core/vdom/patch.js文件的这块

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

由于我们是初次渲染,所以里面的diff比较,我们通通跳过,直接到这里

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

createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中,当渲染到子组件时,则通过createComponent函数来创建子组件,所以渲染的时候是先子后父的,这也解释了为什么是子组件mounted之后才是父组件的mounted。最后渲染完毕,插入到body上。

至此,我们的Vue页面已经全部渲染完毕了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值