Vue.js 源碼剖析-組件化

Study Notes

本博主會持續(xù)更新各種前端的技術(shù)岔帽,如果各位道友喜歡骡苞,可以關(guān)注、收藏撤蟆、點贊下本博主的文章。

Vue.js 源碼剖析-組件化

  • 組件化可以讓我們方便的把頁面拆分成多個可重用的組件

  • 組件是獨立的允睹,系統(tǒng)內(nèi)可重用霸奕,組件之間可以嵌套

  • 有了組件可以像搭積木一樣開發(fā)網(wǎng)頁

    例如,你可能會有頁頭汛蝙、側(cè)邊欄烈涮、內(nèi)容區(qū)等組件,每個組件又包含了其它的像導(dǎo)航鏈接窖剑、博文之類的組件坚洽。

  • 下面我們將從源碼的角度來分析 Vue 組件內(nèi)部如何工作
    • 組件實例的創(chuàng)建過程是從上而下(先創(chuàng)建父組件再創(chuàng)建子組件)
    • 組件實例的掛載過程是從下而上(先掛載子組件再掛載父組件)

組件定義

  • 注冊 Vue.component()入口

src/core/global-api/index.js

// 注冊 Vue.directive()、 Vue.component()西土、Vue.filter()
initAssetRegisters(Vue);

src/core/global-api/assets.js

export function initAssetRegisters(Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  // 遍歷 ASSET_TYPES 數(shù)組讶舰,為 Vue 定義相應(yīng)方法
  // ASSET_TYPES 包括了directive、 component翠储、filter
  ASSET_TYPES.forEach((type) => {
    Vue[type] = function (
      id: string,
      definition: Function | Object,
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id];
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production') {
          if (type === 'component' && config.isReservedTag(id)) {
            warn(
              'Do not use built-in or reserved HTML elements as component ' +
                'id: ' +
                id,
            );
          }
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id;
          // 把組件配置轉(zhuǎn)換為組件的構(gòu)造函數(shù)
          definition = this.options._base.extend(definition);
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        // 全局注冊绘雁,存儲資源并賦值
        // 例如:this.options['components']['comp'] = definition
        // 當(dāng)前this指向vue實例
        this.options[type + 's'][id] = definition;
        return definition;
      }
    };
  });
}

src/core/global-api/extend.js

/* @flow */

import ...

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   * 每個實例構(gòu)造函數(shù)(包括Vue)都具有唯一的cid。
   * 這使我們能夠為創(chuàng)建一個包裹的“子構(gòu)造函數(shù)”通過原型繼承并對其進行緩存援所。
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Vue構(gòu)造函數(shù)
    const Super = this
    const SuperId = Super.cid
    // 從緩存中加載組件的構(gòu)造函數(shù)
    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') {
      // 如果是開發(fā)環(huán)境驗證組件的名稱
      if (!/^[a-zA-Z][\w-]*$/.test(name)) {
        warn(
          'Invalid component name: "' + name + '". Component names ' +
          'can only contain alphanumeric characters and the hyphen, ' +
          'and must start with a letter.'
        )
      }
    }

    // 組件對應(yīng)的構(gòu)造函數(shù)
    const Sub = function VueComponent (options) {
      // 調(diào)用 _init() 初始化
      this._init(options)
    }
    // 原型繼承自 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并 options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    // 對于props和計算屬性庐舟,我們在Vue實例上定義代理getter時擴展原型。
    // 這樣可以避免為每個創(chuàng)建的實例調(diào)用Object.defineProperty住拭。
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    // 集成extension/mixin/plugin
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    // 集成directive挪略、 component、filter
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      // 把組件構(gòu)造函數(shù)保存到 Ctor.options.components.comp = Ctor
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    // 在擴展時保留對super 選項的引用滔岳。
    // 稍后在實例化時杠娱,我們可以檢查Super的選項是否已更新。
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 把組件的構(gòu)造函數(shù)緩存到 options._Ctor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

組件創(chuàng)建和掛載

  • 組件 VNode 的創(chuàng)建過程

    • 創(chuàng)建根組件谱煤,首次 _render() 時摊求,會得到整棵樹的 VNode 結(jié)構(gòu)
    • 整體流程:new Vue() --> $mount() --> vm._render() --> createElement() --> createComponent()
    • 創(chuàng)建組件的 VNode,初始化組件的 hook 鉤子函數(shù)
  • src/core/vdom/create-element.js 中的_createElement方法調(diào)用了createComponent方法創(chuàng)建組件的 VNode

