time: 2019.11.11
author: heyunjiang
目录
1 根组件初始化
1.1 原型初始化
1.2 实例初始化
2 组件 $mount 方法执行 mount 主要包含了 render, update , watcher
3 组件 render - 生成 vnode tree
3.1 vm.$createElement
3.2 children 规范化
3.3 生成 vnode 节点
4 组件 update - 渲染 vnode tree
本章总结的数据驱动,也叫做 template -> vtree -> rtree
的一个转换过程
// vue 构造函数关键代码
function Vue (options) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
在 this._init()
函数执行过程中,前面都是初始化我们 vue 组件中定义好的 lifecycle
, events
, render
, injections
, state
, provide
。
在生命周期初始化之后,数据 injections 初始化之前,会触发生命周期 beforeCreate
,在数据 provide 初始化完成之后,触发生命周期 created
。
从这里是否可以联想到每个组件的初始化过程?每个组件都有 mixin 初始化,状态 state 初始化,事件 event 初始化,生命周期初始化,渲染 render 初始化
在自己做了这么久的 vue 项目,随着对 vue 的使用越来越熟练,对其原理也想知道。这里总结一下 vue 初始化的过程,归纳一下 vue 对象各属性、特点,追求逐步达到全面掌控 vue 对象,做到对 vue 胸有成竹。
在构造函数定义时(实例化之前
),执行了一系列的 mixin,这个给 Vue 对象本身及原型增加了哪些属性呢?
Vue 对象是基于函数,原型是基于 prototype
initMixin
stateMixin
eventsMixin
lifecycleMixin
renderMixin
在 vue._init() 执行 实例化过程中
,初始化了一系列 vue 属性
在生成基本属性之后,会执行下列属性、方法初始化方法
// 挂载 vm.$parent、vm.$root、vm.$children、vm.$refs、vm._watcher 属性,标识组件 _inactive、_isMounted 等状态属性
initLifecycle(vm)
// 初始化 vm._events、vm._hasHookEvent 属性,处理父子组件绑定的事件
initEvents(vm)
// 初始化 vm._vnode、vm.$createElement,通过闭包绑定数据到当前 vm 对象
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化 inject 数据,并且在当前 vm 对象上 defineReactive 其为响应式数据
initInjections(vm) // resolve injections before data/props
// 初始化 vm._watchers, vm._props, vm._data, vm.computed, vm.methods,处理 watch 方法
initState(vm)
// 初始化当前 provide 对象,主要是解决函数绑定到 vm 问题
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
按顺序阅读下来之后,会发现
beforeCreate
方法,也就是说,我们在 beforeCreate 生命周期之前,是拿不到 this.data 数据的组件初始化之后,就会调用 $mount 方法,执行挂载。
源码路径 vue/src/core/instance/lifecycle.js
的 mountComponent
方法
Vue.prototype.$mount
的内部直接调用方法在 mountComponent
方法中,主要是定义了 updateComponent
方法和实例化了一个 watcher
对象
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
这里只能看出 watcher 调用了 updateComponent 方法,那么它的作用是什么呢?
那么它的 render 生成虚拟节点,update 更新虚拟节点是怎么操作的呢?它是如何 watch 数据变化的呢?
在组件实例化的过程中,会调用 vm.$mount
方法渲染,在 $mount 方法过程中,具体又是采用 render 和 update 渲染和更新节点的。
在组件初始化的过程中,我们知道 vm._render
是通过 renderMixin
方法生成的。
源码路径 vue/src/core/instance/render.js
的 renderMixin
方法,该方法定义了下列实例原型方法
下面是 _render 的实现过程
compileToFunctions
方法生成的 vm.$options.render 方法注意点:Vue.prototype._render 和 vm.$options.render 是2个不同名不同用的方法
_render 方法内部的核心实现,是调用了 vm.$options.render 方法,下面是核心代码
Vue.prototype._render = function () {
const vm = this
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
vnode.parent = _parentVnode
return vnode
}
可以看到 _render 是一个执行的过程,而具体生成虚拟节点,还是要分析 vm.$createElement 方法
前面扯了这么多,都是创建虚拟节点的一个过程准备,关键点在 createElement 方法实现
源码路径 vue/src/core/vdom/create-element.js
的 _createElement
方法
先看输入输出定义
输入:context, tag, data, children, normalizationType
输出:vnode
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
) {
// 1 规范化
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 2 生成 vnode
let vnode, ns
if (typeof tag === 'string') {
let Ctor, ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// always false, 为什么?
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))) {
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()
}
}
关键点:children 规范化 + 生成 vnode 节点
思考:在我们编写的 template 结构中,必然存在标签各种嵌套,在编译过后,每个节点的 _createElement 方法中,children 参数都表示它后代节点的集合。
问题:各个节点之间的关系怎么保存的呢?是保存为一个对象树吗?
children 的每一项可能类型
先来看看规范化,源码路径 vue/src/core/vdom/helpers/normalize-children.js
// 普通数组扁平化 - 用于 render 函数编译生成,不处理 for, slot 等复杂结构,扁平化是为了处理 functional 组件
export function simpleNormalizeChildren (children) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children) // apply 要求所有函数参数都放到数组中
}
}
return children
}
// 基础类型 或者 数组 - 用于处理 for, slot 等复杂结构的规范化,因为它内部的 normalizeArrayChildren 内部实现采用来递归
export function normalizeChildren (children) {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
// 伪代码
function normalizeArrayChildren (children, nestedIndex) {
for (i = 0; i < children.length; i++) {
c = children[i]
if (Array.isArray(c)) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
} else if (isPrimitive(c)) {
res.push(createTextVNode(c))
} else {
if (isTextNode(c) && isTextNode(last)) {
// 合并连续2个文本节点
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// 设置默认 key
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
}
simpleNormalizeChildren 和 normalizeChildren 2个处理后代类型多样化、层级次数不同
children 规范化的作用
createTextVNode
转换成标准 vnode text 节点c.key = `__vlist${nestedIndex}_${i}__`
前面知道了 children 规范化的作用,也就是把 children 统一成 vnode array。
createElement 部分代码
// 关键代码
let vnode, ns
if (typeof tag === 'string') {
let Ctor, ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
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 部分源码
const baseCtor = context.$options._base
if (isObject(Ctor)) {
// Vue.extend 表示合并新的配置,生成新的 Vue 函数对象,此刻未实例化
Ctor = baseCtor.extend(Ctor)
}
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 为普通组件注入 component hooks.init 等方法
installComponentHooks(data)
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
可以看到
生成 vnode 的3种方式
使用 createComponent
创建一个 vnode 节点实例,不同于普通字符串的 vnode,它代表的是一个组件
我们编写的组件,同样会被转成 createElement()
,不同于普通元素,它拥有 context 环境,我们来看看是如何创建组件的呢?
组件化分析
在生成 vnode 对象之后,就该 patch 渲染它了。
vm._update 的作用是将虚拟 vnode 渲染成真实 dom,先来看看关键代码
// vm._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) {
// 更新
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 创建
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
要点: 生成 vm.$el 属性,关键方法 vm.__patch__()
// 生成实例 __patch__ 方法
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 定义在 runtime 路径下的 patch 方法,本质还是调用的 core 下面的 createPatchFunction 方法
export const patch: Function = createPatchFunction({ nodeOps, modules })
// 浏览器 patch 关键源码 生成 $el 真实 dom
// core/vdom/patch
export function createPatchFunction (backend) {
function removeNode (el) {}
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
let isInitialPatch = false
const insertedVnodeQueue = []
// 新建时直接创建
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
} else {
// 更新操作
const isRealElement = isDef(oldVnode.nodeType)
// 同一个节点,新旧树对比
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 非同一节点,直接整体替换
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
}
}
在 patch
中,有3个关键方法:createElm
, createComponent
, patchVnode
// createElm, 直接将节点插入父节点
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested // for transition enter check
// 如果是创建一个组件
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 创建的是一个 真实 dom
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)
}
}
createElm 会创建2种类型节点
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
patch.createComponent 作用
而 patchVnode 的主要作用,就是更新节点,对比前后 vnode 进行更新,解析逻辑可以在响应式系统 dom diff 模块看到。
更新流程,主要是数据变动,由属性的 dep 对象通知关联的 watcher 对象进行 update 更新,再次生成 vnode 节点进行 patch 渲染。
new Vue() -> vm._init() -> vm.$mount() -> compile() -> vm._render() -> vm._update()
vm._render: _render() -> createElement() -> children 标准化 -> new Vnode()
vm._update: vnode -> vm.__patch__() -> createElm() | patchVnode() -> DOM -> insert
core/instance/render.js
core/vdom/create-element.js
core/instance/lifecycle.js
core/vdom/patch.js