VUE源码学习第五篇--new Vue都干了啥(初始化)

一、第三部分:初始化相关功能

我们继续上一章,第三部分是对各类功能的初始化。这个章节内容较多,大家还需有耐心。

   /**
     *第三部分,初始化相关功能
     */
    /* 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')

1、initProxy

initProxy的作用是设置vm._renderProxy。

首先判断当前是否为开发环境,如是,则调用initProxy,否则直接将vm对象赋值给vm._renderProxy.Proxy是ES6新增特性,大家可以先了解下相关知识。我们先来看下initProxy。

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

这段主要是给vm._renderProxy设置一个代理,进行一些拦截,不符合的策略(对象上不存在的属性,或者映射表中没有的),提示告警,至于vm._renderProxy是什么,我们后面详细描述。

2、initLifecycle

initLifecycle的作用是初始化一系列变量

先看下前一段代码:

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

源码中注释翻译成中文的意思是,定位到第一个非抽象的父组件。要了解非抽象组件,那我们先了解下抽象组件,官方定义是:"自身不会渲染一个 DOM 元素,也不会出现在父组件链中",keep-alive以及transition都是这类组件,说白了,就是不对其包裹的内容进行渲染,保留其状态。不是抽象组件的组件都是非抽象组件。

    parent不是为undefined,并且标记位非抽象,通过while循环父组件链,找出第一个非抽象的组件,并赋值为parent,该parent的子组件数据添加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

主要是对vm对象的相关属性初始化,可以查阅VUE官方文档实例属性进行了解。

3、initEvents

initEvents的作用存储父组件绑定当前子组件的事件,保存到vm._events中。

 Vue中的事件分为两种,一种是DOM原始的事件,比如click,focus等,另一种就是自定义事件,对事件机制不了解的可以看VUE探索第五篇-组件(Component),下面我们来用具体的实例描述事件如何存储到vm._events中的。

<child @childclick="getMsg" v-on:click.native="postMsg" @hook:created="parentCreate"></child>
...
var childComponent =  Vue.component('child',{
     template:"<div><p @click='showMsg'>{{childMsg}}</p></div>",
     data:function(){
      return {
         childMsg:"Hello child"
      }
     },
     created:function(){
      console.log("子组件created");
     },
     props:['msg1','msg2'],
     methods:{
        showMsg:function(){
          this.$emit('childclick', '子组件抛出去吧');
        }
     }
  });

var vm = new Vue({
    el:"#app",
    components:{
      childComponent
    },
    created () {
      console.log('组件 created')
    },
   
	data:{
       msg:'tttt'
	},
    methods:{
      getMsg:function(data){
          console.log(data);
       },
       postMsg () {
          console.log('父组件中响应的')
       },
       parentCreate(){
        console.log("父组件 created");
       }
    }
})

child组件上绑定了三个事件,分别是childclick(自定义),click(dom原生,需要加上.native),以及钩子事件(@hook:created)。

看下initEvent源码,代码不多,寥寥几行。

function initEvents (vm) {
  //初始化vm._events对象
  vm._events = Object.create(null);
  //是否存在hook:钩子事件
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}

第一行,初始化一个vm._events对象。

第二行,定义了一个标识位_hasHookEvent,用来表示是否有hook:这种钩子事件,hook:可以从父组件上定义子组件的生命周期钩子。

第三行,获取_parentListeners,表示父组件对子组件的监听。这个属性在父组件vm上是没有的,在子组件childComponent对象上的值是

_parentListeners存储了其中的两个事件,有同学在这里可能有两个疑惑,这个_parentListeners是什么时候赋值的?为何没有看到原始dom的事件(click)?

第一个问题涉及到到组件的创建流程,后续专门阐述。第二个问题,需要理解下“父组件绑定当前组件(子组件)的事件”,原始dom事件显然不属于这个范畴,实际上它有单独的处理流程,这里不展开讲。

至此我们已经获取到了事件对象,下面调用updateComponentListeners完成对事件的保存。

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
  target = undefined
}

该方法有三个入参,vm表示childComponent对象本身,listeners即_parentListeners对象,oldListeners为undefined。

第一行,暂时缓存vm对当前target对象。当执行流程完成后在第三行重置。target在后面的remove中用的着。

主要看下updateListeners方法,

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  //定义变量
  let name, def, cur, old, event
  //循环遍历事件监听对象
  for (name in on) {
    //为def,cur赋值事件对象
    def = cur = on[name]
    //oldOn为undefined,故old也是undefined
    old = oldOn[name]
    //对name进行.passive、.capture和.once的修饰符判(&,!,~)
    event = normalizeEvent(name)
    /* istanbul ignore if */
    //对weex框架的支持,暂不研究
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    //判断cur是否为undefined(不存在),即报错
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {//判断old对象是否为undefined,当old不存在时执行
      if (isUndef(cur.fns)) {//判断新对象的fns对象是否为undefined
        //创建事件函数调用器
        cur = on[name] = createFnInvoker(cur)
      }
      //添加新的事件处理
      add(event.name, cur, event.once, event.capture, event.passive, event.params)
    } else if (cur !== old) {//如果新对象与老对象不一致,则用新的覆盖
      old.fns = cur
      on[name] = old
    }
  }
  //遍历oldOn事件,如果在当前事件中找不到,就说明是待老化的,执行删除操作
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