...

if (typeof tag === 'string') {
  let Ctor;
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
  //  如果是瀏覽器的保留標(biāo)簽刘离,創(chuàng)建對應(yīng)的 VNode
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    // 創(chuàng)建vnode對象
    vnode = new VNode(
      config.parsePlatformTagName(tag),
      data,
      children,
      undefined,
      undefined,
      context,
    );
  } else if (
    isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
  ) {
    // component
    // 如果是自定義組件
    // 查找自定義組件構(gòu)造函數(shù)的聲明
    // 根據(jù)Ctor創(chuàng)建組件的VNode
    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 {
  // direct component options / constructor
  // 如果tag不是字符串室叉,即代表其是一個組件
  // 創(chuàng)建組件的VNode
  vnode = createComponent(tag, data, context, children);
}

...
/* @flow */

import ...;

// hooks to be invoked on component VNodes during patch
// 鉤子函數(shù)定義的位置(init()鉤子中創(chuàng)建組件的實例)
const componentVNodeHooks = {
  init(
    vnode: VNodeWithData,
    hydrating: boolean,
    parentElm: ?Node,
    refElm: ?Node,
  ): ?boolean {
    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
      // 創(chuàng)建組件實例掛載到 vnode.componentInstance
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance,
        parentElm,
        refElm,
      ));
      // 調(diào)用組件對象的 $mount(),把組件掛載到頁面
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    } else if (vnode.data.keepAlive) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {...},

  insert(vnode: MountedComponentVNode) {...},

  destroy(vnode: MountedComponentVNode) {...},
};

const hooksToMerge = Object.keys(componentVNodeHooks);

// 創(chuàng)建自定義組件對應(yīng)的 VNode
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string,
): VNode | void {
  if (isUndef(Ctor)) {
    return;
  }

  const baseCtor = context.$options._base;

  // plain options object: turn it into a constructor
  // 如果Ctor不是一個構(gòu)造函數(shù)硫惕,是一個對象
  // 使用Vue.extend()創(chuàng)造一個子組件的構(gòu)造函數(shù)
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  // 如果在此階段它不是構(gòu)造函數(shù)或異步組件工廠茧痕,則拒絕。
  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, context);
    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.
      // 返回異步組件的占位符節(jié)點恼除,該占位符呈現(xiàn)為注釋節(jié)點踪旷,但保留該節(jié)點的所有原始信息。
      // 該信息將用于異步服務(wù)器渲染和客戶端激活豁辉。
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
    }
  }

  data = data || {};

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 解析構(gòu)造函數(shù)選項
  // 在組件構(gòu)造函數(shù)創(chuàng)建后合并當(dāng)前組件選項和通過vue.mixin混入的選項
  resolveConstructorOptions(Ctor);

  // transform component v-model data into props & events
  // 將組件v-model數(shù)據(jù)轉(zhuǎn)換為props 和 events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  // 提取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;
    }
  }

  // merge component management hooks onto the placeholder node
  // 合并組件的鉤子函數(shù)init/prepatch/insert/destroy
  // 準(zhǔn)備好了data.hook中的鉤子函數(shù)
  mergeHooks(data);

  // return a placeholder vnode
  const name = Ctor.options.name || tag;
  // 創(chuàng)建自定義組件的VNode令野,設(shè)置自定義組件的名字
  // 記錄this.componentOptions = componentOptions
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory,
  );
  return vnode;
}

// 創(chuàng)建組件實例的位置,由自定義組件的 init() 鉤子方法調(diào)用
export function createComponentInstanceForVnode(
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
  parentElm?: ?Node,
  refElm?: ?Node,
): Component {
  const vnodeComponentOptions = vnode.componentOptions;
  const options: InternalComponentOptions = {
    _isComponent: true,
    parent,
    propsData: vnodeComponentOptions.propsData,
    _componentTag: vnodeComponentOptions.tag,
    _parentVnode: vnode,
    _parentListeners: vnodeComponentOptions.listeners,
    _renderChildren: vnodeComponentOptions.children,
    _parentElm: parentElm || null,
    _refElm: refElm || null,
  };
  // check inline-template render functions
  // 獲取inline-template
  // 例如:<comp inline-template>xx</comp>
  const inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  // 創(chuàng)建組件實例
  return new vnodeComponentOptions.Ctor(options);
}

