Vue 2.x原始碼學習:render方法、模板解析和依賴收集

beckyyyy發表於2022-11-30

眾所周知,Vue的腳手架專案是透過編寫.vue檔案來對應vue裡元件,然後.vue檔案是透過vue-loader來解析的,下面是我學習元件渲染過程和模板解析中的一些筆記。

之前的筆記:

Vue例項掛載方法$mount

一個普通vue應用的初始化:

import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

new Vue({
  render: (h) => h(App),
}).$mount("#app");

vue是在模板解析的過程中對元件渲染所依賴的資料進行收集的,而模板解析是掛載方法.$mount執行過程中的操作,.$mount方法又是在什麼時候定義的呢?

1. build相關指令碼

package.json中,我們可以看到有幾個build相關的指令碼:

{
  "scripts": {
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
  }
}

普通打包執行的是不帶字尾的指令碼build,即不帶引數。

// scripts/build.js
// ...

let builds = require('./config').getAllBuilds()

// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

build(builds)

function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}

// ...

不帶引數的build指令碼,即代表process.argv[2]為false,進入下面這段程式碼:

let builds = require('./config').getAllBuilds()

// filter builds via command line arg
if (process.argv[2]) {
  // ...
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

由上述程式碼可知,builds是由./config模組執行getAllBuilds()所得:

// scripts/config.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)

getAllBuilds()方法是對Object.keys(builds)陣列做對映操作並將結果返回,再繼續看scripts/config.js中的builds變數,可以看到,是針對不同編譯包不同的配置,關於weex的可以不看,因為b.output.file.indexOf('weex') === -1將weex相關的配置過濾掉了,其餘的就是不同模組系統的打包配置,如cjs、es、es in browser、umd等等。

下面是es的打包配置:

// scripts/config.js
const builds = {
  // ...
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // ...
}

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

可以看到有兩個,一個只有執行時的程式碼,另一個還包含了編譯器compiler的部分。

根據aliases的配置,我們可以找到'web/entry-runtime.js'的路徑解析:

// scripts/alias.js
module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

這裡看只包含執行時程式碼的編譯配置,找到它的入口檔案resolve('web/entry-runtime.js')

// src/platforms/web/entry-runtime.js
import Vue from './runtime/index'

export default Vue

繼續找到src/platforms/web/runtime/index.js

// src/platforms/web/runtime/index.js
/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

// ...

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// ...

export default Vue

至此我們就找到了Vue原型物件上的$mount方法定義。

el拿到真實的dom節點,而mountComponent我們也可以看到,是在src/core/instance/lifecycle.js中定義的。

元件掛載mountComponent

// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      // ...
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

如果我們沒有傳入一個render函式,就會將render賦值為一個建立空VNode的函式:vm.$options.render = createEmptyVNode

再繼續可以看到,建立了一個Watcher例項,並將這個watcher例項標記為renderWatcher。

在之前學習Watcher程式碼的時候我們有看到,在例項被建立時,如果沒有設定lazy,會立即執行一遍expOrFn,也就是說此處傳入的updateComponent會立即被呼叫,也就是會執行例項的_update方法。

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

可以看到在執行_update之前會先呼叫_render,並將結果作為引數傳給_update

渲染方法vm._render

在執行vm._update(vm._render(), hydrating)時,傳入了vm._render(),即vm例項會去執行_render方法。

1. _render定義

// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
      // ...
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
      // ...
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

vnode = render.call(vm._renderProxy, vm.$createElement),如果render未定義,根據mountComponent中的程式碼可知使用的是createEmptyVNode,呼叫render時繫結this為vm例項,傳入引數vm.$createElement

由vue應用初始化程式碼可以看到,根節點元件傳入了render:

render: (h) => h(App),

呼叫render.call(vm._renderProxy, vm.$createElement)可以簡單看作執行vm.$createElement(App);,根據上述程式碼查詢vm例項的$createElement方法,

2. vm.$createElement

initRender中定義的:

// src/core/instance/render.js
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  
  // ...
}

3. 呼叫_createElement

繼續查詢createElement函式及其呼叫的內部_createElement函式:

// src/core/vdom/create-element.js
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
  }
  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)) {
      // 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 {
    // 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()
  }
}

App.vue已經被webpack中的vue-loader解析為一個模組,所以此時傳入_createElement的App是一個物件,即此處的形參tag

因為只有contexttag兩個入參:vmApp,所以可以直接跳到看vnode = createComponent(tag, data, context, children)

