Vue 2.x源码学习:render方法、模板解析和依赖收集

内容乃本人学习Vue2源码的一点笔记,若有错误还望指正。
源码版本:
vue: 2.6
vue-loader: 13.x
vue-template-compiler: 2.6

之前的相关学习笔记:

众所周知,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钩子。

父子组件初始化时触发钩子的顺序:
父:beforeCreate => 父:created => 父:beforeMount => 子:beforeCreate => 子:created => 子:beforeMount => 子:mounted => 父:mounted

posted @ 2022-11-28 16:21  beckyye  阅读(197)  评论(0编辑  收藏  举报