Vue 源码学习 (2) - Vue 初始化

发布于 2023-10-14 12:56:38 字数 10470 浏览 42 评论 0

_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)
	}
}

这里的逻辑我大致讲一下,

  1. uid 递增
  2. 往实例上加一些属性标识
  3. $options 初始化,合并 extend , mixin
  4. 初始化声明周期,初始化 render
  5. beforeCreate
  6. 初始化 inject
  7. 初始化 props -> methods -> data -> computed -> watch
  8. 初始化 provide
  9. created
  10. 开始 $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/lifecyclemountComponent 方法

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.$createElementinitRender 时进行了注册,实际上也是对 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 进行判断,

  • . 如果是 tagstring 类型
    • . 内置 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 初始化的流程:

  1. init 初始化声明周期,数据
  2. $mount 开始挂载
  3. compile template to render fn
  4. render 生成 VNode
  5. patch VNodeDOM

patch 的过程我们也知道了从 虚拟节点 到 DOM 的一个生成逻辑,所以有一个常见的面试题,就是父子组件的生命周期问题,这个问题的答案现在就很简单了

父亲:F  子组件: C

F beforeCreate -> created -> beforeMount -> C beforeCreate -> created -> beforeMount -> Mounted -> F Mounted

上面我们大致讲了下 patch 的过程,很多关于 新旧节点的比较逻辑,diff 算法优化并没有细聊。后续会有文章, 我们接着聊。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文