createComponent返回vnode例項,_createElement函式最後也是返回一個vnode例項。

4. createComponent

// src/core/vdom/create-component.js
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
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  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)) {
    // ... Ctor.cid有定義,此段程式碼可暫時忽略
  }

  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
  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
}

installComponentHooks(data)使在data上掛上一個hook的屬性,並且將const componentVNodeHooks的屬性掛到data.hook物件上。

context.$options._base查詢_base的定義,在src/core/global-api/index.js檔案中的initGlobalAPI函式中定義。

Vue.options._base = Vue

baseCtor.extend(Ctor)查詢extend的定義,在src/core/global-api/extend.js檔案中定義。

Vue.extend = function (extendOptions: Object): Function {
  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

  // 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.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

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

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    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.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

可以看出在Vue.extend方法中,將原本的Ctor物件改造成了一個繼承Vue的子類,並且該子類在例項化時會執行例項的_init方法。

const Sub = function VueComponent (options) {
  this._init(options)
}

原本Ctor物件上帶有的屬性都被掛載子類的options屬性上。

Sub.options = mergeOptions(
    Super.options,
    extendOptions
)

最後,createComponent函式建立了一個vnode例項並將此例項返回:

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }, /* componentOptions */
    asyncFactory
)

可以看出,createComponent建立的vnode例項返回給createElement函式,最終傳遞給了vm._update

更新方法vm._update

1. 方法定義

// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

setActiveInstance(vm):設定activeInstance為當前vm例項。

因為是初次渲染,所以沒有舊的節點,即進入下面這個條件:

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

2. vm.__patch__——>createPatchFunction

透過src/platforms/web/runtime/index.js,我們可以找到vm.__patch__方法的定義。

// src/platforms/web/runtime/index.js
import { patch } from './patch'

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src/platforms/web/runtime/patch.js
/* @flow */

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

nodeOps是訪問和操作真實dom的一些api。

// src/core/vdom/patch.js
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]])
      }
    }
  }

  function emptyNodeAt (elm) {
    // ...
  }

  function createRmCb (childElm, listeners) {
    // ...
  }

  function removeNode (el) {
    // ...
  }

  function isUnknownElement (vnode, inVPre) {
    // ...
  }

  let creatingElmInVPre = 0

  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // ...
  }

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    // ...
  }

  function initComponent (vnode, insertedVnodeQueue) {
    // ...
  }

  function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    // ...
  }

  function insert (parent, elm, ref) {
    // ...
  }

  function createChildren (vnode, children, insertedVnodeQueue) {
    // ...
  }

  function isPatchable (vnode) {
    // ...
  }

  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    // ...
  }

  // set scope id attribute for scoped CSS.
  // this is implemented as a special case to avoid the overhead
  // of going through the normal attribute patching process.
  function setScope (vnode) {
    // ...
  }

  function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    // ...
  }

  function invokeDestroyHook (vnode) {
    // ...
  }

  function removeVnodes (vnodes, startIdx, endIdx) {
    // ...
  }

  function removeAndInvokeRemoveHook (vnode, rm) {
    //...
  }

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
  }

  function checkDuplicateKeys (children) {
    // ...
  }

  function findIdxInOld (node, oldCh, start, end) {
    // ...
  }

  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // ...
  }

  function invokeInsertHook (vnode, queue, initial) {
    // ...
  }

  let hydrationBailed = false
  // list of modules that can skip create hook during hydration because they
  // are already rendered on the client or has no need for initialization
  // Note: style is excluded because it relies on initial clone for future
  // deep updates (#7063).
  const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')

  // Note: this is a browser-only function so we can assume elms are DOM nodes.
  function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
    // ...
  }

  function assertNodeMatch (node, vnode, inVPre) {
    // ...
  }

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
  }
}

可以看到,這個函式主要做了三件事:

  • 首先對本地的hooks和傳入的modules做了一次遍歷

    透過查詢可以看到,modules是以下兩個陣列合並的結果:

    // src/platforms/web/runtime/modules/index.js
    export default [
      attrs,
      klass,
      events,
      domProps,
      style,
      transition
    ]
    // src/core/vdom/modules/index.js
    export default [
      ref,
      directives
    ]

    首先函式中定義了一個本地變數cbs,透過遍歷hooks在cbs上新增名為hooks[i]的屬性,屬性對應的值為陣列;接著再透過巢狀遍歷modules,如果modules[j]中存在與hooks[i]同名的屬性,就將此屬性對應的值(函式)塞進陣列。

    可以看出此巢狀遍歷就是找出hooks對應的所有回撥。

  • 然後定義了一系列的內部方法和變數

    這些方法基本就是用於vnode的操作,比對、更新、移除、建立節點等等。

  • 最後返回了一個函式patch,即vue例項的__patch__方法