该方法是核心部分,主要作用监听事件的变化,对于新增的事件调用add方法保存,对于老化的事件调用remove方法删除。

具体细节请看相关的注释。这里我们主要看下add和remove方法。

function add (event, fn, once) {
  if (once) {//一次性事件
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

这是我们看到$once,$on方法,对这两个方法不了解的可以看下VUE API。这些方法封装在eventsMixin中(该方法我们在第二章节全局API初始化时介绍过)

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {//如果event是数组,则递归调用
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    } else {//将新增的事件push到vm._events数组中
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      //有:hook钩子事件则标注为true
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

该方法主要就是将事件push到vm._events[]中。继续看下$once

Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {//删除后执行
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

这里用了一个讨巧的方式,重新封装了on方法,调用$off方法删除事件,再执行一次。

再看下remove方法。

function remove (event, fn) {
  target.$off(event, fn)
}

调用了target.$off方法删除,还记得的target吧,就是当前的组件对象。

Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    //不传入参数,则全部删除,vm._events置为空
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    //对于event数组,则递归调用
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    //以下是对单个事件进行处理.
    //查找该事件的监听数组
    const cbs = vm._events[event]
    //事件监听如不存在,则直接返回实例
    if (!cbs) {
      return vm
    }
    //如果监听事件为空,则置空该事件的监听数组
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    //如果指定了监听事件,循环找出该事件,从监听数组中删除
    if (fn) {
      // specific handler
      let cb
      let i = cbs.length
      while (i--) {
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1)
          break
        }
      }
    }
    return vm
  }

看下面的vm._events的数据结构,这段代码就不难理解,将对应的监听事件从数组中删除。

至此,initEvents分析完成,正如开头所讲,它的作用是存储父组件绑定当前子组件的事件,打印下vm._events结果:

保存好后接下来就是触发事件,即$emit,在本篇callhook章节会调用。

4、initRender

initRender 定义vm._c和 vm.$createElement方法

Vue有两种方法编写html内容,一种就是我们常用的template,另一种就是render。render在有些情况下会比template更加简洁,不熟悉的同学可以先了解下render相关知识,我们将上面的例子中template改写成render函数。

render:function(createElement){
            return createElement('div',
                                   [createElement('p',
                                                  {
                                                    on:{
                                                    click:this.showMsg
                                                    }
                                                  },
                                                  this.childMsg
                                                  )
                                   ]
                                )
                                   
     },

      无论是template,还是我们的手写的render,最后都会将描述成VDOM,VDOM的内容我们后续专门章节描述。

接下来我们看下initRender源码。

export function initRender (vm: Component) {
  //初始化_vnode,staticTrees属性
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  //获取父vnode
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  //将 dom 节点映射成 Slot
  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
  //定义._c方法,对template进行render处理的方法
  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.
  //.$createElement方法对render处理的方法
  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
  // $attrs & $listeners这个属性进行数据监听
  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)
  }
}

    首先对_vnode与_staticTrees进行初始化。接着对$slots,$scopedSlots赋值。vm._c与vm.$createElement分别是对template和render函数处理的方法,这里只定义,现阶段做个大概的了解即可,相关的内容我们在编译的章节仔细详解。

 调用defineReactive实现对$attrs与$listeners的监听,该方法在后面响应式原理章节中详解。

5、callHook(vm, 'beforeCreate')

我们先来讲下callHook方法,负责调用生命周期的钩子相关方法。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  //记录当前watch的实例
  pushTarget()
  //获取相应钩子的事件处理方法数组
  const handlers = vm.$options[hook]
  //执行对应的事件方法
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  //触发@hook:定义的钩子方法
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  //释放当前的watch实例
  popTarget()
}

这部分代码比较清晰,pushTarget,popTarget是数据响应的相关方法,后续再详解。

   callHook(vm, 'beforeCreate')就是从vm.$options中拿到beforeCreate的钩子方法数组(在上一篇我们专门讲了vm.$options,将定义Vue对象时相关的方法都保存在该对象中,其中就包括自定义的生命周期的方法),传入vm对象作为上下文(this),循环执行。

  接下来判断vm._hasHookEvent是否为true,这就是initEvents中定义的标识,在该章节实例中的子组件定义了

@hook:created="parentCreate"

