vue源码试读(二)

上一篇按照核心文件的结构,从宏观角度了解了Vue对象的构造过程,这里再从微观角度看看new Vue()的过程中框架都做了些什么。

这里写图片描述

首先来个引入vue的demo。在demo中js:

var demo = new Vue({
  el: '#demo',
  data() {
    return {
      text: 'hello world!'
    }
  }
})

_init方法

当我们在new一个vue对象的时候,初始化一个实例,在instance/init中的Vue构造函数,代码执行了this._init(options),那我们就从_init入手。

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

    let startTag, endTag
    /* istanbul ignore if */
    // 浏览器环境&支持window.performance&非生产环境&配置了performance
    if (process.env.NODE_ENV !== 'production' 
        && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      // 相当于 window.performance.mark(startTag)
      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 {
      // 将options进行合并
      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)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    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) {
      vm.$mount(vm.$options.el)
    }
  }
  • 简单看一下,首先吧当前实例给VM:

    1. 在当前实例中,添加_uid,_isVue属性。
    2. 当非生产环境时,用window.performance标记vue初始化的开始。
    3. 由于我们的demo中,没有手动处理_isComponent,所以这里会进入到else分支,将Vue.options与传入options进行合并。
    4. 为当前实例添加_renderProxy,_self属性。
    5. 初始化生命周期,initLifecycle
    6. 初始化事件,initEvents
    7. 初始化render,initRender
    8. 调用生命周期中的beforeCreate
    9. 初始化注入值 initInjections
    10. 初始化状态 initState
    11. 初始化Provide initProvide
    12. 调用生命周期中的 created
    13. 非生产环境下,标识初始化结束,为当前实例增加_name属性
    14. 根据options传入的el,调用当前实例的$mount

这是实例化时_init方法,接下来结合我们的demo,来细细的看下每一步产生的影响,以及具体调用的方法。

mergeOptions

看到给到的三个参数
{
resolveConstructorOptions(vm.constructor),
options || {},
vm
}

记得在runtime对Vue的变更之后,options变成了:

Vue.options = {
    components: {
        KeepAlive: { name: "keep-alive" …}
        Transition: {name: "transition", props: {…} …}
        TransitionGroup: {props: {…}, beforeMount: ƒ, …}
    },
    directives: {
        model: { componentUpdated: ƒ …}
        show: { bind: ƒ, update: ƒ, unbind: ƒ }
    },
    filters: {},
    _base: ƒ Vue
}
  • 首先将this.constructor传入resolveConstructorOptions中,因为我们的demo中没有进行继承操作,所以在resolveConstructorOptions方法中,没有进入if,直接返回得到的结果,就是在runtime中进行处理后的options选项。而options就是我们在调用new Vue({})时,传入的options。此时,mergeOptions方法变为:
vm.$options = mergeOptions(
    {
        components: {
            KeepAlive: { name: "keep-alive" …}
            Transition: {name: "transition", props: {…} …}
            TransitionGroup: {props: {…}, beforeMount: ƒ, …}
        },
        directives: {
            model: { componentUpdated: ƒ …}
            show: { bind: ƒ, update: ƒ, unbind: ƒ }
        },
        filters: {},
        _base: ƒ Vue
    },
    {
      el: '#demo',
      data: ƒ data()
    },
    vm
)

demo经过mergeOptions之后,变为了如下:
这里写图片描述

在merge完options后,会判断如果是非生产环境时,会进入initProxy方法。

initProxy(代理)

if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}
vm._self = vm

不是很明白,大概就是这里在非生产环境时,对config.keyCodes的一些关键字做了禁止赋值操作。

initLifecycle

初始化生命周期相关属性:

    function initLifecycle (vm) {
  const options = vm.$options
  // 省去部分与本次demo无关代码
  ...
  vm.$parent = undefined
  vm.$root = vm

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

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

关于生命周期钩子函数:生命周期钩子

vm.$mount

还记得在给vue原型添加方法和属性的时候有Vue.prototype.mountVue.prototype.mount

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

  const options = this.$options
  if (!options.render) {
    let template = getOuterHTML(el)
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

这里在覆盖mountmount保留至变量mount中,整个覆盖后的方法是将template转为render函数挂载至vm的options,然后调用调用原有的mount。所以还记得mount来自于哪嘛?那就继续吧runtime/index,方法很简单,调用了生命周期中mountComponent。

function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

new watcher()

vue中每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。这里写图片描述

打开src/core/observer/watcher.js,让我们看看Watcher的构造函数吧。为了清楚的看到Watcher的流程。依旧只保留方法我们需要关注的东西:

 constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    vm._watcher = this
    vm._watchers.push(this)
    this.getter = expOrFn
    this.value = this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    this.cleanupDeps()
    return value
  }
  • 在Watcher的构造函数中,本次传入的updateComponent作为Wather的getter。
  • 在get方法调用时,又通过pushTarget方法,将当前Watcher赋值给Dep.target
  • 调用getter,相当于调用vm._update,先调用vm._render,而这时vm._render,此时会将已经准备好的render函数进调用。
  • render函数中又用到了this.text,所以又会调用text的get方法,从而触发了dep.depend()
    dep.depend()会调回Watcher的addDep,这时Watcher记录了当前dep实例。
  • 继续调用dep.addSub(this),dep又记录了当前Watcher实例,将当前的Watcher存入dep.subs中。
  • 这里顺带提一下本次demo还没有使用的,也就是当this.text发生改变时,会触发Observer中的set方法,从而触发dep.notify()方法来进行update操作。

就这样,Vue的数据响应系统,通过Observer、Watcher、Dep完美的串在了一起。其中在mount里template转换为render,render实际调用时,会经历_render, $createElement, patch, 方法,有兴趣可以自己浏览下’src/core/vdom/’目录下的文件,来了解vue针对虚拟dom的使用。关于vdom:

Vue原理解析之Virtual Dom

终!

版权声明:本文为qq_16756237原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16756237/article/details/82257468