3. 呼叫vm.__patch__

呼叫vm.__patch__方法,即呼叫了下面的patch函式。

// src/core/vdom/patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // update parent placeholder node element, recursively
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

根據前面的步驟vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */),可知傳入的引數分別是vm.$elvnodehydratingfalse,可以得出:

  • isUndef(vnode)為false
  • isUndef(oldVnode)為false
  • const isRealElement = isDef(oldVnode.nodeType)為true,真實dom節點

    執行oldVnode = emptyNodeAt(oldVnode),根據下述程式碼:

    function emptyNodeAt (elm) {
      return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
    }

    可知根據此真實dom節點建立了一個對應的虛擬節點vnode,並給它設定以下屬性:

    • tag:真實dom的標籤
    • data:空物件
    • children:空陣列
    • text:undefined
    • elm:原真實dom
  • sameVnode(oldVnode, vnode)為false
  • (ssr暫時不管)
  • isDef(vnode.parent)為false(根節點的話)

故主要關注下面這段程式碼:

// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  // extremely rare edge case: do not insert if old element is in a
  // leaving transition. Only happens when combining transition +
  // keep-alive + HOCs. (#4590)
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
// src/core/vdom/patch.js
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

nested未傳遞為undefined,所以vnode.isRootInsert被賦值為true;

接著進入if判斷執行createComponent(vnode, insertedVnodeQueue, parentElm, refElm)函式:

// src/core/vdom.patch.js createPatchFunction的內部函式
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    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.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

可以看到在此處呼叫了data.hook上的init方法,即上述在create-component.jscomponentVNodeHooks的init對應方法:

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 {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},

可以看到在init方法中,當vnode.componentInstance不存在時,即vnode對應的元件例項不存在時,會呼叫createComponentInstanceForVnode來建立元件例項。

// src/core/vdom/create-component.js
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)
}

createComponentInstanceForVnode函式中,取出vnode對應元件的構造器Ctor進行例項化操作並傳入引數,使用new操作建立新的元件例項。

由前文可知,此構造器函式繼承自Vue,在例項化時會呼叫例項_init方法。

當元件例項建立完成後,會繼續執行元件例項的$mount方法,即這一步:child.$mount(hydrating ? vnode.elm : undefined, hydrating),進入vnode對應元件的掛載操作,即重新走一遍上述的流程。

在該元件的_init過程中,會取出構造器的options中的render方法掛在元件例項的$options上。

現在主要看該render()方法,此方法在vue-loader中透過模板解析生成。

vue-loader生成的render方法

1. vue-loader

vue-loader/lib/loader.js

const parts = parse(
  content,
  fileName,
  this.sourceMap,
  sourceRoot,
  cssSourceMap
)

透過vue-loader/lib/parser.js檔案中匯出的方法將傳入的內容解析:

module.exports = (content, filename, needMap, sourceRoot, needCSSMap) => {
  const cacheKey = hash((filename + content).replace(/\\/g, '/'))
  let output = cache.get(cacheKey)
  if (output) return output
  output = compiler.parseComponent(content, { pad: 'line' })
  if (needMap) {
    if (output.script && !output.script.src) {
      output.script.map = generateSourceMap(
        filename,
        content,
        output.script.content,
        sourceRoot
      )
    }
    if (needCSSMap && output.styles) {
      output.styles.forEach(style => {
        if (!style.src) {
          style.map = generateSourceMap(
            filename,
            content,
            style.content,
            sourceRoot
          )
        }
      })
    }
  }
  cache.set(cacheKey, output)
  return output
}

parser呼叫了vue-template-compiler/build.js中的parseComponent函式,將內容解析為四部分:script、styles、template和customBlocks(自定義部分)。

// vue-template-compiler/build.js
var isSpecialTag = makeMap('script,style,template', true);
// vue-template-compiler/build.js
if (isSpecialTag(tag)) {
  checkAttrs(currentBlock, attrs);
  if (tag === 'style') {
    sfc.styles.push(currentBlock);
  } else {
    sfc[tag] = currentBlock;
  }
} else { // custom blocks
  sfc.customBlocks.push(currentBlock);
}

繼續看loader的解析:vue-loader/lib/loader.js

