实现mini-vue -- runtime-core模块(五)实现组件props功能

135 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

本篇文章会带你实现一下组件的props功能,主要包括能够在setup函数中接收props参数、在render函数中可以访问props中的属性和方法、props无法修改,只能读取

1. 需求分析

要实现的需求:

  1. 能够在setup函数中接收props参数
  2. render函数中能够直接通过this访问到props中的属性和方法
  3. props应当是只读的

2. 搭建案例环境

创建一个子组件Foo.js,在App.js中会调用该组件

import { h } from '../../lib/plasticine-mini-vue.esm';

export const Foo = {
  setup(props) {
    // 假设 props 中有一个 count 属性
    console.log('props: ', props);
    // props.count++;
  },
  render() {
    return h('div', {}, `foo: ${this.count}`);
  },
};
import { h } from '../../lib/plasticine-mini-vue.esm.js';
+ import { Foo } from './Foo.js';

window.self = null;
export const App = {
  // 由于还没有实现模板编译的功能 因此先用 render 函数来替代
  render() {
    window.self = this;
    return h(
      'div',
      {
        class: ['cyan', 'success'],
        onClick() {
          console.log('click...');
        },
        onMouseMove() {
          console.log('mouse moving...');
        },
      },
      [
        h('p', { class: 'cyan' }, 'hi '),
        h('p', { class: 'darkcyan' }, 'plasticine '),
        h('p', { class: 'darkviolet' }, 'mini-vue!'),
        h('p', { class: 'darkcyan' }, `setupState msg: ${this.msg}`),
+       h(Foo, { count: 666 }),
      ]
    );
  },
  setup() {
    // Composition API
    return {
      msg: 'plasticine-mini-vue',
    };
  },
};

只要能够在Foo.js组件中的render函数中能够通过this.count访问到props.count就可以了,并且在setup中修改props.count应当出现警告,告知用户props是只读的,不会修改成功


3. 在 setup 中访问 props

找到之前标记了TODOinitProps的地方,也就是src/runtime-core/component.tssetupComponent函数 新建一个文件src/runtime-core/componentProps.ts,在这里面去处理props的逻辑

export function initProps(instance, rawProps) {
  instance.props = rawProps;
}

目前该函数的功能就只是把原始的vnode中定义的props挂载到组件实例instance上,这样就可以直接通过组件实例去访问props,而不需要总是通过vnode去访问了,由于组件实例现在多了一个属性,所以我们最好才createComponent的时候给它加上一个空对象初始值,这样以后读自己的代码的时候就知道组件实例会有哪些属性了

export function createComponentInstance(vnode) {
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
+   props: {},
  };

  return component;
}

然后现在就可以把initProps放到setupComponent中使用了

export function setupComponent(instance) {
  // TODO
- // initSlots()
+ initProps(instance, instance.vnode.props);
  // initSlots()

  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance: any) {
  const Component = instance.type;
  const { setup } = Component;

  // ctx -- context
  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);

  if (setup) {
-   const setupResult = setup();
+   const setupResult = setup(instance.props);

    // setupResult 可能是 function 也可能是 object
    // - function 则将其作为组件的 render 函数
    // - object 则注入到组件的上下文中
    handleSetupResult(instance, setupResult);
  }
}

现在就能够在组件的setup方法中访问到propsimage.png 接下来就要尝试让render方法能够通过this访问到props


4. render 中通过 this 访问 props

这个和在render方法中通过this访问setupState,即setup()方法返回的对象,是一个道理 由于前面已经将组件实例的this绑定到了上下文代理对象中,因此我们只需要到代理对象中添加props的处理逻辑即可

export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
-   const { setupState } = instance;
+   const { setupState, props } = instance;
    const publicGetter = publicPropertiesMap[key];

    if (publicGetter) {
      return publicGetter(instance);
    }

    if (key in setupState) {
      return setupState[key];
+   } else if (key in props) {
+     return props[key];
+   }
  },
};

现在就已经能够在组件的render方法中通过this访问propsimage.png 这里的key in xxx可以封装成一个hasOwn工具函数,让可读性更高一些

// src/shared/index.ts
export const hasOwn = (target, key) =>
  Object.prototype.hasOwnProperty.call(target, key);
export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const { setupState, props } = instance;
    const publicGetter = publicPropertiesMap[key];

    if (publicGetter) {
      return publicGetter(instance);
    }

-   if (key in setupState) {
+   if (hasOwn(setupState, key)) {
      return setupState[key];
-   } else if (key in props) {
+   } else if (hasOwn(props, key)) {
      return props[key];
    }
  },
};

5. props 设计为只读

Foo组件的setup方法中我们修改了props.count,但是目前并没有给我们警告,且值是真的能够被修改的,这是因为在调用组件的setup方法时,我们是直接把instance.props作为参数传入的,如果我们用readonly包裹后再传入就可以解决该问题了 事实上,vue3使用的是shallowReadonly而不是readonly,也就是只会包裹一层,对于被包裹对象中还有引用数据类型的属性是不会对它们也作出限制的

function setupStatefulComponent(instance: any) {
  const Component = instance.type;
  const { setup } = Component;

  // ctx -- context
  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);

  if (setup) {
-   const setupResult = setup(instance.props);
+   const setupResult = setup(shallowReadonly(instance.props));

    // setupResult 可能是 function 也可能是 object
    // - function 则将其作为组件的 render 函数
    // - object 则注入到组件的上下文中
    handleSetupResult(instance, setupResult);
  }
}

现在来查看一下是否会给出警告 image.png 发现居然报错了,看来是在创建代理对象的时候,给Proxy构造函数传入的target不是一个对象导致的,之前在创建代理对象的时候没有考虑到target的类型,应当限制它是一个object的时候才允许创建代理对象,这里我们可以修复一下

function createActiveObject(raw: any, baseHandlers) {
+ if (!isObject(raw)) {
+   console.warn(`target: ${raw} must be an object`);
+   return raw;
+ }
  return new Proxy(raw, baseHandlers);
}

现在就可以了 image.png 但是我们还是需要找一下原因,为什么会给一个undefined创建代理对象?我们先看一下函数调用栈,看看在创建代理对象之前是由哪个函数进来的 image.png 可以看到是从setupStatefulComponent函数进来的,那么我们就进入调试模式打一个断点看看 image.png 可以看到当前的组件实例的propsundefined,因此给props创建shallowReadonly代理时会报错也是正常的了,不管这个组件实例是谁了,目前很明显问题出在props会有undefinedbug,我们只需要在initProps的时候,如果没有定义props,就给它设置成是一个空对象就好了

export function initProps(instance, rawProps) {
- instance.props = rawProps;
+ instance.props = rawProps ?? {};
}

image.png