我们继续上一章,第三部分是对各类功能的初始化。这个章节内容较多,大家还需有耐心。
/**
*第三部分,初始化相关功能
*/
/* 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')
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是什么,我们后面详细描述。
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官方文档实例属性进行了解。
initEvents的作用存储父组件绑定当前子组件的事件,保存到vm._events中。
Vue中的事件分为两种,一种是DOM原始的事件,比如click,focus等,另一种就是自定义事件,对事件机制不了解的可以看VUE探索第五篇-组件(Component),下面我们来用具体的实例描述事件如何存储到vm._events中的。
...
var childComponent = Vue.component('child',{
template:"{{childMsg}}
",
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, 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, 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章节会调用。
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的监听,该方法在后面响应式原理章节中详解。
我们先来讲下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
}
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
}
}
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)
}
}
接下来我们一个一个看。
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对象为:
是对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对象上
对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就是创建的新实例对象。
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中,实现依赖收集,当依赖变量发生变化,触发计算属性的重新计算。并返回其计算后的值。
对于这部分知识,目前阶段仅做了解,在后面的响应式原理章节中,我们会重点分析。
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)执行。
同样,对于这 部分知识,当前阶段大家仅做了解即可,后面我们会专门分析。
调用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
}
本章节内容较多,主要是初始化相关的工作,我们再来回顾下这张导图。