Vue - The Good Parts: 組件

前言

組件基本上是現代 Web 開發的標配,在 Vue 中組件也是其最核心的基石之一。javascript

Vue 在組件的這方面設計也是很是用心,開發者使用的成本能夠說已經很低了,咱們就一塊兒來分析下,並學習其中的技巧和優秀設計思想。css

正文分析

What

咱們首先仍是須要理解下組件化開發。Vue 官網上有一個圖,簡單形象的描述了最核心的思想:html

components.png

也就是開發的時候,咱們將頁面拆分爲一個個的組件,他們之間互相組合,就像堆積木同樣,最終組成了一個樹的形式。那這個也就是組件化開發的核心思想了。前端

那這個時候,咱們就能夠理解下前端的組件:一個功能較爲獨立的模塊vue

這裏邊有幾個核心點:java

  • 模塊
    • 組件必定是一個模塊(獨立)
      • 其實能夠認爲是多個模塊的組合(邏輯模塊 JS、視圖模塊 CSS、結構模塊 HTML)
    • 模塊的目的就是分治、解耦
  • 獨立
    • 獨立意味着追求複用
    • 獨立意味着可組合性(嵌套)
    • 模塊自己具有獨立性,但這裏更多強調的是功能獨立
  • 功能
    • 強調完整性,這是具有功能的基礎
    • 強調功能性,即具體能夠作什麼事情,很具體(表格、導航等)

Vue 中的組件,有一個很好的入門 cn.vuejs.org/v2/guide/co… ,以及推薦相搭配的單文件組件 cn.vuejs.org/v2/guide/si… (我的仍是很是喜歡這種組織方式)node

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

那咱們其實就以一個使用組件的示例,帶着順便分析下 Vue 組件的內幕:react

import Vue from 'vue'
import App from './App.vue'
 
const vm = new Vue({
  render (h) {
    return h(App)
  }
})
 
vm.$mount('#app')
複製代碼

App.vue 就是一個上述的單文件組件,大概內容以下:git

<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 等)的處理差很少。github

固然 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

滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索