本文基于 Vue 2.6.14 进行源码剖析
为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码
文章内容
- 流程图展现 Vue2 初始化渲染流程
- 源码 (删减、调整程序) 剖析无 / 有 Component 时的渲染流程
- 用简略例子,进行整体流程的剖析
整体流程图
流程图代码剖析
_init():初始化逻辑
- 初始化生命周期
- 初始化 event
- 初始化 createElement 等渲染办法
- 生命周期
beforeCreate
调用 - 初始化 props、methods、data、computed、watch
- 生命周期
created
调用 -
vm.$mount
渲染到实在 DOM 上function Vue (options) {this._init(options); } Vue.prototype._init = function (options) { const vm = this; // 合并配置 vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm ); initLifecycle(vm); // 初始化生命周期 initEvents(vm); // 初始化 event initRender(vm); // 初始化 createElement 等渲染办法 callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props initState(vm); // 初始化 props、methods、data、computed、watch initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); if (vm.$options.el) {vm.$mount(vm.$options.el); } };
实例挂载剖析
Vue.$mount 流程
从上面的代码剖析能够晓得,
Vue.$mounte
首先会判断是否有render()
办法,如果没有手写render()
办法,只有<template>
,那得先把template
转化为render()
的模式,最终所有渲染都得转化为render()
办法// node_modules/vue/src/platforms/web/entry-runtime-with-compiler.js const mount = Vue.prototype.$mount; // 在原来 $mount()根底上再封装一层逻辑,而后调用原来的 $mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component {el = el && query(el) const options = this.$options if (!options.render) { let template = options.template if (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template) } } 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) {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 } } return mount.call(this, el, hydrating) }
初始化渲染 Watcher
由上面代码能够晓得,转化
render()
会进行渲染 Watcher
的注册,而后调用生命周期mounted
调用
从上面代码剖析也能够晓得,最终渲染触发的办法是vm._update(vm._render(), hydrating)
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component {el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) } // node_modules/vue/src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) {vm.$options.render = createEmptyVNode} callHook(vm, 'beforeMount') // 删除源码中的 if 分支 let updateComponent; 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 }
首次渲染会触发 new Watcher 的渲染,因为首次渲染
vm._isMounted=false
,因而不会调用生命周期beforeUpdate
,只有下一次渲染才会触发生命周期beforeUpdate
的打印vm._render()
最终通过调用
render()
办法进行渲染,而后返回VNode
数据render
函数传入vm.$createElement
进行渲染
在下面下面initRender()
的剖析中,咱们晓得vm.$createElement=createElement
而createElement
实际上会调用_createElement
// node_modules/vue/src/core/instance/render.js Vue.prototype._render = function (): VNode { // ... const {render, _parentVnode} = vm.$options vnode = render.call(vm._renderProxy, vm.$createElement) if (Array.isArray(vnode) && vnode.length === 1) {vnode = vnode[0] } if (!(vnode instanceof VNode)) {vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode }
_createElement()
- VNode 的 children 节点进行解决,可能是任意类型,咱们须要解决为标准的
length=1
的VNode
数组 - 依据
tag
进行VNode
的创立,比方Component
组件类型,须要调用不同的创立办法 -
最初返回创立的
VNode
function _createElement(context, tag, data, children, normalizationType) { // children 的整顿和规范化 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); } // 依据 tag 做类型判断,是要间接创立 createVNode 还是 createComponent // 实质都是返回 VNode 数据 var vnode, ns; if (typeof tag === 'string') { var Ctor; ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag); if (config.isReservedTag(tag)) { 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 { vnode = new VNode( tag, data, children, undefined, undefined, context ); } } else {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() } }
createComponent():创立组件类型的 VNode
如果遇到组件类型,_createElement()则调用 createComponent()进行组件 VNode 的创立
- 继承 Vue 函数,构建扩大后的
Constructor()
办法 - 合并 4 个钩子到
VNodeData.hook
中,不便后续逻辑调用 - 传入下面构建的
Ctor
和VNodeData
作为参数,实例化VNode
-
返回
VNode
// node_modules/vue/src/core/vdom/create-component.js export function createComponent(...args): VNode | Array<VNode> | void { // core/global-api/index.js: Vue.options._base = Vue // 因而 baseCtor = Vue const baseCtor = context.$options._base if (isObject(Ctor)) {// Vue.extend = function (extendOptions: Object): Function {// const Sub = function VueComponent(options) {// this._init(options) // } // } Ctor = baseCtor.extend(Ctor); // 返回 Vue 的继承类,继承根底上扩大一些性能 } // 合并 4 个钩子函数到 VNodeData.hook 中,不便后续逻辑调用 installComponentHooks(data) // 创立 vue-component 类型的 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 ) return vnode } const componentVNodeHooks = {init (...){}, prepatch (...){}, insert (...){}, destroy (...){}} const hooksToMerge = Object.keys(componentVNodeHooks) function installComponentHooks(data: VNodeData) {const hooks = data.hook || (data.hook = {}) for (let i = 0; i < hooksToMerge.length; i++) {const key = hooksToMerge[i] const existing = hooks[key] const toMerge = componentVNodeHooks[key] if (existing !== toMerge && !(existing && existing._merged)) {hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } }
vm._update()
- 作用:获取
vm._render()
渲染的VNode
,进行实在DOM
的渲染 -
流程:分为 3 种状况进行剖析,外围是调用
createElm()
办法进行 VNode 的渲染Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { // ... if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // updates vm.$el = vm.__patch__(prevVnode, vnode); } // ... } // node_modules/vue/src/platforms/web/runtime/index.js Vue.prototype.__patch__ = inBrowser ? patch : noop // node_modules/vue/src/platforms/web/runtime/patch.js export const patch: Function = createPatchFunction({nodeOps, modules})
// node_modules/vue/src/core/vdom/patch.js const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction(backend) {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]]) } } } return function patch(oldVnode, vnode, hydrating, removeOnly) {if (isRealElement) {oldVnode = emptyNodeAt(oldVnode) } // 三种情景代码... } }
patch 情景 1: 初始化 root/ 渲染更新 - 无可复用的 VNode
return function patch(oldVnode, vnode, hydrating, removeOnly) {if (isRealElement) {oldVnode = emptyNodeAt(oldVnode) } var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); } else { // 初始化 root 时调用 // index.html 的 id='app' const oldElm = oldVnode.elm // id='app' 的 <div> 的 parent,即 body 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) ) // destroy old node if (isDef(parentElm)) {removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode) } } }
- 初始化 root 时调用,进行
newVNode
的创立,而后插入到id=app
的旁边,而后删除<div id="app">
的 DOM,如上面代码所示 -
渲染更新 - 无可复用的 VNode,监测到
sameVnode()=false
,阐明以后 VNode 无奈复用,不是之前那个VNode
,间接从新建设一个新的VNode
,而后将旧的VNode
删除(跟初始化 root 流程差不多)// 初始化 root // patch 之前的状态 <div id='app'></div> // createElm 之后的状态 <div id='app'></div> <div id='app1'></div> // destroy old node <div id='app1'></div>
patch 情景 2: 渲染更新 - 可复用的 VNode 进行 patchVnode
监测到
sameVnode()=true
,阐明以后 VNode 可复用,间接进行数据更新,以及它们的children
的diff
比拟,找出children
可复用的中央(不可复用的中央得从新创立和销毁)var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); } function sameVnode(a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) }
注:当两个 VNode 的 key、tag、isComment、VNodeData、inputType 都雷同时,阐明是同一个节点,只是有所扭转,能够进行复用
patchVnode()流程
- 因为 oldVNode 和 newVNode 是同一个节点(sameVnode=true),尝试将 oldVNode 转化为新 VNode,包含 props、listeners、slot 等更新
- 执行
update
的钩子函数(自定义指令注册) -
依据它们各自的 children 进行分组解决
- oldVNodeChildren!==newVNodeChildren,进行 diff 算法比对更新
- oldVNodeChildren 为空,newVNodeChildren 不为空,执行 newVNodeChildren 新建插入操作
- oldVNodeChildren 不为空,newVNodeChildren 为空,执行 oldVNodeChildren 删除操作
- 如果是文本节点,则更新文本内容
-
执行
postpatch
的钩子函数(自定义指令注册)具体代码剖析请看下一篇文章 Vue2 双端比拟 diff 算法 -patchVNode 流程浅析
patchVnode()总结概述
更新两个VNode
的数据,并且比对两个VNode
的chlidren
,先进行简略的解决,如果有其中一个不存在,则间接执行create
/remove
操作,如果两者都存在,才须要调用updateChildren()
进行比照和复用patch 情景 3: Component 内部结构渲染
遇到组件渲染时,应用
if (isUndef(oldVnode)) {// empty mount (likely as component), create new root element isInitialPatch = true; createElm(vnode, insertedVnodeQueue); }
外围办法 createElm()- 非 component 渲染
流程图
- 作用:通过
VNode
创立实在的 DOM 节点并插入 -
流程:
- document.createElement 创立
vnode.elm
- 遍历 children,进行
createElm()
的递归调用 - 调用所有
生命周期 create
的办法 - 调用
Node.appendChild
/Node.insertBefore
办法将VNode.elm
挂载的DOM
元素插入到目前 parentElm
如果是初始化,此时的 parentElm=
<Body></Body>
// node_modules/vue/src/core/vdom/patch.js function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return;} const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { // 有标签的内容 // 实质是 document.createElement 创立实在 DOM 的元素 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) // if (Array.isArray(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))) // } createChildren(vnode, children, insertedVnodeQueue); // 办法内容如下面正文代码 if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue); // 办法内容如上面正文代码 // for (let i = 0; i < cbs.create.length; ++i) {// cbs.create[i](emptyNode, vnode) // } // i = vnode.data.hook // Reuse variable // if (isDef(i)) {// if (isDef(i.create)) i.create(emptyNode, vnode) // if (isDef(i.insert)) insertedVnodeQueue.push(vnode) // } } // parentElm = body(初始化时) insert(parentElm, vnode.elm, refElm); // 办法内容如上面正文代码 // function insert (parent, elm, ref) {// if (isDef(parent)) {// if (isDef(ref)) {// if (nodeOps.parentNode(ref) === parent) {// // parent.insertBefore(elm, ref) // nodeOps.insertBefore(parent, elm, ref) // } // } else {// // parent.appendChild(elm) // nodeOps.appendChild(parent, elm) // } // }} } 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) } }
外围办法 createElm()- 有 component 渲染
由代码剖析能够晓得,会先调用
createComponent()
尝试进行Component
的创立,如果创立胜利,则不持续往下执行// node_modules/vue/src/core/vdom/patch.js function createElm(...args) {if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return} }
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {if (isDef((i = i.hook)) && isDef((i = i.init))) { // Component 外面的内容进行初始化和渲染 i(vnode, false /* hydrating */) // componentVNodeHooks.init()} if (isDef(vnode.componentInstance)) { // 拿到曾经渲染好的 Component 的 DOM 树:vnode.componentInstance.$el initComponent(vnode, insertedVnodeQueue) // 将曾经渲染好的 Component 的 DOM 树插入到 parentElm 之前占位的 <component> 局部 insert(parentElm, vnode.elm, refElm) } } var componentVNodeHooks = {init: function (vnode, hydrating) {var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)); child.$mount(hydrating ? vnode.elm : undefined, hydrating); } } function initComponent(vnode, insertedVnodeQueue) { // vnode.componentInstance.$el 此时就是曾经渲染的 Component 造成的 DOM 树 vnode.elm = vnode.componentInstance.$el }
- document.createElement 创立
- 由下面代码能够晓得,先调用了
componentVNodeHooks.init()
进行 Component 的外面内容的渲染:child.$mount
- Component 渲染实现后,将渲染实现的 DOM 挂载在
vnode.componentInstance.$el
上 -
而后再进行以后 Component 所在的占位符的 parent 的插入 children-DOM 的操作
DOM 的渲染程序因而是 先子后父
示例剖析 -Component 渲染
因为 createComponet 波及的点过多,因而应用例子进行独自剖析,次要是剖析创立 Component 所经验的流程
例子
具体代码请看 github-component 调试
<div id='el'>
</div>
<script type='text/x-template' id='demo-template'>
<div id='children1'>
<p id='children1_1'>Selected: {{selected}}</p>
<component-select :options='options' v-model='selected' id='children1_2_component'>
</component-select>
</div>
</script>
<script type='text/x-template' id='select2-template'>
<select id='children_component_select'>
<option disabled value='0' id='children_component_select_option'>Select one</option>
</select>
</script>
<script>
Vue.component('component-select', {.....})
var vm = new Vue({
el: '#el',
template: '#demo-template',
data: {
selected: 0,
options: [{ id: 1, text: 'Hello'},
{id: 2, text: 'World'}
]
}
})
</script>
从下面 html
内容能够晓得,最终是要渲染出 <component-select></component-select>
组件内容
由一开始的剖析能够晓得,最终 <template></template>
都会转化为 render()
函数,下面示例代码最终转化的 render()
函数是
// _v = createTextVNode;
// _c = function (a, b, c, d) {return createElement$1(vm, a, b, c, d, false); };
(function anonymous() {with (this) {
return _c('div', {
attrs: {"id": "children1"}
}, [_c('p', {
attrs: {"id": "children1_1"}
}, [_v("Selected:" + _s(selected))]), _v(""), _c('component-select', {
attrs: {
"options": options,
"id": "children1_2_component"
},
model: {value: (selected),
callback: function($$v) {selected = $$v},
expression: "selected"
}
})], 1)
}
})