function mergeHooks(data: VNodeData) {
  if (!data.hook) {
    data.hook = {};
  }
  // 用戶可以傳遞自定義鉤子函數(shù)
  // 把用戶傳入的自定義鉤子函數(shù)和componentVNodeHooks中預(yù)定義的鉤子函數(shù)合并
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i];
    const fromParent = data.hook[key];
    // 獲取鉤子函數(shù)(init()鉤子中創(chuàng)建組件的實例)
    const ours = componentVNodeHooks[key];
    data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours;
  }
}

function mergeHook(one: Function, two: Function): Function {
  return function (a, b, c, d) {
    one(a, b, c, d);
    two(a, b, c, d);
  };
}

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel(options, data: any) {
  const prop = (options.model && options.model.prop) || 'value';
  const event = (options.model && options.model.event) || 'input';
  (data.props || (data.props = {}))[prop] = data.model.value;
  const on = data.on || (data.on = {});
  if (isDef(on[event])) {
    on[event] = [data.model.callback].concat(on[event]);
  } else {
    on[event] = data.model.callback;
  }
}

組件實例的創(chuàng)建和掛載過程

Vue._update() --> patch() --> createElm() --> createComponent()

src/core/vdom/patch.js

// 注意:先創(chuàng)建父組件再創(chuàng)建子組件徽级;先掛載子組件再掛載父組件彩掐。
// 1.創(chuàng)建組件實例,掛載到真實 DOM
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    // 調(diào)用init()方法灰追,創(chuàng)建和掛載組件實例
    // init()的過程中創(chuàng)建好了組件的真實DOM堵幽,掛載到了vnode.elm上
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      i(vnode, false /* hydrating */, parentElm, refElm);
    }
    if (isDef(vnode.componentInstance)) {
      // 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.
      // 調(diào)用鉤子函數(shù)(VNode的鉤子函數(shù)初始化屬性/事件/樣式等,組件的鉤子函數(shù))
      initComponent(vnode, insertedVnodeQueue);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true;
    }
  }
}

// 2.調(diào)用鉤子函數(shù)弹澎,設(shè)置局部作用于樣式
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)) {
    // 調(diào)用鉤子函數(shù)
    invokeCreateHooks(vnode, insertedVnodeQueue);
    // 設(shè)置局部作用于樣式
    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);
  }
}

// 3.調(diào)用create鉤子函數(shù)
function invokeCreateHooks(vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    // 觸發(fā)create鉤子函數(shù)
    cbs.create[i](emptyNode, vnode);
  }
  i = vnode.data.hook; // Reuse variable
  // 調(diào)用組件的鉤子函數(shù)
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode);
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode);
  }
}

總結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末朴下,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子苦蒿,更是在濱河造成了極大的恐慌殴胧,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佩迟,死亡現(xiàn)場離奇詭異团滥,居然都是意外死亡,警方通過查閱死者的電腦和手機报强,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門灸姊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秉溉,你說我怎么就攤上這事力惯。” “怎么了召嘶?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵父晶,是天一觀的道長。 經(jīng)常有香客問我弄跌,道長甲喝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任铛只,我火速辦了婚禮埠胖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘格仲。我一直安慰自己押袍,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布凯肋。 她就那樣靜靜地躺著谊惭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侮东。 梳的紋絲不亂的頭發(fā)上圈盔,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天,我揣著相機與錄音悄雅,去河邊找鬼驱敲。 笑死,一個胖子當(dāng)著我的面吹牛宽闲,可吹牛的內(nèi)容都是我干的众眨。 我是一名探鬼主播握牧,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼娩梨!你這毒婦竟也來了沿腰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤狈定,失蹤者是張志新(化名)和其女友劉穎颂龙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纽什,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡措嵌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了芦缰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片企巢。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖饺藤,靈堂內(nèi)的尸體忽然破棺而出包斑,到底是詐尸還是另有隱情,我是刑警寧澤涕俗,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布罗丰,位于F島的核電站,受9級特大地震影響再姑,放射性物質(zhì)發(fā)生泄漏萌抵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一元镀、第九天 我趴在偏房一處隱蔽的房頂上張望绍填。 院中可真熱鬧,春花似錦栖疑、人聲如沸讨永。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卿闹。三九已至,卻和暖如春萝快,著一層夾襖步出監(jiān)牢的瞬間锻霎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工揪漩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旋恼,地道東北人。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓奄容,卻偏偏與公主長得像冰更,于是被迫代替她去往敵國和親产徊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內(nèi)容