initElement
调用patch打补丁的时候,如果虚拟DOM的type属性是 string,那么这个虚拟DOM描述的则是一个HTML元素,调用processElement来处理。
processElement
-
调用mountElement(vNode, container)将虚拟DOM挂载到对应容器中
-
调用 document.createElement 创建对应的真实DOM - el
-
如果虚拟DOM的 children 是string类型,直接赋值给 el 的 textContent 属性
-
如果虚拟DOM的 children 是一个数组,说明由多个子元素,调用mountChildren处理 - mountChildren(vNode, el)
-
遍历 vNode 的props中,对每一个子元素执行patch操作
-
最后循环vNode.props将虚拟DOM的属性添加到 el 上,最后将 el 添加到container中
processElement - 对应代码
function processElement (vNode, container) {
mountElement(vNode, container)
}
function mountElement(vNode, container) {
// $el:这里的虚拟节点时element类型的,也就是App中的根元素div;return instance.vNode.el中的虚拟节点是组件实例对象的虚拟节点
// 创建对应的DOM,同时绑定到虚拟DOM上
const el = vNode.el = document.createElement(vNode.type)
// 处理子节点 - 如果是string类型直接赋值给DOM元素的textContent属性
if (typeof vNode.children === 'string') {
el.textContent = vNode.children
} else if (Array.isArray(vNode.children)) {
// 如果是数组类型(说明有多个子元素),调用patch递归处理子节点
mountChildren(vNode, el)
}
// 处理vNode对应的属性
for (const key in vNode.props) {
el.setAttribute(key, vNode.props[key])
}
// 将DOM添加到对应容器中
container.appendChild(el)
}
// 当children为数组时,处理子节点
function mountChildren (vNode, container) {
vNode.children.forEach(child => {
patch(child, container)
})
}
拦截this
App组件的render方法 返回一个虚拟DOM,return h('div',{}, 'hello,' + this.msg),这个虚拟DOM中调用this.msg访问到了setup中的msg属性。
setupStatefulComponent
该方法在处理setup的时候,创建了一个代理对象,并将代理对象添加到实例的proxy属性上。
proxy
当访问 proxy 的时候,会先到组件实例中获取到组件实例的 setupState属性,即setup函数的返回值。如果访问的属性 key 存在于setupState中,那么直接返回setupState[key]
function setupStatefulComponent(instance) {
// 第一次patch的时候instance.type就是传入的App组件
const Component = instance.type
// 创建一个代理对象用来拦截render函数中的this
instance.proxy = new Proxy({}, {
get(target, key) {
const { setupState } = instance
// 如果setupState中存在这个属性就返回它的值
if (key in setupState) {
return setupState[key]
}
}
})
const { setup } = Component
if (setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult)
}
}
App组件中的render方法中的this.msg,这里的this指向App组件。如果想让他指向组件实例的 proxy 属性,需要改变它的this指向。通过 instance.render.call(instance.proxy)改变this指向。
function setupRenderEffect (instance, vNode, container) {
// instance.render 来自于 finishComponentSetup 方法,就是组件的render方法
// 绑定this,让render中的this指向创建的代理对象
const subTree = instance.render.call(instance.proxy)
// vNode -> patch
// vNode -> element -> mountElement
patch(subTree, container)
// subTree指的就是class="root"的根节点
// 子元素处理完成之后
vNode.el = subTree.el
}
$el
在组件中通过 this.$el 可以访问到组件对应的真实DOM。
同理:
function setupStatefulComponent(instance) {
// 第一次patch的时候instance.type就是传入的App组件
const Component = instance.type
// 创建一个代理对象用来拦截render函数中的this
instance.proxy = new Proxy({}, {
get(target, key) {
const { setupState } = instance
// 如果setupState中存在这个属性就返回它的值
if (key in setupState) {
return setupState[key]
}
if (key === '$el') {
return instance.vNode.el
}
}
})
const { setup } = Component
if (setup) {
const setupResult = setup()
handleSetupResult(instance, setupResult)
}
}
这里通过虚拟DOM的el属性返回真实DOM,所以在这之前需要将真实DOM添加到对应的虚拟DOM的 el 属性中。
在创建真实DOM的时候,将真实DOM添加到虚拟DOM的el属性上: const el = vNode.el = document.createElement(vNode.type)。
function mountElement(vNode, container) {
// $el:这里的虚拟节点时element类型的,也就是App中的根元素div;return instance.vNode.el中的虚拟节点是组件实例对象的虚拟节点
// 创建对应的DOM,同时绑定到虚拟DOM上
const el = vNode.el = document.createElement(vNode.type)
// 处理子节点 - 如果是string类型直接赋值给DOM元素的textContent属性
if (typeof vNode.children === 'string') {
el.textContent = vNode.children
} else if (Array.isArray(vNode.children)) {
// 如果是数组类型(说明有多个子元素),调用patch递归处理子节点
mountChildren(vNode, el)
}
// 处理vNode对应的属性
for (const key in vNode.props) {
el.setAttribute(key, vNode.props[key])
}
// 将DOM添加到对应容器中
container.appendChild(el)
}
添加完之后通过 $el 还是访问不到它对应真实DOM,因为这里的虚拟节点时element类型的,也就是App中的根元素div;return instance.vNode.el中的虚拟节点是组件实例对象的虚拟节点。
function setupRenderEffect (instance, vNode, container) {
// instance.render 来自于 finishComponentSetup 方法,就是组件的render方法
// 绑定this,让render中的this指向创建的代理对象
const subTree = instance.render.call(instance.proxy)
// vNode -> patch
// vNode -> element -> mountElement
patch(subTree, container)
// subTree指的就是class="root"的根节点
// 子元素处理完成之后
vNode.el = subTree.el
}
添加完之后就可以通过this.$el访问到虚拟DOM对应的真实DOM了。