手撸mini-vue之props&emit

170 阅读2分钟

props

需求
  1. setup中传入
  2. render中可以通过this来访问props的值
  3. props第一层不可以被修改,内部深层次不做处理
场景
// App.js

export const App = {
  render() {
    return h(
      'div',
      {},
      [
        h('div', {}, 'hello, ' + this.name),
        // 创建 Foo 组件,向其中传入 count
        h(Foo, { count: 1 })
      ]
    )
  },
  setup() {
    return {
      name: 'mini-vue3'
    }
  }
}
// Foo.js

export const Foo = {
  setup(props) {
    // props 对象是只读的,但不是深度只读的
    props.count++
    console.log(props.count)
  },
  render() {
    // 在 通过 this 获取 props 中的 count
    return h('div', {}, 'foo: ' + this.count)
  }
}
实现代码

setupComponent初始化props

// component.ts

export function setupComponent(instance) {
  initProps(instance, instance.vnode.props)
}
// componentProps.ts

export function initProps(instance, rawProps) {
  // 根组件 App 的 props 是 undefined,所以需要判断一下
  instance.props = rawProps || {}
}

在调用setup的时候引入props,用shallowReadonly处理props

// component.ts

export function setupStatefulComponent(instance) {
  ...
  
  if (setup) {
    const  setupResult = setup(shallowReadonly(instance.props))
    
    handleSetupResult(instance, setupResult)
  }
}

在获取setup返回值的方法PublicInstanceProxyHandlers中处理props返回值

// componentPublicInstance.ts

export const PublicInstanceHandlers = {
  get({ _: instance }, key) {
    const { setupState, props } = instance
     
    const hasOwn = () => Object.prototype.hasOwnProperty.call(val, key)
    
    if (hasOwn(setupState, key)) {
      return setupState[key]
    } else if (hasOwn(props, key)) {
      return props[key]
    }

    ...
}

emit

需求
  1. setup第二个参数context对象中导出
  2. 在子组件发射事件,由父组件接收。例如子组件有一个emit('bar')事件,父组件通过onBar接收
场景
// Foo.js

export const Foo = {
  setup(props, { emit }) {
    const emitAdd = () => {
      console.log("emmit add");
      emit("add", 1, 2);
      emit("add-foo");
    };

    return {
      emitAdd,
    };
  },

  render() {
    const btn = h(
      "button",
      {
        onClick: this.emitAdd,
      },
      "emitAdd"
    );

    const foo = h("p", {}, "foo");
    return h("div", {}, [foo, btn]);
  },
};
// App.js

export const App = {
  render() {
    return h("div", {}, [
      h("div", {}, "App"),
      h(Foo, {
        onAdd(a, b) {
          console.log("onAdd", a, b);
        },
        onAddFoo() {
          console.log("onAddFoo");
        },
      }),
    ]);
  },

  setup() {
    return {
      msg: "mini-vue",
    };
  },
};
实现代码

setup中传入一个对象,这个对象包含emit

// component.ts

import emit from './componentEmit'

export function createComponentInstance(vnode) {
  const component = {
    vnode, 
    type: vnode.type,
    setupState: {},
    props: {},
    emit: () => {},
  }
  
  // 将组件实例作为参数绑定在 emit 上,这样用户调用 emit 不需要传 instance,只用传事件名
  component.emit = emit.bind(null, component) as any
  
  return component
}

function setupStatefulComponent(instance) {
  ...
  
  if (setup) {
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    })
  }
}

调用emit的时候可以传入一个event

export function emit(instance, event, ...args) {
  const { props } = instance
  
  const handlerName = toHandlerKey(camelize(event))
  const handler = props[handlerName]
  handler && handler(...args)
}
// src/shared/index.ts

// 将带 - 的字符串转换为驼峰式  add-foo -> addFoo
export const camelize = (str: string) => {
  return str.replace(/-(\w)/g, (_, c: string) => {
    return c ? c.toUpperCase() : ''
  })
}

// 将字符串首字母转换为大写
export const capitalize = (str: string) => {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

// 在字符串之前加上 on
export const toHandlerKey = (str: string) => {
  return str ? 'on' + capitalize(str) : ''
}