# 手写 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 分钟前
Vue3实现Pixso中的钢笔工具3——梳理函数调用
前端·vue.js
碎碎念_49214 小时前
SpringBoot + Vue 前后端分离从 0 到 1 完整环境配置流程
vue.js·spring boot·后端
shuaijie051814 小时前
强制修改调用接口的api地址。
javascript·vue.js·ecmascript
katiua18 小时前
Vue-i18n TypeScript 补全提示
vue.js
Fuzio18 小时前
用 Spring Boot + Vue + Fuzio 构建现代 Java 桌面应用
vue.js·spring boot
kisshyshy18 小时前
从0到1彻底理解流式输出:先读懂Vue的“乐高积木”,再拧开LLM的“数据水龙头”
javascript·vue.js·人工智能
饮茶三千19 小时前
电子保函模板编辑器实战二:HTML→FTL 编译转换的设计与实现
前端·vue.js·设计模式
英勇无比的消炎药19 小时前
表单筛选利器:TinyVue BaseSelect 基础选择器实战
vue.js
先吃饱再说19 小时前
从零理解 Vue 3:组件化、数据绑定与响应式完全指南
vue.js·前端框架
gis开发之家21 小时前
《Vue3 从入门到大神29篇》单元测试与 E2E 测试 —— 保障 Vue3 项目的质量
前端·javascript·vue.js·单元测试·前端框架·vue3