所以就会触发该方法,我们看下$emit方法,该方法在eventsMixin中有定义。

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    ...
    //获取该事件对应的事件处理器数组
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      //循环执行
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          cbs[i].apply(vm, args)
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
  }

此时,仅做了一些方法的初始化,还没有涉及到state,provide等属性,所以在beforeCreate中调用el,data都是undefined。以VUE源码学习第三篇--new Vue都干了啥(概述)的样例为例:

 beforeCreate:function(){
     console.log("beforeCreate");
     console.log("this el:"+this.$el);//undefined
     console.log("this data:"+this.$data);//undefined
      console.log("this msg:"+this.msg);//undefined
   }

6、initInjections和initProvide

initInjections获取inject定义的各属性值并增加对该属性的监听。

initProvide设置 vm._provided值,以便子组件逐级查找。

 inject与provide配合实现了属性的多级传递,官方的说法"以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效"。具体的用法大家可参考下官网API

export function initInjections (vm: Component) {
  //将inject保存到result字面量对象对象中
  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)
  }
}

前面我们分析过normalizeInject方法,主要是将inject规整后设置给vm.$options。我们继续看下initInject,该方法主要分两部分:

1、resolveInject方法中,通过inject属性查到到对应父级组件上的provide的值,并保存到result字面量对象中,格式如下:{foo: "this is foo"}。

2、增加对该属性的监听。

我们来分析下第一部分(resolveInject),第二部分关于属性的监听,我们会在后面响应式原理一起说。

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    //创建一个纯对象
    const result = Object.create(null)
    //判断是否支持ES6的特性Symbol和Reflect,支持则采用Reflect.ownKeys,否则采用Object.keys
    const keys = hasSymbol
      ? Reflect.ownKeys(inject).filter(key => {
        /* istanbul ignore next */
        //筛选可枚举的属性
        return Object.getOwnPropertyDescriptor(inject, key).enumerable
      })
      : Object.keys(inject)
    //对key数组进行循环
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]

      const provideKey = inject[key].from
      let source = vm
      //向上查找父级组件对象的provide上包含key的对象,并保存到result中
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {//如果没有父级组件匹配上,则判断是否有default值 
        if ('default' in inject[key]) {
          //将default值赋值给属性对象
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

这部分的代码逻辑也比较清晰:

1、过滤出inject定义的属性。

2、逐级向上查找属性在父级组件对象上的provide定义值,并保存到result中。

3、如果没有查到,就启用default值,default值如果都没有,不好意思,给出提示告警。

      这里有个知识点,关于Reflect.ownKeys和Object.keys的区别。Object.keys是表示给定对象的所有可枚举属性的字符串数组( Object.getOwnPropertyNames中可枚举的属性);Reflect.ownKeys不含包括Object.keys的属性,还包括了 Symbol 属性。官方文档中有以下说明"在该对象中(inject)你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作"。

initProvide比较简单,大家看下代码即可明白。

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

7、initState

initState从源码中可以很清楚看到是对prop,method,data,computed,watch的初始化。

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

接下来我们一个一个看。

(1)initProps

initProps获取props属性的值,并增加监听。

前一章节我们讲到通过normalizeProps方法,将props进行了规整,该实例规整后vm.$options.props格式如下:

这个例子中包含了两个属性对象likes,title。initProps方法将会对这个对象进行再次加工。

function initProps (vm: Component, propsOptions: Object) {
  //获取父组件的propsData,该属性主要是用于测试
  const propsData = vm.$options.propsData || {}
  //存放最终的props对象到vm._props
  const props = vm._props = {}
  //将props的key缓存到vm.$options._propKeys,为了后续更新时的快速的查找,而不需要动态的枚举
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  //循环处理props的属性对象
  for (const key in propsOptions) {
    //存储key
    keys.push(key)
    //1、校验props,包括对类型的校验以及产生最后的属性值
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (vm.$parent && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      //2、对props的各属性进行赋值,并进行监听
      defineReactive(props, key, value)
    }

    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    //3、代理到vm对象上,使用vm.xx或者组件中this.xx直接访问props属性
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

首先定义了属性对象以及key存放的位置,为vm._props以及vm.$options._propKeys,然后对props中的每个属性对象进行循环,在该循环中,做了如下的处理:

1、对props对象校验,并获取属性对象的值,比如like的值是“this is like phone”。当前没有的话就是undefined。

2、调用defineReactive对props(vm._props)赋值,并添加属性监听(在响应式原理部分将详细讲解)

3、调用proxy将props的属性对象代理到vm上,使用vm.xx或者组件中this.xx直接访问props属性。

执行完成后,上面的例子最后生成的vm._props对象为:

(2)initMethods

是对methods的初始化,将methods定义的方法绑定到vm上,我们看下源码:

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      //判断props中是否已经定义了该方法,如果是,则提示已经定义,可见props的优先级要大于method中定义
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      //判断vm对象中是否已经定了该方法,如是则提示冲突,尽量不要以_ 或者 $打头,这个是vue自带属性和方法的命名规范,避免冲突
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    //把每个methods[key]绑定到vm上,并将methods[key]的this指向vm
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

首先校验了props以及vm已有的方法是否冲突,然后将方法绑定到vm对象上。比如我们定义了如下的method的

 methods:{
    postMsg () {
      console.log('父组件中响应的')
    }
}

执行完成后,会绑定到vm对象上

(3) initData

对data的初始化,主要是对定义的属性建立钩子函数,实现数据的绑定。代码如下:

function initData (vm: Component) {
  let data = vm.$options.data
  //保存data到vm._data中,判断data是否是function类型,对于组件必须是function
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    //对象必须是纯粹的对象
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  //代理到vm实例上
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    //确保data的属性不与method与props命名冲突
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {//判断是否以 _ 或 $ 开头,如果是则认为是系统的属性,则不会被代理
      proxy(vm, `_data`, key)//代理到vm对象上
    }
  }
  // observe data
  //运行 observe 函数建立起 Observer 和钩子函数
  observe(data, true /* asRootData */)
}

首先从vm.$options.data获取data对象,并根据是否是function类型(组件必须是该类型)进行处理,转化为统一的字面量对象。接下来,与props类似,代理到vm对象上,最重要的是,调用observe建立Observer和劫持钩子函数,后续数据的变化都会触发其watcher对象,实现数据的绑定(这个方法在响应式原理章节再表)

对于组件,官方文档说话:"data必须申明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象”。

我们看怎么实现的,答案在getData中。

return data.call(vm, vm)

将vm对象设置为上下文,即this,所以,data中的this就是创建的新实例对象。

(4) initComputed

computed是vue重要的属性,与method相比,计算属性是基于它们的依赖进行缓存的,即计算属性中的涉及到的变量不变化的情况下,采用缓存的值,只在相关依赖发生改变时它们才会重新求值。我们来看下是如何实现的。

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    //1、获取getter方法
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      //2、创建watcher订阅类
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      //3、绑定到vm对象上,计算属性值
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

循环computed中每个属性,对属性进行如下的处理:

1、获取getter方法,如果没有显性定义,则默认是该函数。

2、创建watcher的订阅类(响应式原理章节再表)

3、绑定到vm对象上,计算属性值。

继续看defineComputed定义。

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    ....
  }
  //绑定到vm对象上,并对计算属性的getter和setter进行劫持
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

通过Object.defineProperty实现对计算属性的getter和setter方法的劫持。调用getter方法时,实际调用createComputedGetter方法。

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //依赖收集,将订阅类添加到
      watcher.depend()
      //返回值
      return watcher.evaluate()
    }
  }
}