// vue-loader/lib/loader.js
const functionalTemplate = templateAttrs && templateAttrs.functional

output += '/* template */\n'
const template = parts.template
if (template) {
  if (options.esModule) {
    output +=
      (template.src
        ? getImportForImport('template', template)
        : getImport('template', template)) + '\n'
  } else {
    output +=
      'var __vue_template__ = ' +
      (template.src
        ? getRequireForImport('template', template)
        : getRequire('template', template)) +
      '\n'
  }
} else {
  output += 'var __vue_template__ = null\n'
}

// template functional
output += '/* template functional */\n'
output +=
  'var __vue_template_functional__ = ' +
  (functionalTemplate ? 'true' : 'false') +
  '\n'

parts.template.attrs物件上如果沒有functional屬性,__vue_template_functional__就為false。

繼續看esm並且沒有src的分支。

// vue-loader/lib/loader.js
function getImport (type, part, index, scoped) {
  return (
    'import __vue_' + type + '__ from ' +
    getRequireString(type, part, index, scoped)
  )
}
// vue-loader/lib/loader.js
function getRequireString (type, part, index, scoped) {
  return loaderUtils.stringifyRequest(
    loaderContext,
    // disable all configuration loaders
    '!!' +
      // get loader string for pre-processors
      getLoaderString(type, part, index, scoped) +
      // select the corresponding part from the vue file
      getSelectorString(type, index || 0) +
      // the url to the actual vue file, including remaining requests
      rawRequest
  )
}
// vue-loader/lib/loader.js
function getRawLoaderString (type, part, index, scoped) {
  let lang = part.lang || defaultLang[type]

  let styleCompiler = ''
  if (type === 'styles') {
    // ...
  }

  let loader =
    options.extractCSS && type === 'styles'
      ? loaders[lang] || getCSSExtractLoader(lang)
      : loaders[lang]

  const injectString =
    type === 'script' && query.inject ? 'inject-loader!' : ''

  if (loader != null) {
    if (Array.isArray(loader)) {
      loader = stringifyLoaders(loader)
    } else if (typeof loader === 'object') {
      loader = stringifyLoaders([loader])
    }
    if (type === 'styles') {
      // ...
    }
    // if user defines custom loaders for html, add template compiler to it
    if (type === 'template' && loader.indexOf(defaultLoaders.html) < 0) {
      loader = defaultLoaders.html + '!' + loader
    }
    return injectString + ensureBang(loader)
  } else {
    // unknown lang, infer the loader to be used
    switch (type) {
      case 'template':
        return (
          defaultLoaders.html +
          '!' +
          templatePreprocessorPath +
          '?engine=' +
          lang +
          '!'
        )
      // ...
    }
  }
}

最後將所有內容傳入一個函式中執行

output +=
  'var Component = normalizeComponent(\n' +
  '  __vue_script__,\n' +
  '  __vue_template__,\n' +
  '  __vue_template_functional__,\n' +
  '  __vue_styles__,\n' +
  '  __vue_scopeId__,\n' +
  '  __vue_module_identifier__\n' +
  ')\n'

normalizeComponent函式:

output +=
  'var normalizeComponent = require(' +
  loaderUtils.stringifyRequest(loaderContext, '!' + componentNormalizerPath) +
  ')\n'

componentNormalizerPath函式:

const componentNormalizerPath = normalize.lib('component-normalizer')
// vue-loader/lib/component-normalizer.js
module.exports = function normalizeComponent (
  rawScriptExports,
  compiledTemplate,
  functionalTemplate,
  injectStyles,
  scopeId,
  moduleIdentifier /* server only */
) {
  var esModule
  var scriptExports = rawScriptExports = rawScriptExports || {}

  // ES6 modules interop
  var type = typeof rawScriptExports.default
  if (type === 'object' || type === 'function') {
    esModule = rawScriptExports
    scriptExports = rawScriptExports.default
  }

  // Vue.extend constructor export interop
  var options = typeof scriptExports === 'function'
    ? scriptExports.options
    : scriptExports

  // render functions
  if (compiledTemplate) {
    options.render = compiledTemplate.render
    options.staticRenderFns = compiledTemplate.staticRenderFns
    options._compiled = true
  }

  // functional template
  if (functionalTemplate) {
    options.functional = true
  }

  // ...

  return {
    esModule: esModule,
    exports: scriptExports,
    options: options
  }
}

__vue_template_functional__為false的情況,即functionalTemplate為false。

可以看到是把compiledTemplate.render放在了返回的物件的options上。

