实现 this 指向
ts
import { h } from '../lib/guide-mini-vue.esm.js';
export const App = {
name: 'App',
render() {
window.self = this;
return h('div', { id: 'app' }, [
h('p', { class: 'red' }, 'Hello' + this.msg),
]);
},
setup() {
return {
msg: 'jerry'
}
}
}
在这个例子中,我们需在 render 中实现 this 的指向,在 setupRenderEffect 调用 instance.render 时,理论上是让 render 函数指向 setupState。
setupState 指的是 setup 函数的返回结果
ts
// 伪代码
instance.render.call(setupState);
实现
在 createComponentInstance 这里,给 instance 初始化了 setupState 属性
ts
function createComponentInstance(vnode) {
const component = {
setupState: {},
}
}
在 setupStatefulComponent 这里,实现组件对象的代理
ts
// runtime-core/component.ts
function setupStatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
// 实现组件对象的代理
instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers)
if(setup) {
const setupResult = setup();
// 处理 setup 函数的返回结果
handleSetupResult(instance, setupResult);
}
}
ts
// runtime-core/componentPublicInstance.ts
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { setupState, props } = instance;
const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key);
if(hasOwn(setupState, key)) {
return setupState[key];
}
}
}
回顾 runtime-core 的流程中,等到 setupRenderEffect 执行时,已完成实例对象 proxy 的绑定。
ts
function mountComponent() {
createComponentInstance();
setupComponent();
setupRenderEffect();
}
在 setupRenderEffect 内调用 instance.render 时,把实例的代理对象 proxy 绑定到 render,那么 render 函数内的 this 便指向了 instance.proxy
ts
// rumtime-core/renderer.ts
function setupRenderEffect(instance) {
const { proxy } = instance;
const subTree = instance.render.call(proxy);
patch(subTree, container);
}
这里需要注意的是,被 Proxy 代理的目标对象是 instance,但是我们在 render 函数内部访问 this.msg 的时候,不是访问 instance 的属性,而是 instance.proxy 的属性。
在访问 this.msg 的时候,被 Proxy 拦截处理,内部的机制是,从 instance.setupState 里面获取 msg 属性值并返回。
ts
const { setupState, props } = instance;
if(hasOwn(setupState, key)) {
return setupState[key];
}
$el
vnode 的 el
ts
function createVNode(type, props?, children) {
const vnode = {
el: null,
}
}
开始给 el 赋值是在 mountElement 函数内部处理。
ts
function mountElement(vnode, container) {
const el = vnode.el = document.createElement(vnode.type); // 开始给 el 赋值
}
ts
function mountComponent(initialVNode, container) {
const instance = createComponentInstance(initialVNode)
setupComponent(instance);
// 把 initialVNode 传给 setupRenderEffect
setupRenderEffect(instance, initialVNode, container);
}
ts
function setupRenderEffect(instance, initialVNode, container) {
const subTree = instance.render.call(proxy);
patch(subTree, container);
// 经过 patch 之后,subTree 获得了 el
initialVNode.el = subTree.el;
}
为什么是 initialVNode.el = subTree.el; 呢?
vnode 和 subTree 的区别
比如,在这个例子中:
js
const Child = {
name: 'Child',
render() {
return h('div', {}, 'child')
}
};
const App = {
name: 'App',
render() {
return h('div', { class: 'app' }, [
h(Child, {})
])
}
}
App 的 el 应该是
html
<div class="app">
<div class="child">child</div>
</div>
Child 的 el 应该是:
html
<div class="child">child</div>
App vnode 的 el
一开始 App 传给 createVNode 得到了一个 App 的 initialVnode,
ts
{
type: App,
props: {},
children,
el: null,
}
initialVnode 传给了 render
接着 patch(initialVnode)
接着 mountComponent(initialVnode)
接着来到 setupRenderEffect(initialVnode)
接着执行 App instance 的 render,得到 subTree
ts
// App vnode 的 subTree
subTree = h('div', { class: 'app' }, [
h(Child, {})
])
h 函数返回的是:
ts
{
type: 'div',
props: {
class: 'app'
},
children: h(Child, {}),
el: null,
}
也就是 subTree 指的是:
ts
{
type: 'div',
props: {
class: 'app'
},
children: h(Child, {}),
el: null,
}
接着 patch(subTree)
接着 mountElement(subTree)
接着 subTree.el = document.createElement('div')
至此,patch 完成
接着 initialVnode.el = subTree.el
最后,initialVnode 即 App 对应的这个 vnode 的 el 为 document.createElement('div')
Child vnode 的 el
在第二次 patch 后,根据 type='div'
来到 mountElement
接着判断到 vnode.children 是数组
接着 mountChildren
接着再次 patch
ts
mountChildren(vnode) {
vnode.children.forEach(v => {
patch(v);
})
}
这里,v 表示 Child 的 vnode
根据 type=Child
接着来到 mountComponent,这时的 initialVnode 为 Child 的 vnode
ts
initialVnode = {
type: Child,
props: {},
children: {},
el: null,
}
接着来到 setupRenderEffect
接着执行 Child 的 render得到 Child 的 subTree
ts
const subTree = instance.render.call(proxy);
即
ts
subTree = h('div', { class: 'child' }, 'child')
然后再 patch
最终 Child vnode 的 el 是 <div class="child">child</div>
el 挂载到 instance.proxy 代理对象
ts
const PublicPropertiesMaps = {
$el: (i) => i.vnode.el,
}
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { setupState, props } = instance;
// 省略...
// 在这里统一处理 $el、$props 等对象
const publicGetter = PublicPropertiesMaps[key];
if(publicGetter) {
return publicGetter(instance);
}
}
}
这样,在 render 中访问 this.$el,就可获取到组件对应的 el。