在前面文章中,详细探讨了Vue声明流程,Vuejs响应式实现流程,虚拟Dom及模版编译流程,感兴趣的童鞋可以自己查看。
本篇文章将继续探讨Vuejs中一些常用方法的实现过程,包含$set,component,extend。
- 为什么探讨$set方法?
在Vue中可以通过this.$set()为一个响应式数据添加新的属性或者响应式数组添加新项,内部是如何实现的?
$delete内部实现和$set内部实现相似,探讨一个,可以举一反三。
- 为什么探讨component方法?
component方法用于注册组件,探讨此方法,可以弄清楚vuejs内部是如何注册及渲染组件的。
$set
在官方文档中,实例方法$set是Vue静态方法set的一个别名,二者实现原理一样。
使用set方法可以在响应式数据中添加新的属性或者新项:
<body>
<div id="app">
<span>
<strong>{{person.name}}</strong>
</span>
<span>{{person.age}}</span>
</div>
<script>
let vm = new Vue({
el: '#app',
data() {
return {
person: {
name: 'zs'
}
}
}
})
vm.$set(vm.$data.person, 'age', 12)
</script>
</body>
$set方法定义在core/instance/state.js
文件中:
export function stateMixin(Vue: Class<Component>) {
Vue.prototype.$set = set
Vue.prototype.$delete = del
}
其调用的是set函数,该函数定义在core/observer/index.js
中:
- 判断目标target是否是一个响应式对象,如果目标没有定义或者是一个非响应式对象,那么在测试环境下就会发出警告:
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
- 如果目标target是一个数组,那么首先判断key是否是一个有效的索引数字,然后判断target数组能否包含key传入的索引,如果不能包含,则调用length修改数组长度,然后再调用splice修改数组插入值。
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
在前面响应式原理中已经说过,通过splice修改数组,能够触发响应。
- 如果新增的属性已经存在,那么说明此属性已经添加了响应式,直接返回结果即可。
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
- 获取target上存储的ob对象,如果存在ob对象,那么就通过Object.defineProperty为新添加的属性添加getter/setter。然后调用ob.dep.notify方法触发更新。
const ob = (target: any).__ob__
// target._isVue代表target是Vue实例
// ob && ob.vmCount 代表target指向的是$data
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
extend
Vue.extend是使用一个包含组件选项的对象创建一个继承自Vue的子类。
官网示例如下:
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
这样做的好处是:
在通常vue-cli项目中,我们可以通过路由将不同的Dom挂载到id为app的div中,但是类似alert等,应该添加在body节点上,此时就可以用Vue.extend定义一个Alert类,然后在合适的时机渲染并挂载到body中:
const alertComponent = new Alert().$mount()
document.body.appendChild(alertComponent.$el)
extend方法定义在core/global-api/extend.js
中:
- 创建原型链继承,核心内容是将新生成的子类的prototype指向一个原型为Vue的对象:
Sub.prototype = Object.create(Super.prototype)
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
- 添加实例成员和静态成员:
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
if (name) {
Sub.options.components[name] = Sub
}
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
cachedCtors[SuperId] = Sub
component
Vue.component方法用于注册全局组件,本部分将探索全局组件如何实例化及渲染。
Vue.component方法定义在core/global-api/assets.js
文件中:
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 调用extend方法生成一个Vue的子类
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
在注册时,会调用extend方法生成一个子类并添加到全局的Vue.options.components对象上。
组件Vnode创建过程
下面是一个使用组件的示例:
const Comp = Vue.component('comp', {
template: '<div>Hello Component</div>'
})
const vm = new Vue(
{
el: '#app',
render(h) {
return h(Comp)
}
}
)
在render函数中通过h(Comp)的方式创建组件Vnode,在虚拟Dom一章中我们说过h参数其实就是createElement函数,该函数定义在core/vdeom/create-element.js
中,内部调用了_createElement
函数:
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 调用createComponent函数生成组件Vnode
vnode = createComponent(Ctor, data, context, children, tag)
}
// ...
}
在此函数中,如果Ctor是一个组件,那么就调用createComponent生成Vnode。
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
installComponentHooks(data)
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// ...
return vnode
}
在createComponent方法中会调用installComponentHooks函数合并componentVNodeHooks 中预定义的钩子函数和用户传入的钩子函数。
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 创建组件实例并添加到vnode.componentInstance属性上。
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 执行$mount方法,将组件挂载到页面
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
//...
},
insert(vnode: MountedComponentVNode) {
//...
},
destroy(vnode: MountedComponentVNode) {
// ...
}
}
在预定义的init钩子函数中,会创建组件实例并调用$mount方法将组件挂在到页面。
export function createComponentInstanceForVnode(
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// 创建组件实例
return new vnode.componentOptions.Ctor(options)
}
那么init钩子函数什么时候被执行?
通过虚拟Dom的工作机制可以看出,当页面首次渲染和数据变化的时候会执行patch函数,在patch函数内部会调用createComponent
函数,此函数定义在core/vdom/patch.js
中:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 1. 调用init钩子函数,组件在创建vnode的时候已经添加了此钩子函数
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 2. 判断vnode是否定义了componentInstance属性,此属性在init钩子函数中用于存放组件实例
if (isDef(vnode.componentInstance)) {
// 调用其他钩子函数,用于设置局部作用域样式等
initComponent(vnode, insertedVnodeQueue)
// 把组件dom插入到父元素中
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
在此函数中完成了init钩子函数调用及挂载dom。
此时组件从注册到渲染完成的整个流程已经梳理完毕,总结以下,可以分为以下几个步骤:
- Vue.component注册组件。
- 在声明Vue组件的render函数时,用h生成组件Vnode
- 在h函数内部会调用createComponent创建组件Vnode,此过程中会添加内置init钩子函数。
- 首次渲染或者数据变更时会调用patch函数,此函数内部会调用里一个createComponent函数。
- createComponent函数会调用init钩子函数生成组件实例。
- init钩子函数内部会创建组件实例并调用$mount函数渲染。
- createComponent会将渲染后的dom添加到父元素中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。