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

相关推荐
醉の虾17 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
chusheng184039 分钟前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站
游走于计算机中摆烂的1 小时前
启动前后端分离项目笔记
java·vue.js·笔记
幼儿园的小霸王1 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
码蜂窝编程官方2 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
乐闻x3 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
533_4 小时前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
ZBY520315 小时前
【Vue】 npm install amap-js-api-loader指南
javascript·vue.js·npm
计算机毕设指导65 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
木子02046 小时前
前端VUE项目启动方式
前端·javascript·vue.js