家喻户晓,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
。
因为只有context
和tag
两个入参:vm
和App
,所以能够间接跳到看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.$el
、vnode
、hydrating
、false
,能够得出:
isUndef(vnode)
为falseisUndef(oldVnode)
为falseconst 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.js
中componentVNodeHooks
的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
钩子。