Vue - The Good Parts: 组件

avatar
@滴滴出行

前言

组件基本上是现代 Web 开发的标配,在 Vue 中组件也是其最核心的基石之一。

Vue 在组件的这方面设计也是非常用心,开发者使用的成本可以说已经很低了,我们就一起来分析下,并学习其中的技巧和优秀设计思想。

正文分析

What

我们首先还是需要理解下组件化开发。Vue 官网上有一个图,简单形象的描述了最核心的思想:

components.png

也就是开发的时候,我们将页面拆分为一个个的组件,他们之间互相组合,就像堆积木一样,最终组成了一个树的形式。那这个也就是组件化开发的核心思想了。

那这个时候,我们就可以理解下前端的组件:一个功能较为独立的模块

这里边有几个核心点:

  • 模块
    • 组件一定是一个模块(独立)
      • 其实可以认为是多个模块的组合(逻辑模块 JS、视图模块 CSS、结构模块 HTML)
    • 模块的目的就是分治、解耦
  • 独立
    • 独立意味着追求复用
    • 独立意味着可组合性(嵌套)
    • 模块本身具备独立性,但这里更多强调的是功能独立
  • 功能
    • 强调完整性,这是具备功能的基础
    • 强调功能性,即具体可以做什么事情,很具体(表格、导航等)

Vue 中的组件,有一个很好的入门 cn.vuejs.org/v2/guide/co… ,以及推荐相搭配的单文件组件 cn.vuejs.org/v2/guide/si… (个人还是非常喜欢这种组织方式)

image2021-7-1_16-13-56.png

那我们其实就以一个使用组件的示例,带着顺便分析下 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 钩子

那对应的如果涉及到组件销毁的过程,基本上是从更新组件开始,到 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

滴滴前端技术团队的团队号已经上线,我们也同步了一定的招聘信息,我们也会持续增加更多职位,有兴趣的同学可以一起聊聊。