前言
组件基本上是现代 Web 开发的标配,在 Vue 中组件也是其最核心的基石之一。
Vue 在组件的这方面设计也是非常用心,开发者使用的成本可以说已经很低了,我们就一起来分析下,并学习其中的技巧和优秀设计思想。
正文分析
What
我们首先还是需要理解下组件化开发。Vue 官网上有一个图,简单形象的描述了最核心的思想:
也就是开发的时候,我们将页面拆分为一个个的组件,他们之间互相组合,就像堆积木一样,最终组成了一个树的形式。那这个也就是组件化开发的核心思想了。
那这个时候,我们就可以理解下前端的组件:一个功能较为独立的模块。
这里边有几个核心点:
- 模块
- 组件一定是一个模块(独立)
- 其实可以认为是多个模块的组合(逻辑模块 JS、视图模块 CSS、结构模块 HTML)
- 模块的目的就是分治、解耦
- 组件一定是一个模块(独立)
- 独立
- 独立意味着追求复用
- 独立意味着可组合性(嵌套)
- 模块本身具备独立性,但这里更多强调的是功能独立
- 功能
- 强调完整性,这是具备功能的基础
- 强调功能性,即具体可以做什么事情,很具体(表格、导航等)
Vue 中的组件,有一个很好的入门 cn.vuejs.org/v2/guide/co… ,以及推荐相搭配的单文件组件 cn.vuejs.org/v2/guide/si… (个人还是非常喜欢这种组织方式)
那我们其实就以一个使用组件的示例,带着顺便分析下 Vue 组件的内幕:
import Vue from 'vue'
import App from './App.vue'
const vm = new Vue({
render (h) {
return h(App)
}
})
vm.$mount('#app')
App.vue 就是一个上述的单文件组件,大概内容如下:
<template>
<div id="app">
<div @click="show = !show">Toggle</div>
<p v-if="show">{{ msg }}</p>
</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello World!',
show: false
}
}
}
</script>
<style lang="stylus">
#app
font-family Avenir, Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
这里也可以进一步感受到,在 Vue 中一个组件的样子:模板 + 脚本逻辑 + 样式。在逻辑部分,使用的就是和我们在生命周期分析中所涉及到的初始化部分:对一些配置项(data、methods、computed、watch、provide、inject 等)的处理差不多。
当然 Vue 中还有其他的很多的配置项,详细的可以参考官方文档,这里不细说了。
How
根据我们的示例,结合我们在生命周期文章中的分析,Vue 应用 mount 之后,就会调用 render() 函数得到 vdom 数据,而我们也知道这个 h
就是实例的 $createElement
,同时参数 App 是我们定义的一个组件。
回到源码 createElement 相关的具体实现就在 github.com/vuejs/vue/b… 这里简要看下:
import { createComponent } from './create-component'
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// 直接 _createElement
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
// 字符串,我们这里大概了解下
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 内置元素,在 Web 中就是普通 HTML 元素
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 组件场景
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// 这肯定是组件场景,也就是我们上述的 Case 会进入这里
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
接下来的重点看起来就是这个 createComponent 了,来自 github.com/vuejs/vue/b…
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 也就是 Vue
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
// 我们的场景,因为是一个普通对象,所以这里会调用 Vue.extend 变为一个构造器
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
// 之前有涉及一点点的 抽象组件
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
// 安装组件 hooks 很重要!!
installComponentHooks(data)
// return a placeholder vnode
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
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
仔细看看这个重要的安装 hooks
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// ...
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},
insert (vnode: MountedComponentVNode) {
// ...
},
destroy (vnode: MountedComponentVNode) {
// ...
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
// 安装组件 hooks
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
// 遍历 & 安装,hook 主要有 init prepatch insert destroy
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
// 这个 mergeHook
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
function mergeHook (f1: any, f2: any): Function {
// 返回了一个新的函数 新的函数 按照顺序 依次调用 f1 f2
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
可以看出,基本上就是把 componentVNodeHooks 上定义的 hook 点(init prepatch insert destroy)的功能赋值到 vnode 的 data.hook 上。
以及这里还有一个技巧,mergeHook 利用闭包特性,使得可以达到合并函数执行的目的。
按照在生命周期的介绍,调用完 render() 后就会执行 patch 相关逻辑,进而会执执行到 createElm 中 github.com/vuejs/vue/b…
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
这里就会优先执行 createComponent github.com/vuejs/vue/b…
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 重点:调用 vnode.data.hook 中的 init 钩子
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
// init 钩子中会创建组件实例 且 mounted 了,下面详细分析
// 此时 componentInstance 就会已经创建
if (isDef(vnode.componentInstance)) {
// 初始化组件
initComponent(vnode, insertedVnodeQueue)
// 插入元素
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 触发 create hooks
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// 重点:cbs 中的 create 钩子
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
// vnode 上自带的
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
// create 钩子
if (isDef(i.create)) i.create(emptyNode, vnode)
// 重点:插入钩子,没有立即调用 而是放在队列中了
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
上边的分析有几个重点需要我们关注下:
- init 钩子执行了啥
- cbs 的钩子哪里来的,大概做了啥事情
- 插入钩子为何放在队列中了,而不是立即执行
init 钩子执行了啥
回到安装 hooks 中,我们知道 componentVNodeHooks 中定义了 init 钩子需要做的事情 github.com/vuejs/vue/b…
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// 忽略
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 为 vnode 创建组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// mount 这个组件实例
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
而这个 createComponentInstanceForVnode 的逻辑是这样的
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// 实例化构造器
return new vnode.componentOptions.Ctor(options)
}
而在前边的分析我们已经知道了这个构造器就是一个继承 Vue 的子类,所以初始化的过程就是基本上是 Vue 初始化的过程;同时在 init 钩子里,有了组件实例,就会立即调用 $mount 挂载组件,这些逻辑都已经在生命周期相关的分析中已经分析过了,这里就不细说了,感兴趣的可以看 Vue - The Good Parts: 生命周期。
cbs 的钩子哪里来的,大概做了啥事情
那 cbs 中的钩子来自哪里呢?这个需要回到 patch 中 github.com/vuejs/vue/b…
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
}
在 createPatchFunction
的最顶部,执行的时候,就会给 cbs 做赋值操作,依据的就是传入的 modules 中的配置。这里我们就不需要看所有的 modules 都做了什么事情了,我们可以挑选两个大概来看下,可能会做一些什么样的事情:一个是来自于 core 中的指令 github.com/vuejs/vue/b… ,另一个是来自于平台 Web 的 style github.com/vuejs/vue/b…
// directives.js
export default {
// 钩子们,这里用到了 create update 以及 destroy
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
function _update (oldVnode, vnode) {
// 根据新旧 vnode 信息更新 指令信息
const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
const dirsWithInsert = []
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) {
// 指令 bind 钩子
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// existing directive, update
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
// 指令 update 钩子
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
// 指令 inserted 钩子
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
// 指令 componentUpdated 钩子
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
// 指令 unbind 钩子
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
可以看到基本上就是根据各种条件调用指令的各个周期的钩子函数,核心也是生命周期的思想。
// style.js
export default {
create: updateStyle,
update: updateStyle
}
function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) {
const data = vnode.data
const oldData = oldVnode.data
if (isUndef(data.staticStyle) && isUndef(data.style) &&
isUndef(oldData.staticStyle) && isUndef(oldData.style)
) {
return
}
let cur, name
const el: any = vnode.elm
const oldStaticStyle: any = oldData.staticStyle
const oldStyleBinding: any = oldData.normalizedStyle || oldData.style || {}
// if static style exists, stylebinding already merged into it when doing normalizeStyleData
const oldStyle = oldStaticStyle || oldStyleBinding
const style = normalizeStyleBinding(vnode.data.style) || {}
// store normalized style under a different key for next diff
// make sure to clone it if it's reactive, since the user likely wants
// to mutate it.
vnode.data.normalizedStyle = isDef(style.__ob__)
? extend({}, style)
: style
const newStyle = getStyle(vnode, true)
for (name in oldStyle) {
if (isUndef(newStyle[name])) {
setProp(el, name, '')
}
}
for (name in newStyle) {
cur = newStyle[name]
if (cur !== oldStyle[name]) {
// ie9 setting to null has no effect, must use empty string
setProp(el, name, cur == null ? '' : cur)
}
}
}
大概的逻辑就是新的和旧的style对比,去重置元素的style样式。
通过这种方式很好的实现了,在运行时动态扩展能力的特性。
插入钩子为何放在队列中了,而不是立即执行
那是因为需要保证 insert 的钩子一定是元素已经实际插入到 DOM 中之后再去执行 insert 的钩子。这种情况主要出现在子组件作为根节点,且是首次渲染的情况下,这个时候实际的 DOM 元素本身是一个,所以需要等到父组件的 initComponent 的时候插入到父组件 patch 的队列中,最后在执行。
这个逻辑在 patch 的最后阶段 github.com/vuejs/vue/b… 会调用 invokeInsertHook 这个有关系:
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
// 我们上边所解释的情况
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
// 其他时候直接调用 vnode 的 data.hook.insert 钩子
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
那么这个时候就再次回到了我们的安装组件hook相关逻辑中,这个时候的 insert 钩子做了什么事情呢?github.com/vuejs/vue/b…
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
// 此时还没有 mounted
componentInstance._isMounted = true
// 调用组件实例的 mounted 钩子
callHook(componentInstance, 'mounted')
}
// keep alive 的情况
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
}
这里我们需要关注的重点就是第一个判断,此时子组件(我们场景中 App 组件对应的实例)还没有调用挂载钩子,所以直接调用了 mounted 钩子,完成了调用挂载生命周期钩子。
接着,回到最初 Vue 实例的 patch 完成之后的逻辑,最终调用了 Vue 实例的 mounted 生命周期钩子。
到了这里基本上整个初始化且挂载的整个过程基本上就完成了,所以这里回顾下整个的过程:
- 根实例 create 阶段完成
- 根实例 mount 阶段
- render
- 子组件 vnode 创建 & 安装 hook
- patch
- 遇到普通元素
- 创建DOM元素
- 遇到组件
- 创建子组件实例(通过 init 钩子)& mount
- 触发子组件 mounted 钩子(通过 insert 钩子)
- 遇到普通元素
- 触发根实例 mounted 钩子
- render
那对应的如果涉及到组件销毁的过程,基本上是从更新组件开始,到 patch,发现被移除了,接着触发对应 vnode 的 destroy 钩子 github.com/vuejs/vue/b…
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
剩下的就和在Vue - The Good Parts: 生命周期文章中所涉及的销毁的逻辑保持一致了。
Why
如同我们在开篇的时候分享的关于组件化和组件的内容,以及从前端本身的整个历史来看,组件化开发时一直是一种最佳实践。
最核心的原因是组件化开发可以带给我们最大的好处:分治,那分治可以带来的好处:拆分和隔离复杂度。
当然,还有其他的很多好处:
- 高内聚、低耦合(通过组件规范约束,如 props、events 等)
- 易于开发、测试
- 便于协同
- 复用
- 到处可用
- 易扩展
有了这些,从而达到了提升开发效率和可维护性的终极目标。
总结
通过以上分析,我们也更加清楚了 Vue 中是如何实现组件化的,组件都继承 Vue,所以基本上他们都具备相同的配置、生命周期、API。
那除了我们对组件有了更深的理解之外,整个也是最重要的点,我们还可以从 Vue 的实现中学到哪些东西呢?
组件设计
在 Vue 里组件是按照类来设计的,虽然对于用户而言,更多的时候你写的就是一个普通的对象,传入一对的配置项,但在 Vue 内部处理的时候,还是通过 extend 的方式转换为了一个构造器,进而方便进行实例化,这点就是一个经典的继承思维。
现在我们已知的 Vue 组件的配置项包含了,生命周期钩子们(create 相关、mount 相关、update 相关、destroy 相关),还有状态数据相关的 props、data、methods、computed、watch,也有 DOM 相关的 el、template、render。这些选项也是日常最最常用的部分了,所以我们需要好好理解且知晓他们背后的实现和作用。
额外的, Vue 中组件还包含了资源相关 cn.vuejs.org/v2/api/#%E9… 、组合相关 cn.vuejs.org/v2/api/#%E9… 、还有其他 cn.vuejs.org/v2/api/#%E9… 这些的配置项,也都是常用的,感兴趣的可以自己研究下内部的实现以及找到他们实现的精粹。
除了配置项,还有组件实例,大多在我们相关的分析中也有涉及,如 $props
、$data
、$el
、$attrs
、$watch
、$mount()
、$destroy()
以及事件相关 $on()
、$off()
、$emit()
、$once()
等,也可以看出从命名上都是以 $
开头的,很规范,可以参考官网了解更多。
还有非常好用的动态组件和异步组件,设计的十分友好 cn.vuejs.org/v2/guide/co…
插件化思维
modules 的组织,即 createPatchFunction 中传入的 modules。上边我们也分析了两个 modules 的示例,可以看出,借助于我们在 VDOM 层面设计好的 patch 钩子,我们将很多的功能做了模块拆分,每个模块自行去根据钩子的时机去做对应的事情。到这里你也可以发现这其实大概是一种插件化思维的运用,插件化思维本身又是一种微内核架构的体现。这个点也是符合 Vue 的整个设计理念的:渐进式的框架。
所以 Vue 基本上从内部的一些设计到整个的生态建设,都是遵循着自身的设计理念,这是一种很重要的践行和坚持,值得我们深思。
其他小Tips
- 再次多次出现的不同场景的生命周期钩子(VDOM的、指令的),可以参考Vue - The Good Parts: 生命周期
- 利用闭包实现的 mergeHook,把原本可能需要数组实现的调用变为了单一的函数调用 niubility
- Vue.extend 实现,经典的继承实现 github.com/vuejs/vue/b…
- Vue 中如何 resolveConstructorOptions(考虑继承) github.com/vuejs/vue/b…
滴滴前端技术团队的团队号已经上线,我们也同步了一定的招聘信息,我们也会持续增加更多职位,有兴趣的同学可以一起聊聊。