Vue 源码学习 (2) - Vue 初始化
_init
当我们使用 Vue 2
时,我们会用 Vue
实例化一个应用
const app = new Vue({ render: h => h(App) }).$mount('#app')
Vue
是一个构造函数,实例化时会调用 this._init(options)
方法.
function Vue(options) { this._init(options) }
找到 core/instance/init.ts
文件中, initMixin
函数执行时挂载了 _init
方法
Vue.prototype._init = function (options?: Record<string, any>) { const vm: Component = this // a uid vm._uid = uid++ if (options && options._isComponent) { initInternalComponent(vm, options as any) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor as any), options || {}, vm ) } initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) initState(vm) initProvide(vm) callHook(vm, 'created') if (vm.$options.el) { vm.$mount(vm.$options.el) } }
这里的逻辑我大致讲一下,
uid
递增- 往实例上加一些属性标识
$options
初始化,合并extend
,mixin
等- 初始化声明周期,初始化
render
beforeCreate
- 初始化
inject
- 初始化
props
->methods
->data
->computed
->watch
- 初始化
provide
created
- 开始
$mount
$mount
上面的流程大致过下,接下来我们看看 $mount
执行的过程。
在 web/runtime-with-compiler.ts
中我们讲过,这里将 compileToFunctions
函数引入进来,用于讲 template
转化成 render
函数,这里我们也讲一下详细的逻辑
// 不能挂在到 html 或者 body 下 if (el === document.body || el === document.documentElement) return if (!options.render) { // 如果 template 存在 // 字符串 // 直接解析 // 通过 id 获取元素,然后获取 innerHTML // 元素 获取 innerHTML // 如果 templet 不存在 ,el 存在 获取 outerHTML // 执行 compileToFunctions 得到 render // 执行缓存的原方法 return mount.call(this, el, hydrating) }
来到 web/runtime/index.ts
, 这里就是 runtime
时 $mount
方法
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
这里调用了 core/instance/lifecycle
的 mountComponent
方法
export function mountComponent( vm: Component, el: Element | null | undefined, hydrating?: boolean ): Component { // 声明 $el,检查 render vm.$el = el if (!vm.$options.render) { vm.$options.render = createEmptyVNode } callHook(vm, 'beforeMount') let updateComponent = () => { vm._update(vm._render(), hydrating) } const watcherOptions: WatcherOptions = { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } } new Watcher( vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */ ) if (vm.$vnode == null) { const preWatchers = vm._preWatchers if (preWatchers) { for (let i = 0; i < preWatchers.length; i++) { preWatchers[i].run() } } vm._isMounted = true callHook(vm, 'mounted') } return vm }
这里实例化了一个 渲染的 Watcher
, 在它的回调中会调用 updateComponent
方法,执行 _render
, _update
更新 DOM
这里还有一个判断 vm.$node === null
, 这里 $node
表示父 vnode
,为 null
则表示为根结点的实例.
Watcher 我们先不看,这主要是响应式原理和依赖收集相关的部分,我们继续看 _render
, _update
方法的逻辑.
_render
在 instance/render.ts
中, renderMixin
函数注册了 _render
方法.
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options vm.$vnode = _parentVnode! let vnode = render.call(vm._renderProxy, vm.$createElement) // set parent vnode.parent = _parentVnode return vnode }
_render
函数调用 render
生成 vnode
进行返回
平时我们使用 render
函数时是这么写的:
render: h => h('div', {}, '')
这里的 h
就对应了 vm.$reateElement
, 而 renderProxy
就是响应式的数据
其中 vm.$createElement
在 initRender
时进行了注册,实际上也是对 createElement
方法进行了封装。
createElement
在 core/vdom/create-element.ts
中定义了 createElement
函数,该函数是对 _createElement
的封装,让参数更加灵活
export function _createElement( context: Component, tag?: string | Component | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { // 处理 :is if (isDef(data) && isDef(data.is)) { tag = data.is } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor if (config.isReservedTag(tag)) { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (...) { vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode(tag, data, children, undefined, undefined, context) } } else { // direct component options / constructor vnode = createComponent(tag as any, data, context, children) } return vnode }
这里主要执行的逻辑是,先将 children
规范为 VNode
数组, normalizationType
标识规范类型。
然后创建 VNode
, 对 tag
进行判断,
- . 如果是
tag
是string
类型- . 内置
HTML
元素的话,直接创建普通node
- . 已经注册过的组件名,则使用
createComponent
创建组件VNode
- . 创建未知标签的
vnode
- . 内置
- . 如果是
component
类型,直接创建component
组件VNode
到这里我们大致讲完了 createElement
的逻辑了。
update
在 mountComponent
方法中, _render
执行完成后,会执行 _update
方法
vm._update(vm._render(), hydrating)
查看 _update
方法
Vue.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 if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) } else { 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 } }
这里的核心逻辑是 __patch__
方法. 由于 Vue
不只面向 web
平台, 也会处理其他平台的逻辑,例如死去的 Weex
. 而且在 Vue SSR
时,也是不需要 patch 的
。这里我们看 web
端的 patch
方法。
function createPatchFunction({ nodeOps, modules }) { return function patch(oldVnode, vnode, hydrating, removeOnly) { let isInitialPatch = false const insertedVnodeQueue: any[] = [] if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) oldVnode = emptyNodeAt(oldVnode) // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, 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) } const insert = ancestor.data.hook.insert if (insert.merged) { for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
这里主要通过 cretateElem
创建元素
if (isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode) } 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) 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) }
这里的逻辑就是通过 VNode
创建正式的 DOM
然后插入到它的父节点中。
这里先创建 elm
元素,然后创建 children
,遍历子节点,递归调用 createElm
, 然后调用 invokeCreateHook
。
最后调用 insert
方法插入到 父节点,所以插入节点的顺序是 子>父 的。
总结
最后回顾下整个 Vue 初始化的流程:
init
初始化声明周期,数据$mount
开始挂载compile template to render fn
render
生成VNode
patch VNode
到DOM
上
从 patch
的过程我们也知道了从 虚拟节点 到 DOM 的一个生成逻辑,所以有一个常见的面试题,就是父子组件的生命周期问题,这个问题的答案现在就很简单了
父亲:F 子组件: C
F beforeCreate -> created -> beforeMount -> C beforeCreate -> created -> beforeMount -> Mounted -> F Mounted
上面我们大致讲了下 patch
的过程,很多关于 新旧节点的比较逻辑,diff 算法优化并没有细聊。后续会有文章, 我们接着聊。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论