将定义的watcher类,加入到发布器dep中,实现依赖收集,当依赖变量发生变化,触发计算属性的重新计算。并返回其计算后的值。

对于这部分知识,目前阶段仅做了解,在后面的响应式原理章节中,我们会重点分析。

(5) initWatch

watch对属性的观察,当属性有变化时,会回调定义的函数,具体的实现方式可以参阅watch官方文档

function initWatch (vm: Component, watch: Object) {
  //循环watch中的属性
  for (const key in watch) {
    const handler = watch[key]
    //包含多个handle,则循环创建watcher
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        //创建watcher对象
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

为每个handle回调函数创建一个watcher对象。

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  //如果是options对象,则取出handler对象
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  //如果是方法名,则获取方法实体
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  //创建watch对象
  return vm.$watch(expOrFn, handler, options)
}

根据不同的表达,进一步获取handle方法,调用vm.&watch创建

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    //如果cb还是字面量对象,则继续调用createWatcher
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    //创建watcher对象,进行监听
    const watcher = new Watcher(vm, expOrFn, cb, options)
    //设置了immediate,则立即调用
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

我们看到new Watcher对象,将这个对象添加到属性(expOrFn)的Dep中,每次对应的属性发送变化,都会触发回调函数(cb)执行。

同样,对于这 部分知识,当前阶段大家仅做了解即可,后面我们会专门分析。

8、callHook(vm, 'created')

调用create生命周期钩子方法,同5。此时已经初始化了相关属性,所以能获取到值,但是el节点还没有挂载,依然为undefined。以VUE源码学习第三篇--new Vue都干了啥(概述)的样例为例:

created:function(){
    console.log("created");
     console.log("this el:"+this.$el);//undefined
     console.log("this data:"+this.$data);//[object Object]
     console.log("this msg:"+this.msg);//tttt
   }

二、总结

本章节内容较多,主要是初始化相关的工作,我们再来回顾下这张导图。

发布了33 篇原创文章 · 获赞 95 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/tcy83/article/details/85218316