-
以
src/platform/web/entry-runtime-with-compiler.js
中的$mount
为例// 缓存了原型上的 $mount 方法,再重新定义该方法 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) // el不能为html和body,Vue不能挂载在 body、html 这样的根节点上 /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) }
-
$mount
方法会去调用mountComponent
方法,这个方法定义在src/core/instance/lifecycle.js
核心:实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。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') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } 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 // 渲染Watcher 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 }
-
vm._render()
生成vnodeVue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots ) } // 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 { vnode = render.call(vm._renderProxy, vm.$createElement) // 调用render方法,返回vnode } 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) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } // 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)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode }
-
Virtual DOM
原生的javascript对象抽象描述DOM节点 (core/vdom/vnode.js) -
createElement
:_init
方法重调用initRender
,该方法中赋值了vm.$createElement
。(core/vdom/create-element.js)// wrapper function for providing a more flexible interface // without getting yelled at by flow 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> { // __ob__ 响应式对象 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 } // children normalize if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } // 创建vnode 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 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() } } // core/vdom/helpers/normalize-children.js // 数组降维 apply 和 concat 降维 // MDN 上对于 apply 方法的介绍 “The apply() method calls a function with a given this value and arguments provided as an array.” // apply 方法会调用一个函数,apply 方法的第一个参数会作为被调用函数的 this 值,apply 方法的第二个参数(一个数组,或类数组的对象)会作为被调用对象的 arguments 值,也就是说该数组的各个元素将会依次成为被调用函数的各个参数;将该特性应用到代码中:如果 concat 方法的参数是一个元素,该元素会被直接插入到新数组中;如果参数是一个数组,该数组的各个元素将被插入到新数组中; export function simpleNormalizeChildren (children: any) { for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { // 利用apply func.apply(thisArg, [argsArray]),给数组降维 return Array.prototype.concat.apply([], children) } } return children } function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> { const res = [] let i, c, lastIndex, last for (i = 0; i < children.length; i++) { c = children[i] if (isUndef(c) || typeof c === 'boolean') continue lastIndex = res.length - 1 last = res[lastIndex] // nested if (Array.isArray(c)) { if (c.length > 0) { c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`) // merge adjacent text nodes // 优化:上一次最后一个处理的结点和这一次处理的第一个节点都是文本节点,则合并 if (isTextNode(c[0]) && isTextNode(last)) { res[lastIndex] = createTextVNode(last.text + (c[0]: any).text) c.shift() } res.push.apply(res, c) } } else if (isPrimitive(c)) { if (isTextNode(last)) { // merge adjacent text nodes // this is necessary for SSR hydration because text nodes are // essentially merged when rendered to HTML strings res[lastIndex] = createTextVNode(last.text + c) } else if (c !== '') { // convert primitive to vnode res.push(createTextVNode(c)) } } else { if (isTextNode(c) && isTextNode(last)) { // merge adjacent text nodes res[lastIndex] = createTextVNode(last.text + c.text) } else { // default key for nested array children (likely generated by v-for) if (isTrue(children._isVList) && isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) { c.key = `__vlist${nestedIndex}_${i}__` } res.push(c) } } } return res }
-
vm._update(vm._render(), hydrating)
利用patch将vnode转化为真实的DOMVue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode // 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. }
-
__patch__
创建真实的DOM// platforms/web/runtion/index.js // install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop // 此处运用函数柯里化(???) // platforms/web/runtion/patch.js // 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 }) // core/vdom/patch.js (createPatchFunction 返回了patch方法) export function createPatchFunction (backend) { // 部分截取 function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // .... // 创建真实的dom vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) // ... // 插入dom节点 insert(parentElm, vnode.elm, refElm) } // 创建children function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children) } // 如果是array,递归调用createElm创建children for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } } // 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) ) // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } }
Vue源码解读2——Vue实例挂载实现
最新推荐文章于 2023-01-01 17:21:45 发布
本文深入解析Vue实例挂载过程,通过实例化渲染Watcher触发updateComponent,进而调用vm._render生成虚拟Node。接着,vm._update方法负责更新DOM,详细阐述如何将vnode转换为实际的DOM节点。
摘要由CSDN通过智能技术生成