# 手写 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

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。

相关推荐
用户094248568032 分钟前
第3章:Redis字符串 String 实战:验证码、计数器与分布式开关
redis·源码
专业程序开发源1 小时前
springboot校企合作双选会一站式预约与面试系统16010-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·面试·php·课程设计
凯凯姜2 小时前
Vue 3 深入学习笔记 16:API 请求封装与统一错误处理
vue.js·笔记·学习
Java后端的Ai之路4 小时前
前端框架Vue-07_原生的Vue.js
前端·vue.js·前端框架
试图让你心动4 小时前
【无标题】
前端·javascript·vue.js
tryCbest5 小时前
Vue3——第 6 篇:事件处理与表单绑定——v-on 与 v-model
前端·javascript·vue.js
程序员小孔.6 小时前
基于 Spring Cloud + Vue 的 JEEWMS 开源仓储管理平台设计与实现
前端·javascript·vue.js·spring boot·spring cloud·开源
夕阳_醉了6 小时前
花了一天的时间,地板式扫盲了vue3所有API盲点
前端·javascript·vue.js
专业程序开发源6 小时前
django视听资源推荐系统16690-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
lemon_yyds20 小时前
Vue 项目嵌入 React H5 技术方案
vue.js·react.js