Vue源码解析四——初始化

我们最开始的列子是:

<div id="app">{{ a }}</div>
var vm = new Vue({
  el: '#app',
  data: { a: 1 }
})

初始化执行_init方法,该方法进行到vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)的时候, 我们通过mergeOptions方法了解了选项规范化选项合并

在我们的例子中,执行mergeOptions方法的时候父选项是Vue.options, 子选项就是参数{ el: '#app', data: { a: 1} }el使用默认合并策略进行合并,data合并完是一个函数,我们在选项合并中分析过了。看一下vm.$options的打印结果

options.png

接着往下看_init方法中的其他代码

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

这是一个判断分支,当是非生产环境时执行initProxy方法,生产环境时在实例上添加_renderProxy属性。

再看一下initProxy方法中是如何处理的,该函数定义在core/instance/proxy.js文件中:

initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
 }

整体也是一个判断分支,不过不论是哪个分支,最后都在实例上添加了_renderProxy属性。先看一下hasProxy的定义, 在当前文件中:

 const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)

isNative定义在core/util/env.js文件中:

export function isNative (Ctor: any): boolean {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

hasProxy的作用就是判断宿主环境是否支持原生的 Proxy。

如果支持Proxy,则对实例进行代理,代理对象赋给vm._renderProxy; 如果不支持,直接给_renderProxy属性赋值vm

重点看一下支持的情况:

const options = vm.$options
const handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler
vm._renderProxy = new Proxy(vm, handlers)

拦截目标是vm, handles就是拦截行为。如果options.render && options.render._withStripped条件为真,handlers的值就是getHandler,否则是hasHandler_withStripped属性只在测试代码中被设置为true,所以一般是走hasHandler, 它的定义也在该文件中:

const hasHandler = {
    has (target, key) {
      // key属性是否存在在target中
      const has = key in target
      // 是全局变量或者是以_开头的字符串并且没有在$data中,返回true
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      // target没有该属性并且不是全局变量
      if (!has && !isAllowed) {
        // 以 _ 或 $ 开头的属性要使用 `$data.${key}`访问
        if (key in target.$data) warnReservedPrefix(target, key)
        // 属性没定义
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

has可以拦截以下操作:

  • in操作
  • Reflect.has()
  • with

这里有两个警告函数:

  1. warnReservedPrefix, 为了避免和Vue 内置的属性、API 方法冲突,以 _ 或 开头的属性不会被 Vue 实例代理。可以通过`data.${key}`访问
const warnReservedPrefix = (target, key) => {
    warn(
      `Property "${key}" must be accessed with "$data.${key}" because ` +
      'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
      'prevent conflicts with Vue internals' +
      'See: https://vuejs.org/v2/api/#data',
      target
    )
  }

例如

var vm = new Vue({data: {_a: 1}})
vm._a // undefined
vm.$data._a // 1
  1. warnNonPresent. 在渲染的时候用到了${key}属性,但是并没有在实例上定义
const warnNonPresent = (target, key) => {
    warn(
      `Property or method "${key}" is not defined on the instance but ` +
      'referenced during render. Make sure that this property is reactive, ' +
      'either in the data option, or for class-based components, by ' +
      'initializing the property. ' +
      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
      target
    )
  }

比如:

<div>{{ a }}</div>
var vm = new Vue({data: {_a: 1}})

因为没有在data中定义a属性,就会报上面那段警告,${key}就是a

这是为了在开发阶段给我们一个友好的提示,帮助我们快速定位问题。再回到_init函数中看接下来的代码

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

首先在实例对象上添加了_self属性,其值就是实例本身。接着执行了一系列的initxxx方法,我们来一个个看。

初始化之 initLifecycle

initLifecycle函数定义在core/instance/lifecycle.js文件中

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

  // locate first non-abstract parent
  // parent指向当前实例的父组件
  let parent = options.parent
  // 如果有父组件且当前实例不是抽象的
  if (parent && !options.abstract) {
    // while循环查找当前实例的第一个非抽象的父组件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 将vm添加到当前第一个非抽象的父实例中
    parent.$children.push(vm)
  }
  // 当前实例的$parent属性指向第一个非抽象的父实例
  vm.$parent = parent
  // 设置$root实例属性,有父实例就用父实例的$root,没有就指向当前实例
  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
}

首先定义了常量options,它指向当前实例的$options属性。接着定义了parent,它就是vm.$options.parent的值,也就是当前实例的父组件。接着是一个条件判断parent && !options.abstract(如果有父组件且当前实例不是抽象的), 什么是抽象的组件呢?

官方文档中介绍的keep-alive就是一个抽象组件,其中有这个一句话:<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。所以抽象组件的特性就是

  • 不渲染真实DOM
  • 不出现在父组件链中

再来看这段代码:

// parent指向当前实例的父组件
let parent = options.parent
// 如果有父组件且当前实例不是抽象的
if (parent && !options.abstract) {
    // while循环查找当前实例的第一个非抽象的父组件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 将vm添加到当前第一个非抽象的父实例中
    parent.$children.push(vm)
}
// 当前实例的$parent属性指向第一个非抽象的父实例
vm.$parent = parent
// 设置$root实例属性,有父实例就用父实例的$root,没有就指向当前实例
vm.$root = parent ? parent.$root : vm

如果if条件为假,也就是当前实例是抽象的,就会跳过if语句块直接设置实例属性$parent$root,并不会将当前实例添加到父组件的$children中。

如果条件为真,也就是当前实例不是抽象的,就会执行if语句块内的代码。通过while循环查找第一个非抽象的父组件,因为抽象组件不能作为父级。找到父级(第一个非抽象的父组件)之后,将当前组件实例添加到父级的$children中,再接着设置$parent$root实例属性。

在这之后又设置了一些实例属性

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

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

初始化之 initEvents

回到_init函数中,initLifecycle执行完之后,就是initEvents, 它定义在core/instance/events.js文件中

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_hasHookEvent两个属性并初始化。那_events都包含实例的哪些事件呢?我们通过个例子打印看看

<template lang="html">
  <div class="">
    <button-counter @on-change-count="changeCount"></button-counter>
  </div>
</template>

<script>
export default {
  components: {
    'button-counter': {
      data: function () {
        return {
          count: 0
        }
      },
      template: '<button v-on:click="addCount">You clicked me {{ count }} times.</button>',
      mounted () {
        console.log('events', this._events);
      },
      methods: {
        addCount () {
          this.count++;
          this.$emit('on-change-count')
        }
      }
    }
  },
  methods: {
    changeCount () {
      console.log('changeCount');
    }
  }
}
</script>

看一下在子组件中打印_events的结果:

events.png

结果是只有在父组件中使用当前组件时绑定在当前组件上的事件。

// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
    updateComponentListeners(vm, listeners)
}

这段代码应该是初始化父组件添加的事件,没看太懂,先略过。

初始化之 initRender

接下来执行的是initRender函数,该函数存在于core/instance/render.js文件中,函数体如下:

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

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

这么一大段代码一眼望去让人头大,我们一点点来看,不懂的就暂时略过,不能丢了主线^o^

首先在Vue实例上添加了两个属性:_vnode_staticTrees

vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees

它们都被初始化为null, 会在合适的地方进行赋值并使用

接下来是这样一段代码:

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

这段代码是解析处理slot的,内容比较复杂,先略过~

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

这是添加了两个实例属性:_c$createElement, 它们都是对createElement方法的再封装。

我们都写过渲染函数

render: function (createElement) {
    return createElement(
      'h',
      'test'
    )
}

其中createElement也可以替换成this.$createElement

render: function () {
    return this.$createElement(
      'h',
      'test'
    )
}

最后是这样一段代码:

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

这里是在Vue实例上添加了两个属性$attrs$listeners, 可以用来创建高阶组件,官方文档上对这两个属性有说明

这两个对象定义的时候用了defineReactive函数,该函数的作用是定义响应式属性,具体实现在后面看双向数据绑定的时候再看。也就是说这两个属性是响应式的,并且是两个只读属性。

生命周期钩子

剩余的函数调用包括这些:

callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

callHook就是调用钩子函数,在beforeCreatecreated钩子函数调用的中间,调用了initInjectionsinitStateinitProvide三个方法,在initState函数里面又调用了initProps initMethods initData initComputed initWatch, 所以在beforeCreate钩子被调用时,所有与inject、provide props、methods、data、computed 以及 watch 相关的内容都不能使用。在created钩子函数中可以使用,但是不能访问dom,因为这时候还没挂载呢。

我们看一下callHook的实现方式, 该函数存在于lifecycle.js文件中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

函数体的开头和结尾分别调用了pushTargetpopTarget, 这是为了在调用生命周期时禁止收集依赖。

在看选项合并的时候我们知道生命周期钩子最终被处理成了数组,所以handlers如果有值的话就是一个数组。

所以下面判断如果handlers存在的话循环调用每一个函数

if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
}

invokeWithErrorHandling定义在core/util/error.js文件中,里面调用了handlers[i]函数

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (isPromise(res)) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

为了捕获可能出现的错误,使用try...catch包裹了函数调用。如果函数调用返回Promise,用catch捕获错误。

最后是这样一段代码:

if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
}

这是因为我们的生命周期还可以这样调用:

<component
  @hook:beforeCreate="handleChildBeforeCreate"
  @hook:created="handleChildCreated" />

使用@hook:生命周期名来监听组件的生命周期,当有这种方式时,_hasHookEvent属性就被设置为true。

因为initInjectionsinitState都有涉及数据双向绑定的内容,所以接下来我们先看数据双向绑定

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 回忆 首先,render函数中手写h=>h(app),new Vue()实例初始化init()和原来一样。$mou...
    LoveBugs_King阅读 2,206评论 1 2
  • 前言 您将在本文当中了解到,往网页中添加数据,从传统的dom操作过渡到数据层操作,实现同一个目标,两种不同的方式....
    itclanCoder阅读 25,583评论 1 12
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 4,989评论 0 29
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,125评论 0 6
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32