# 手写 mini-vue3 - 实现组件对象的代理(三)

实现 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

vnodeel

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, {})
        ])
    }
}

Appel 应该是

html 复制代码
<div class="app">
    <div class="child">child</div>
</div>

Childel 应该是:

html 复制代码
<div class="child">child</div>

App vnodeel

一开始 App 传给 createVNode 得到了一个 AppinitialVnode

ts 复制代码
{
    type: App,
    props: {},
    children,
    el: null,
}

initialVnode 传给了 render

接着 patch(initialVnode)

接着 mountComponent(initialVnode)

接着来到 setupRenderEffect(initialVnode)

接着执行 App instancerender,得到 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

最后,initialVnodeApp 对应的这个 vnodeeldocument.createElement('div')

Child vnodeel

在第二次 patch 后,根据 type='div'

来到 mountElement

接着判断到 vnode.children 是数组

接着 mountChildren

接着再次 patch

ts 复制代码
mountChildren(vnode) {
    vnode.children.forEach(v => {
        patch(v);
    })
}

这里,v 表示 Childvnode

根据 type=Child

接着来到 mountComponent,这时的 initialVnodeChildvnode

ts 复制代码
initialVnode = {
    type: Child,
    props: {},
    children: {},
    el: null,
}

接着来到 setupRenderEffect

接着执行 Childrender得到 ChildsubTree

ts 复制代码
const subTree = instance.render.call(proxy);

ts 复制代码
subTree = h('div', { class: 'child' }, 'child')

然后再 patch

最终 Child vnodeel<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

相关推荐
前端摸鱼匠7 小时前
Vue 3 的v-bind合并行为:讲解v-bind与普通属性合并的规则
前端·javascript·vue.js·前端框架·ecmascript
Python私教17 小时前
Pure-Admin-Thin 深度解析:完整版和精简版到底怎么选?
vue.js·人工智能·开源
ayqy贾杰20 小时前
Cursor SDK发布!开发者可直接搬走其内核
前端·vue.js·面试
李白的天不白20 小时前
vue 数据格式问题
前端·vue.js·windows
小白蒋博客20 小时前
【ai开发段永平投资理财的知识图谱网站】第一天:搭 Vite + Vue 项目,跑通 Hello World
vue.js·人工智能·trae
@yanyu6661 天前
登录注册功能-明文
vue.js·springboot
liang_jy1 天前
Android SparseArray
android·源码
liang_jy1 天前
Activity 启动流程扩展篇(一)—— startActivityInner 任务决策全解析
android·源码
冬奇Lab1 天前
RAG 系列(四):文档处理——从原始文件到高质量 Chunk
人工智能·llm·源码
滕青山1 天前
在线PDF拆分工具核心JS实现
前端·javascript·vue.js