Vue源码翻译之组件初始化。
废话不多说。
我们先来看看Vue的入口文件。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index' 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)
} initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue) export default Vue
本章先讲第17行开始的initMixin方法 —— 组件初始化
initMixin
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)
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对象在实例化的时候,会给一个uid。 然后我们再看后续的代码:
这里可以看到有一个属性是_isComponent,这个属性在我们整个程序执行第一次的时候,是不存在的,所以这个地方,在Vue首次实例化Vm的时候,肯定是会跳过的,这个怎么理解?其实很简单,整个项目的根VM对象肯定是非内部组件,你看你项目的main.js,是不是这样一段代码:
new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})
这个Vue对象并非内部组件,而是一个根组件。那上面那段代码的意思就是,如果options存在,且当前正在执行初始化的是一个组件(根组件_isComponent估计是undefind或者null),那就要进行内部组件的初始化。我们知道,一般一个Vue单页应用个,是一个根vm,然后根vm实例下面有多个VueComponent,这么一个结构。然后我们现在先把initInternalComponent(vm,options)这个部分先跳过,走else的代码——options融合(mergeOptions)。在理解这个mergOptions的之前,还得先解决
这段代码有点复杂,js的是有一个原型链的概念,对象的constructor(构造函数)实际上就是指向这个类(不是对象本身,对象本身是类的实例。就像Java中的类,和你new一个类的实例这样的区别),还有,需要注意的是,这个方法的入参,有一个:Class<Component>,这个是Flow的语法,然后这个Component连接的类是在/flow/component.js这个文件里定义的。打开这个类,你就能理解这段代码里面的Ctor.superOptions,Ctor.super这些意思。
然后这段代码的意义就是:通过循环递归,找到Component这个类的继承链,然后把所有的配置都进行融合。为什么这么做,得后面再说。我们先mark一下这个位置。反正知道这是一个把所有类的继承链的配置属性进行融合的一个过程。然后回归上面说的,mergerOptions,这段代码,
做的事情也不难理解,就是把你组件属性中的props、inject,directive等进行规范化,并且还会检验你是否按Vue的照规范写,如果没有,还会报出提示。然后就开始根据不同的合并策略,进行数据的合并,合并什么呢?合并刚才从类的继承链中获取的配置对象及你自己在代码中编写的配置对象(从第一次合并肯定是new Vue(options)这个options,当然这个mergeOptions方法在很多地方都有用到,也有用来合并Component的Options,所以需要对props等属性的规范进行检查)。好了,反正这里,就是进行参数信息的融合。我们进入下一步:
initLifecycle
这里呢,可以看到,就是vm实例进行初始化,并且开始执行写生命周期函数。好了,来看看initLifecycle这个方法,这个方法就有点好玩了。
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
}
我们看看,截出的代码第六行开始,这个函数的入参是一个Component —— 组件,所以你要这样去理解。如果组件的parent存在,并且组件还不是抽象的(其实抽象组件目前好像就只有keep-alive,这个也先mark,回头遇到说这个概念,或者看Vue文档,里面有解释抽象组件的意义。),然后做一件什么事儿呢,就是找到这个Component最上级的,非抽象的父组件,然后让这个父组件添加自己到父组件的children数组当中。并且看第13行代码,这个vm对象做了一个与父vm的关联,从而完善这个tree型的组件树。这样其实在每个组件在初始化的时候,都会给自己的父组件进行关联。从父组件的children中可以找到该父组件下所有的子组件,这就是一颗树形结构的数据对象,或者说是一个金字塔。
其实对这个vm的理解,我个人是这么理解的,vm自然是一个Vue的实例,就是一个VueComponent的实例,但是这个VueComponent与我们自己编码的Component可不是同一个东西,我们自己编码的那个Component只是一个模板,框架加载了这个模板,然后,根据组件使用的次数实例化对应个数的VueComponent实例。
为什么这么做呢?现在解释还太早,往后看。这个方法的后续代码就是初始化一些vm的参数,现在也没啥可解释的。反正记住这里给了这些值一个初始化的值,以后有遇到再回头就知道是在这里进行了初始赋值。
initEvents
好了,进入下一个方法,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)
}
}
从这段代码可以看出,首先是为vm的一些参数设置了值,这边可能会有让我们困惑的地方就是_parentListeners这个属性特么又是从哪里来的?不要着急,我们之所以错过了这个方法,是因为我们前面不是有一个_isComponent的判断,然后跳过了initInternalComponent方法么?其实这个listeners是针对父子组件的事件通知的,就是你可能经常会在html标签上写 v-on。那其实这部分代码就是对组件的事件监听,及更新组件的监听事件,并且对v-on设置的参数进行一些规范化,比如你会有一些方法后缀如.capture || .once,把它们从你的配置,转成js对象,这样也才方便后续对事件进行一些操作。
initRender:
好,接下来,初始化render:
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)
}
}
在initRender当中,我们可以看到,对vm的一些关于界面绘制相关的属性和方法都会被进行初始化,比如:vm._c及vm.$createElement,这个方法的具体内容,暂时不看,先看主线任务。代码第23行开始,可以看到$attrs及$listeners这两个对象的数据从父Vnode中来。而这两个属性的解释,也可以从Vue的Api文档中找到:$attrs && $listeners
好了这里我来插播一个概念:我们刚才在initLifecycle的地方有接触到Component有一个树形结构,但是我们也都知道一个问题,就是Vue里的组件,是可以复用的,如果在一个页面中同时出现两个相同的组件,这两个组件的数据是相互隔离的,并不会互相干涉,这是因为实际上在Vue当中,真正跟数据相关联的,不是咱们自己写的那个Component(*.vue),而是通过Component(*.vue)组件创建的VueComponent实例。所以你写的组件,其实就是相当于一个VueComponent的模板,框架根据模板,创建一个或多个VueComponent,而这些VueComponent相互之间都是独立的,否则当你使用v-for并且循环体是一个组件的话,你就会发现,循环后的每个组件值都是一样的,而且改一个组件的数据,其他的也都会跟着改。这里非常的有意思。你编写的Component,其实只是一个模板,实际被创建出来的是Vdom,这个概念一定要分清楚。要理解到Component(*.vue)与VueComponent的关系其实真的不太容易,我是按照Vue的原理自己实现一个框架走到这一步的时候,才理解了其中的深意,而这部分,可以从/src/core/vdom/create-component.js这个文件export出的createComponent()方法中看出细节。
还有VueComponent是包含一个或多个Vnode(一个HTML节点就会对应生成一个Vnode)组成的,这段代码中的vm代表的是一个组件,一个组件是有好多个Vnode组成的一个Vdom,你可能会问,为什么是从parentVnode当中获取data的attrs?所谓attrs其实就是html'标签上的attributes(元素),我们回想一下,当我们制作好一个组件后,是不是使用类似<component-name/>的自定义标签来代表在html文档中的某个地方使用组件?然后你要给这个组件所配置的所有属性,都是在这个<component-name/>标签上去写,比如传入一个title属性:<component-name title="testAttrs"/>所以其实这个自定义标签,就是你自己编写的组件的父节点,而这个attrs属性,也只可能从你的父节点中获取。简单讲一下这个$attr的作用,比如你要写一个列表组件,肯定是先有一个<custom-ul/>然后再来<custom-li/>,但是我们希望这两个组件要配合一起用,然后我们又希望,只需要把数组数据设置给<custom-ul/>这个标签而省去写<custom-li/>,那你就要在外层ul组件上传递给li组件的数据,为了区分这个数据其实是给内部li使用的,而ul这个外层组件其实是不需要用的,不需要自然不用定义props,而我们又想要把数据传递进去给内部的li组件用,那就可以使用$attrs来实现了。当然这个例子不一定恰当,但是应该能解释的清除这个特性。
继续我们看31行和32行,这地方开始对这两个参数进行响应式配置。这里是整个Vue响应式原理的核心。我已经迫不及待的想要解析这部分的代码,但是这个要讲还挺占篇幅,所以放到后面,反正同学们只要知道,通过了这个defineReacttive方法之后,这个数据就会具有响应式的特性,数据变动就会触发页面重绘及变更。好了,这个render的初始化就告一段落,我们进入下一段,下面紧接着,是一个生命周期函数,就是beforeCreate,我们从Vue的文档中可以看到,在这个生命周期函数当中,其实数据观测是尚未设定的,从源码中我们也可以看到,执行到beforeCreate这个步骤的时候,其实Vue只是做了一些数据、实例初始化操作,这也是为什么在beforeCreate方法中如果对数据进行更改,你会发现数据并没有如你所愿的变更。
initInjections/initProvide
这两个函数要一起说,因为这两个函数所对应的inject和provider是成对出现的。这两个函数其实内容也不会很复杂,来看第一个
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
observerState.shouldConvert = 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])
}
})
observerState.shouldConvert = true
}
}
这部分的代码,开始就是resolveInject,这个方法其实也就是从配置当中读取inject配置,
这段代码的意思是,是否是ES2015的Symbol?如果是则通过Reflect来获取key,Object.getOwnPropertyDescriptor的方法时获取对象某个值的属性描述,这一小段的意思是,通过ownKeys得到了一个key数组,但是这个数组要使用filter过滤,只留下“可枚举”的特性的Key,不可枚举的就不需要留下了,至于这么做的原因,嗯,如果有兴趣,可以去看看相关知识,我就不拓展了,否则要跑题了。至于Object.keys就没什么好解释了,这个方法直接就返回的就是具有可枚举特性的key值数组。
这段代码的意思是,从当前的vm向上查找,如果从父级vm上找到了对应的provide那就取对应的值,然后跳出循环,我们观察一下,这个while只有在两种情况下回跳出循环,一个是source为undefind或null,另一个就是找到了对应provide的值,那如果跳出循环的时候,发现父级的vm都undefind了(就是代码中if(!source)这个判断)那说明这个值还没找到,那就要看inject的配置当中,是否有默认配置,其实这个逻辑可以看Vue的文档 provide/inject 就能理解这个逻辑的意义,provide是能注入到所有子组件当中,这里循环向上查找provide就是这个所谓的注入到所有子组件的道理。好了,回到上层代码,剩下的,其实就是把数据进行响应化,上面讲过就不多说了。至于provide,文档里说了,他必须是Object或者返回Object的函数,所以initProvide其实也没有什么特别的,基本就是一看就懂。
至于两个函数一个在initState前初始化,一个在后初始化,按照我的理解,因为provide不是跟自己组件使用,而是给子组件使用,而inject是给当前组件自己用的,并且provide的数据还有可能是从其他props或data传入,这些数据都是需要经过initState进行可响应化。
initState
接下来说说这个initState
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)
}
}
其实这个方法也不会很复杂,首先就是初始化Props,其实就是对Props数据进行一些校验或赋值,来看一下initProps
在这段代码中,我们可以看到,如果vm是根节点或根vm,则此vm的props,需要进行响应化转换,但是如果他不是根vm,则不需要进行响应化转换。这是为什么呢?因为,我们的props都是从父级组件传进来的,而父级组件传进来的值大多是定义在父级组件的data属性,而data属性是必须响应化,所以到了子vm自然就不需要再做一次响应化处理。(就算传入子组件的值是来自父组件的props,并且向上依然如此,当追溯到根vm时,根vm的props是经过了响应化的,所以最终依然还是会成为可响应的属性。)
这剩下initState剩下的部分倒还真的没啥好说,都很容易就能看明白。
接下来,就是 stateMixin了。
Vue源码翻译之组件初始化。的更多相关文章
- 阅读vue源码-----内置组件篇(keep-alive)
1.前言: <keep-alive>是vue实现的一个内置组件,也就是说vue源码不仅实现了一套组件化的机制,也实现了一些内置组件. <keep-alive>官网介绍如下:&l ...
- Vue源码翻译之渲染逻辑链
本篇文章主要要记录说明的是,Vue在Vdom的创建上的相关细节.这也是描绘了Vue在界面的创建上的一个逻辑顺序,同时我也非常拜服作者编码的逻辑性,当然或许这么庞大复杂的编码不是一次性铸就的,我想应该也 ...
- Vue源码学习之数据初始化
首发地址:CJWbiu's Blog 在这里思考一个问题,使用Vue的时候需要在创建Vue实例时传入一个option,这里包含了我们定义的props.methods.data等.而在methods的方 ...
- vue 源码学习二 实例初始化和挂载过程
vue 入口 从vue的构建过程可以知道,web环境下,入口文件在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compil ...
- vue 源码详解(二): 组件生命周期初始化、事件系统初始化
vue 源码详解(二): 组件生命周期初始化.事件系统初始化 上一篇文章 生成 Vue 实例前的准备工作 讲解了实例化前的准备工作, 接下来我们继续看, 我们调用 new Vue() 的时候, 其内部 ...
- 一起学习vue源码 - Vue2.x的生命周期(初始化阶段)
作者:小土豆biubiubiu 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/58c61b4361ff4b005d9e8 ...
- 你还不知道Vue的生命周期吗?带你从Vue源码了解Vue2.x的生命周期(初始化阶段)
作者:小土豆biubiubiu 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/58c61b4361ff4b005d9e8 ...
- Vue源码探究-状态初始化
Vue源码探究-状态初始化 Vue源码探究-源码文件组织 Vue源码探究-虚拟DOM的渲染 本篇代码位于vue/src/core/instance/state.js 继续随着核心类的初始化展开探索其他 ...
- Vue源码之组件化/生命周期(个人向)
大致流程 具体流程 组件化 (createComponent) 构造⼦类构造函数 const baseCtor = context.$options._base // plain options ob ...
随机推荐
- python_day1_python第一个程序 hello world
Python 第一个程序 1)安装好python后,cmd进入DOS下,直接输入python Python 3.6.0 (v3.6.0:41df79263a11, Dec 23 2016, 08:06 ...
- _杂谈_C语言历史
早期的操作系统软件主要是用汇编语言(包括UNIX操作系统在内)编写的.由于汇编语言依赖于计算机硬件,所以程序的可读性和可移植性都比较差,所以呢,为了提高操作系统软件的可读性和可移植性,最好改用高级语言 ...
- String、Stringbuffer、Stringbuilder三者之间的区别
1.首先说运行速度,速度由快到慢排列:StringBuilder > StringBuffer > String String最慢的原因: String为字符串常量,而StringBuil ...
- Java中方法重写和方法重载
首先方法重写和方法重载是建立在Java的面向对象的继承和多态的特性基础上而出现的.至于面向对象的继承和多态的特性我就不在这里多说了.继承是指在一个父类的基础再创建一个子类,这样子类就拥有了父类的非私 ...
- MySQL查询实例
单表查询查询所有列 1 SELECT * FROM product; 查询指定列 1 SELECT pro_name,price,pinpai FROM product; 添加常量列 1 SELECT ...
- 正确的类引用却显示* cannot be resolved
eclipse 出现的问题:在一个类中引入自己编写的类竟然说“cannot be resolved”,这非常明显不正常的! 解决办法:很简单,project->clean.我的问题就解决了. 至 ...
- [mobile]监听手机mobile上面软键盘的回车[enter]事件
$(document).keypress(function(e) { if(e.which == 13) { if(!$(".qaSearchInput").val()) { Hn ...
- List of RGBD datasets
This is an incomplete list of datasets which were captured using a Kinect or similar devices. I init ...
- 实现输入框不可输入、解决Enable,Disable等不能更新值问题
当在前台JS中更新不可用输入框(TextBox.Enable ="false" or Input box ReadOnly ="True")的值时, 后台可能 ...
- MVC+Nhibernate+spring.net(二)
在上一篇文章中我们已经把数据查了出来,现在我们来完善一下:前台使用easyui 首先我们将NHelper类完善一下 public class EmpDal { public IList<Emp& ...