前言
前段时间,分别学习了newVue流程、Vue的响应式、Vue的模板编译,为了将所有流程串联起来,这几天参考源码梳理了下从render函数到真实dom挂载到页面的过程。
整体流程
我们再来回忆一下整个流程:
- 初始化的时候,将数据变成响应式,每个数据的key,get函数中收集依赖(Watcher实例)到dep,set函数中dep通知watcher执行更新函数(参考Vue的响应式)
- $mount时获取render函数,options中存在render函数无需处理,否则将template编译成render函数(参考Vue的模板编译)
- mount时,new Watcher传入updateComponent更新函数,在更新函数中执行render函数,获取到VNode的树,通过patch方法,将VNode转化成真实dom树。因为Watcher实例在执行render函数的过程中,访问数据触发了get收集依赖,所以当数据变化时,就通知updateComponent更新dom。
function $mount(el: any) {
const options = this.$options;
const vm = this
// 获取render函数
if (!options.render) {
let template = options.template
if (typeof template === 'string') {
const root = parse(template);
optimize(root, {});
const code = generate(root)
options.render = new Function(code.render);
}
}
vm.$el = document.querySelector(el)
// 更新组件的函数
let updateComponent = () => {
// 执行render函数,传入$createElement其实就是手写render函数中的h函数,主要用来创建Vnode
const vnode = options.render.call(vm, vm.$createElement);
vm._update(vnode)
}
new Watcher(vm, updateComponent, () => {}, {})
return vm
}
render函数生成Vnode
我们知道render函数有两种方式得到,
- 1.手写的render函数
- 2.传入template字符串或写的.vue组件,编译生成的render函数
// 1.
render(h) {
return h('div', 'Hello World!')
}
// 2.
function() {
with(this) {
return _c( "div", { }, [ _v('Hello World!') ] )
}
}
第1种方式传入的h函数和第2种方式的_c函数,最终执行的都是createElement;在init中会执行initRender函数,
- 第1种方式是因为with绑定了vue实例,直接可以访问
- 第2种方式是在执行render的时候,vm.$createElement作为参数传入
initRender(vm: any) {
vm._c = (a: any, b: any, c: any, d: any) => createElement(vm, a, b, c, d, false);
vm.$createElement = (a: any, b: any, c: any, d: any) => createElement(vm, a, b, c, d, true)
}
createElement
对参数进行处理,将传入编译或者传入的对象,转换成vnode,实际就是创建VNode实例。
export function createElement(
context: any,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): Array<any> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement(
context: any,
tag?: any,
data?: any,
children?: any,
normalizationType?: number
): any | Array<any> {
// ...
let vnode
if (typeof tag === 'string') {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
return vnode
}
VNode是什么
在Vue.js中存在一个VNode类,使用它可以实例化不同类型的vnode实例,而不同类型的vnode实例各自表示不同类型的DOM元素。简单的说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的dom节点。
class VNode {
constructor(
tag?: string,
data?: any,
children?: Array<VNode>,
text?: string,
elm?: Node,
context?: any,
componentOptions?: any,
asyncFactory?: Function
) {
this.tag = tag // 标签名,元素节点或组件该值会存在
this.data = data
this.children = children // 子元素
this.text = text // 文本或者注释内容
this.elm = elm // 对应的真实dom
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
}
}
patch函数
执行完render函数获取vnode后,我们将vnode传入,执行update函数,核心执行的patch函数,将vnode转化成真实dom。下面步骤是忽略了diff的简化过程。
- 传入旧的vnode和新的node
- 根据旧的vnode找到元素的原本位置,将新的vnode创建成dom
- 最后将新生成的dom插入对应位置,移除页面旧的dom
function _update(vnode: any) {
const vm: any = this;
const prevVnode = vm._vnode;
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
// patch方法
function patch(oldVnode: any, vnode: any) {
// 第一次传入真实dom,创建一个vnode
if (isDef(oldVnode.nodeType)) {
oldVnode = new VNode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
// 创建元素
createElm(
vnode,
oldElm.parentNode,
oldElm.nextSibling
)
removeNode(oldVnode.elm); // 移除旧的dom
return vnode.elm
}
// 创建元素
function createElm(vnode: any, parentElm?: any, refElm?: any, ownerArray?: any, index?: number) {
const children = vnode.children
const tag = vnode.tag
// 创建元素节点
if (tag != null) {
vnode.elm = nodeOps.createElement(tag, vnode)
// 递归创建dom树
createChildren(vnode, children)
} else if (vnode.isComment) { // 创建注释节点
vnode.elm = nodeOps.createComment(vnode.text)
} else { // 创建文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
}
// 元素插入到对应位置
insert(parentElm, vnode.elm, refElm)
}
// 创建子元素
function createChildren() {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], vnode.elm, null, children, i)
}
} else if (tyeof vnode.text === "string" || tyeof vnode.text === "number") {
nodeOps.appendChild(vnode.elm, document.createTextNode(String(vnode.text)))
}
}
最后
2021年过的很快,这一年总结起来,感觉看的东西也比较乱,没有一个系统化的过程,但是知识是还是靠一点点积累的,虽然写的文章目前还是比较简单的,但是也是对自己知识的一种梳理,即是分享又是总结。走过路过的朋友,多多点赞关注,祝大家2022年加油,新年快乐~