所以就是要看compiledTemplate.render的定義。

2. vue-template-compiler

在上述vue-loader/lib/loader.js中的getRawLoaderString函式定義中,可以看到使用了defaultLoaders.html這個loader來處理template中的html內容。

// vue-loader/lib/loader.js
const defaultLoaders = {
  html: templateCompilerPath + templateCompilerOptions,
  // ...
}

這個loader定義在template-compiler/index.js檔案中:

可以看到此loader的返回中包含以下程式碼:

// template-compiler/index.js
code =
  transpile(
    'var render = ' +
      toFunction(compiled.render, stripWithFunctional) +
      '\n' +
      'var staticRenderFns = [' +
      staticRenderFns.join(',') +
      ']',
    bubleOptions
  ) + '\n'

這就是vue-loader生成的render方法!

// template-compiler/index.js
function toFunction (code, stripWithFunctional) {
  return (
    'function (' + (stripWithFunctional ? '_h,_vm' : '') + ') {' + code + '}'
  )
}

compiled的定義:

// template-compiler/index.js
const compiled = compile(html, compilerOptions)

compile的定義:

// vue-template-compiler/build.js
var ref = createCompiler(baseOptions);
var compile = ref.compile;

createCompiler的定義:

// vue-template-compiler/build.js
var createCompiler = createCompilerCreator(function baseCompile (
  template,
  options
) {
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
    optimize(ast, options);
  }
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
});

可以看到baseCompile函式做了三件事:

  • 根據options配置,將template轉為ast
  • 呼叫optimize最佳化ast
  • 透過執行generate得到最終的code

可以看到render方法中的具體程式碼,是透過generate方法將ast轉換得到:

// vue-template-compiler/build.js
function generate (
  ast,
  options
) {
  var state = new CodegenState(options);
  // fix #11483, Root level <script> tags should not be rendered.
  var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
  return {
    render: ("with(this){return " + code + "}"),
    staticRenderFns: state.staticRenderFns
  }
}

可以看到此處的render是一個字串,最終會透過上述template-compiler/index.js檔案中的toFunction轉為函式。

genElement就是分別處理不同的元素內容,最終得到的code會被設定到render的函式體中,在render被執行時,code部分的程式碼就會被執行。

// vue-template-compiler/build.js
function genElement (el, state) {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre;
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    var code;
    if (el.component) {
      code = genComponent(el.component, el, state);
    } else {
      var data;
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData$2(el, state);
      }

      var children = el.inlineTemplate ? null : genChildren(el, state, true);
      code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
    }
    // module transforms
    for (var i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code);
    }
    return code
  }
}

看下這裡的genIf

// vue-template-compiler/build.js
function genIf (
  el,
  state,
  altGen,
  altEmpty
) {
  el.ifProcessed = true; // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions,
  state,
  altGen,
  altEmpty
) {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  var condition = conditions.shift();
  if (condition.exp) {
    return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
  } else {
    return ("" + (genTernaryExp(condition.block)))
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

從return的程式碼字串中可以看出,在render方法被呼叫時,v-if中的表示式即condition.exp會被求值,又此時vue例項在呼叫$mount時已經建立了自身對應的renderWatcher,加上資料經過響應式改造,v-if中被訪問的屬性其對應的getter會被觸發,也就收集到了元件渲染的依賴。

其他元素中的表示式也是類似,會被收集為元件渲染的依賴。

小結

父元件呼叫$mount方法時,執行了mountComponent函式,觸發beforeMount鉤子,然後會建立元件自身的renderWatcher,在watcher初始化過程中會呼叫_render方法,然後呼叫_update方法。

render執行過程中,基於Vue建立了一個元件子類,接著生成虛擬節點vnode,並且此vnode的data屬性會掛上一些hook方法。

_update內部呼叫__patch__方法時,呼叫了createComponent(vnode, insertedVnodeQueue, parentElm, refElm)方法,呼叫了此vnode的data屬性上hooks中的init建立了對應的元件例項,在元件例項化過程中透過呼叫_init對該例項進行初始化,然後呼叫$mount例項方法,在呼叫$mount時,該例項也會建立一個自身的renderWatcher。

子元件對應.vue檔案透過vue-loader解析,在template解析時得到其對應的render方法,在render方法被呼叫時,模板中對應的表示式會被求值,即元件的資料會被訪問,就被收集為元件渲染的依賴。

mountComponent函式的最後,觸發了mounted鉤子。

相關文章