runtime-only & runtime-with-compiler
在用vue-cli
构建应用时,一般会让我们做如下选择:
Runtime + Compiler:recommended for most usersRuntime-only: about 6KB lighter min+gzip, but templates...
其原因是$mount
的实现方式在不同平台构建时具有差异性。 compiler
的版本和runtime-only
版本的差异就在于:
runtime-only
版本是只包含Vue.js
运行时的代码,体积更轻量,通常需要借助vue-loader
将.vue
文件编译为.js
,而compiler
版本会在执行的过程中直接预编译。
$mount
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating)}
$mount
方法接收两个参数,一是挂载对象,第二个跟服务器渲染相关,浏览器环境不需要传。其内部实际调用了/src/core/instance/lifecycle
里的mountComponent
方法:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component { vm.$el = el // ... callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // ... } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm}
为了看着简洁,此处的源码删除了一些判断逻辑。
callHook(vm, 'beforeMount')
- 调用
beforeMount
生命钩子函数。
实例化 Watcher
- 传入上面定义的
updateComponent
作为回调函数 这里有两个关键点:
- 在初始化显示界面的时候,会执行
updateComponent
,在调用render
生成vnode
并且执行update
渲染界面的过程中,会对组件内data
取值,此时会进行依赖收集。 - 在更新数据时,由于依赖收集劫持了
set
方法,会通知去更新界面,于是又一次调用updateComponent
去更新界面。
- 在初始化显示界面的时候,会执行
callHook(vm, 'mounted')
- 设置
vm._isMounted = true
,代表已经挂载完毕 - 调用
mounted
生命钩子函数
vm._render()
上面提到了,_render
是生成vnode
的关键,其源码的关键在:
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options let vnode try { vnode = render.call(vm._renderProxy, vm.$createElement) } return vnode }}
需要了解的一点是,Vue
最终都是通过render
函数产生vnode
在去渲染真实DOM,就算使用template
,最终也会在编译的过程中,将template
转化为可执行的render
函数,这个编译的过程十分复杂,在这里就不详细说明了。
看看render
方法的参数vm._renderProxy
和 vm.$createElement
还熟悉吗?在文章 我还是不够了解Vue - Vue的实例化 的 vue.prototype._init
中有这样一段代码。
if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } initRender(vm) // ...
前者其实就是vm
实例本身,后者则是调用initRender
中定义了vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
。
总的来说,vm._render
最终通过 render
中的 createElement
方法得到 vnode
。
Virtual Dom
用最简单的话讲:Virtual Dom
就是一个可以描述 DOM
的JS
对象。
而在Vue
的体现就是上面说的vnode
,通过大量的属性去抽象一个DOM
节点,包括了标签、数据、文本、子节点等等。
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance }}
上面的_render
只讲述了vnode
的 create
过程,而要将整个Virtual Dom
映射到真实 DOM
上,还需要经历update
的过程。
vm._update
下面精简代码中核心的就是__patch__
方法了,preVnode
起到了判断初始化还是更新的作用。。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } }
__patch__
在不同平台有不同的表现,这里以浏览器运行环境举例 Vue.prototype.__patch__ = inBrowser ? patch : noop
。
patch
patch
函数实际是createPatchFunction
的返回值,createPatchFunction
的内部实现非常复杂,大概有 700
多行代码,就不复制过来了,其源码路径为 'core/vdom/patch'
。
patch
函数: return function patch (oldVnode, vnode, hydrating, removeOnly) {}
,接收4个参数,分别是,旧的节点、新节点、是否为服务器渲染、removeOnly
和<transition-group>
有关。
它内部调用的比较重要的辅助函数是 createElm
、 patchVnode
createElm
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) if (__WEEX__) { } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } } 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) } }
首先会对tag
进行判断和校验,然后通过 nodeOps
中的方法创建一个临时元素,内部其实就是熟悉的document.createElement
等等方法。
接着会开始创建子元素createChildren
,内部会遍历子节点,递归调用 createElm
。
最后调用 insert
方法把 DOM
插入到父节点,这里因为递归,子元素会先进行 insert
,它原理就是通过appendChild
实现的。
patchVnode
组件差异化比较的逻辑,其核心步骤大概为3部。
- 判断是否为静态节点,由于
vue
在编译阶段做了优化,将不会再改变的节点做了标记,于是在这个阶段可以直接元素服用,从而提高效率。 - 更新节点,调用
updateAttrs
、updateClass
等等module
目录中的方法。 - 比较新旧节点,最重要的就是
updateChildren
方法,实现了diff
算法。
总结
这里初略的叙述了vue
挂载过程,其中子组件创建过程
、编译render函数
、响应式
、diff算法
是比较重要的的内容,会在后期单独记录。