关于前端:Vue-2x源码学习render方法模板解析和依赖收集

3次阅读

共计 34304 个字符,预计需要花费 86 分钟才能阅读完成。

家喻户晓,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 钩子。

正文完
 0