家喻户晓,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 argif (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 argif (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.jsexports.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.jsconst 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.jsmodule.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.jsimport 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 functionVue.prototype.__patch__ = inBrowser ? patch : noop// public mount methodVue.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.jsexport 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.jsVue.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.jsexport 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.jsexport 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.jsexport 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.jsVue.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.jsimport { patch } from './patch'// install platform patch functionVue.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.jsconst 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.jsexport default [  attrs,  klass,  events,  domProps,  style,  transition]
    // src/core/vdom/modules/index.jsexport 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.jsreturn 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 nodecreateElm(  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.jsfunction 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.jsexport 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.jsvar isSpecialTag = makeMap('script,style,template', true);
// vue-template-compiler/build.jsif (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.jsconst functionalTemplate = templateAttrs && templateAttrs.functionaloutput += '/* template */\n'const template = parts.templateif (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 functionaloutput += '/* 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.jsfunction getImport (type, part, index, scoped) {  return (    'import __vue_' + type + '__ from ' +    getRequireString(type, part, index, scoped)  )}
// vue-loader/lib/loader.jsfunction 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.jsfunction 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.jsmodule.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.jsconst defaultLoaders = {  html: templateCompilerPath + templateCompilerOptions,  // ...}

这个loader定义在template-compiler/index.js文件中:

能够看到此loader的返回中蕴含以下代码:

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

这就是vue-loader生成的render办法!

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

compiled的定义:

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

compile的定义:

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

createCompiler的定义:

// vue-template-compiler/build.jsvar 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.jsfunction 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.jsfunction 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.jsfunction 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钩子。