vue3源码分析-实现props,emit,事件处理等

引言

  • vue3源码分析——rollup打包monorepo
  • vue3源码分析——实现组件的挂载流程
  • 本期来实现,setup里面使用props,父子组件通信props和emit等,所有的源码请查看

    本期的内容与上一期的代码具有联动性,所以需要明白本期的内容,最后是先看下上期的内容哦!

    实现render中的this

    在render函数中,可以通过this,来访问setup返回的内容,还可以访问this.$el等

    测试用例

    由于是测试dom,jest需要提前注入下面的内容,让document里面有app节点,下面测试用例类似在html中定义一个app节点哦

    let appElement: Element; beforeEach(() => { appElement = document.createElement(‘p’); appElement.id = ‘app’; document.body.appendChild(appElement); }); afterEach(() => { document.body.innerHTML = ”; })复制代码

    本功能的测试用例正式开始

    test(‘实现代理对象,通过this来访问’, () => { let that; const app = createApp({ render() { // 在这里可以通过this来访问 that = this; return h(‘p’, { class: ‘container’ }, this.name); }, setup() { return { name: ‘123’ } } }); const appDoc = document.querySelector(‘#app’) app.mount(appDoc); // 绑定值后的html expect(document.body.innerHTML).toBe(‘123’); const elDom = document.querySelector(‘#container’) // el就是当前组件的真实dom expect(that.$el).toBe(elDom); })复制代码

    分析

    上面的测试用例

  • setup返回是对象的时候,绑定到render的this上面
  • $el则是获取的是当前组件的真实dom
  • 解决这两个需求:

  • 需要在render调用的时候,改变当前函数的this指向,但是需要思考的一个问题是:this是啥,它既要存在setup,也要存在el,咋们是不是可以用一个proxy来绑定呢?在哪里创建呢 可以在处理组件状态setupStatefulComponent来完成改操作
  • el则是在mountElement中挂载真实dom的时候,把当前的真实dom绑定在vnode当中
  • 编码

    针对上面的分析,需要在setupStatefulComponent中来创建proxy并且绑定到instance当中,并且setup的执行结果如果是对象,也已经存在instance中了,可以通过instance.setupState来进行获取

    function setupStatefulComponent(instance: any) { instance.proxy = new Proxy({}, { get(target, key){ // 判断当前的key是否存在于instance.setupState当中 if(key in instance.setupState){ return instance.setupState[key] } } }) // …省略其他}// 然后在setupRenderEffect调用render的时候,改变当前的this执行,执行为instance.proxyfunction setupRenderEffect(instance: any, vnode: any, container: any) { // 获取到vnode的子组件,传入proxy进去 const { proxy } = instance const subtree = instance.render.call(proxy) // …省略其他}复制代码

    通过上面的操作,从render中this.xxx获取setup返回对象的内容就ok了,接下来处理el

    需要在mountElement中,创建节点的时候,在vnode中绑定下,el,并且在setupStatefulComponent 中的代理对象中判断当前的key

    // 代理对象进行修改 instance.proxy = new Proxy({}, { get(target, key){ // 判断当前的key是否存在于instance.setupState当中 if(key in instance.setupState){ return instance.setupState[key] }else if(key === ‘$el’){ return instance.vnode.el } } }) // mount中需要在vnode中绑定el function mountElement(vnode: any, container: any) { // 创建元素 const el = document.createElement(vnode.type) // 设置vnode的el vnode.el = el //…… 省略其他 }复制代码

    看似没有问题吧,但是实际上是有问题的,请仔细思考一下,mountElement是不是比setupStatefulComponent 后执行,setupStatefulComponent执行的时候,vnode.el不存在,后续mountelement的时候,vnode就会有值,那么上面的测试用例肯定是报错的,$el为null

    解决这个问题的关键,mountElement的加载顺序是 render -> patch -> mountElement,并且render函数返回的subtree是一个vnode,改vnode中上面是mount的时候,已经赋值好了el,所以在patch后执行下操作

    function setupRenderEffect(instance: any, vnode: any, container: any) { // 获取到vnode的子组件,传入proxy进去 const { proxy } = instance const subtree = instance.render.call(proxy) patch(subtree, container) // 赋值vnode.el,上面执行render的时候,vnode.el是null vnode.el = subtree.el}复制代码

    至此,上面的测试用例就能ok通过啦!

    实现on+Event注册事件

    在vue中,可以使用onEvent来写事件,那么这个功能是怎么实现的呢,咋们一起来看看

    测试用例

    test(‘测试on绑定事件’, () => { let count = 0 console.log = jest.fn() const app = createApp({ render() { return h(‘p’, { class: ‘container’, onClick() { console.log(‘click’) count++ }, onFocus() { count– console.log(1) } }, ‘123’); } }); const appDoc = document.querySelector(‘#app’) app.mount(appDoc); const container = document.querySelector(‘.container’) as HTMLElement; // 调用click事件 container.click(); expect(console.log).toHaveBeenCalledTimes(1) // 调用focus事件 container.focus(); expect(count).toBe(0) expect(console.log).toHaveBeenCalledTimes(2) })复制代码

    分析

    在本功能的测试用例中,可以分析以下内容:

  • onEvent事件是在props中定义的
  • 事件的格式必须是 on + Event的格式
  • 解决问题:

    这个功能比较简单,在处理prop中做个判断, 属性是否满足 /^on[A-Z]/i这个格式,如果是这个格式,则进行事件注册,但是vue3会做事件缓存,这个是怎么做到?

    缓存也好实现,在传入当前的el中增加一个属性 el._vei || (el._vei = {}) 存在这里,则直接使用,不能存在则创建并且存入缓存

    编码

    在mountElement中增加处理事件的逻辑 const { props } = vnode for (let key in props) { // 判断key是否是on + 事件命,满足条件需要注册事件 const isOn = (p: string) => p.match(/^on[A-Z]/i) if (isOn(key)) { // 注册事件 el.addEventListener(key.slice(2).toLowerCase(), props[key]) } // … 其他逻辑 el.setAttribute(key, props[key]) }复制代码

    事件处理就ok啦

    父子组件通信——props

    父子组件通信,在vue中是非常常见的,这里主要实现props与emit

    测试用例

    test(‘测试组件传递props’, () => { let tempProps; console.warn = jest.fn() const Foo = { name: ‘Foo’, render() { // 2. 组件render里面可以直接使用props里面的值 return h(‘p’, { class: ‘foo’ }, this.count); }, setup(props) { // 1. 此处可以拿到props tempProps = props; // 3. readonly props props.count++ } } const app = createApp({ name: ‘App’, render() { return h(‘p’, { class: ‘container’, }, [ h(Foo, { count: 1 }), h(‘span’, { class: ‘span’ }, ‘123’) ]); } }); const appDoc = document.querySelector(‘#app’) app.mount(appDoc); // 验证功能1 expect(tempProps.count).toBe(1) // 验证功能3,修改setup内部的props需要报错 expect(console.warn).toBeCalled() expect(tempProps.count).toBe(1) // 验证功能2,在render中可以直接使用this来访问props里面的内部属性 expect(document.body.innerHTML).toBe(`1123`) })复制代码

    分析

    根据上面的测试用例,分析props的以下内容:

  • 父组件传递的参数,可以给到子组件的setup的第一个参数里面
  • 在子组件的render函数中,可以使用this来访问props的值
  • 在子组件中修改props会报错,不允许修改
  • 解决问题:

    问题1: 想要在子组件的setup函数中第一个参数,使用props,那么在setup函数调用的时候,把当前组件的props传入到setup函数中即可 问题2: render中this想要问题,则在上面的那个代理中,在加入一个判断,key是否在当前instance的props中 问题3: 修改报错,那就是只能读,可以使用以前实现的api shallowReadonly来包裹一下既可

    编码

    1. 在setup函数调用的时候,传入instance.props之前,需要在实例上挂载propsexport function setupComponent(instance) { // 获取props和children const { props } = instance.vnode // 处理props instance.props = props || {} // ……省略其他 } //2. 在setup中进行调用时作为参数赋值 function setupStatefulComponent(instance: any) { // ……省略其他 // 获取组件的setup const { setup } = Component; if (setup) { // 执行setup,并且获取到setup的结果,把props使用shallowReadonly进行包裹,则是只读,不能修改 const setupResult = setup(shallowReadonly(instance.props)); // …… 省略其他 }}// 3. 在propxy中在加入判断 instance.proxy = new Proxy({}, { get(target, key){ // 判断当前的key是否存在于instance.setupState当中 if(key in instance.setupState){ return instance.setupState[key] }else if(key in instance.props){ return instance.props[key] }else if(key === ‘$el’){ return instance.vnode.el } } })复制代码

    做完之后,可以发现咋们的测试用例是运行没有毛病的

    组件通信——emit

    上面实现了props,那么emit也是少不了的,那么接下来就来实现下emit

    测试用例

    test(‘测试组件emit’, () => { let count; const Foo = { name: ‘Foo’, render() { return h(‘p’, { class: ‘foo’ }, this.count); }, setup(props, { emit }) { // 1. setup对象的第二个参数里面,可以结构出emit,并且是一个函数 // 2. emit 函数可以父组件传过来的事件 emit(‘click’) // 验证emit1,可以执行父组件的函数 expect(count.value).toBe(2) // 3 emit 可以传递参数 emit(‘clickNum’, 5) // 验证emit传入参数 expect(count.value).toBe(7) // 4 emit 可以使用—的模式 emit(‘click-num’, -5) expect(count.value).toBe(2) } } const app = createApp({ name: ‘App’, render() { return h(‘p’, {}, [ h(Foo, { onClick: this.click, onClickNum: this.clickNum, count: this.count }) ]) }, setup() { const click = () => { count.value++ } count = ref(1) const clickNum = (num) => { count.value = Number(count.value) + Number(num) } return { click, clickNum, count } } }) const appDoc = document.querySelector(‘#app’) app.mount(appDoc); // 验证挂载 expect(document.body.innerHTML).toBe(`1`) })复制代码

    分析

    根据上面的测试用例,可以分析出:

  • emit 的参数是在父组件的props里面,并且是以 on + Event的形式
  • emit 作为setup的第二个参数,并且可以结构出来使用
  • emit 函数里面是触发事件的,事件名称,事件名称可以是小写,或者是 xxx-xxx的形式
  • emit 函数的后续可以传入多个参数,作为父组件callback的参数
  • 解决办法: 问题1: emit 是setup的第二个参数,那么可以在setup函数调用的时候,传入第二个参数 问题2: 关于emit的第一个参数,可以做条件判断,把xxx-xxx的形式转成xxxXxx的形式,然后加入on,最后在props中取找,存在则调用,不存在则不调用 问题3:emit的第二个参数,则使用剩余参数即可

    编码

    // 1. 在setup函数执行的时候,传入第二个参数 const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit });// 2. 在setup中传入第二个参数的时候,还需要在实例上添加emit属性哦export function createComponentInstance(vnode) { const instance = { // ……其他属性 // emit函数 emit: () => { }, } instance.emit = emit.bind(null, instance); function emit(instance, event, …args) { const { props } = instance // 判断props里面是否有对应的事件,有的话执行,没有就不执行,处理emit的内容,详情请查看源码 const key = handlerName(capitalize(camize(event))) const handler = props[key] handler && handler(…args) } return instance}复制代码

    到此就圆满成功啦!

    郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
    用户投稿
    上一篇 2022年6月19日 15:13
    下一篇 2022年6月19日 15:14

    相关推荐

    • 短视频策划内容的3个要点(短视频策划内容怎么做)

      短视频在制作时,内容框架非常重要。如果直奔主题,然后结束,聚卓告诉你,这样的短视频已经过时了。现在的短视频需要框架的,但不是任何框架,它需要一种易于理解和消化的框架。而且,现在大多…

      2022年11月27日
    • 存储过程语法(sql server存储过程语法)

      今天小编给各位分享存储过程语法的知识,其中也会对sql server存储过程语法进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧! oracle存储过程基本语法…

      2022年11月26日
    • 全民K歌升级新版本7.0之后,有哪些隐藏功能?

      作者:高百烈来源:知乎 这个功能,旧版并没有,要升级到全新的全民K歌7.0版本才能发现。 作为朋友圈当代K歌之王,我费了不少功夫才抢到内测版本。有一说一,全民K歌的路子真的很野,新…

      2022年11月25日
    • 上手Reno8 Pro体验跨屏互联 实在太方便!

      11月已经来到了月底,在手机品牌又要推出新一年度的新品手机之前,我们来点评一下今年令人惊喜的产品。如OPPO的Reno8 Pro系列,该系列搭载双芯影像配置获得了很多消费者的认可。…

      2022年11月25日
    • 小红书平台的一些机制及玩法详解(小红书玩法有哪些)

      关于小红书 一:小红书平台的一些机制 1. 笔记内容的CES评分机制 2. 笔记流量入口与长尾效应 二:小红书优质笔记的特点(分维度、类型分析) 1.笔记的本身架构组成 维度 2.…

      2022年11月25日
    • 博客营销的3大优势解析(博客营销怎么做)

      不知不觉已经写了24篇文章,加上这篇是第25篇了,都是自己这几年来用过的营销方法,如果遇到有些不懂的,我会咨询我的朋友和同事帮忙,尽量让每一篇有价值,哪怕是对大家有一点点帮助也行,…

      2022年11月25日
    • 什么是内容营销策略如何策划一套成功的内容营销策略

      很多时候,营销人员会在创作营销内容时感到沮丧,这也是很多企业至今没用好数字化营销工具的重要原因之一。 举个例子,您可能会花上数小时期待制作一些令人惊叹的东西,实际却是得到很少的受众…

      2022年11月25日
    • 直播带货详细脚本(直播文案策划怎么写)

      短视频运营策划方案怎么写?涉及哪几个方面? 我在网上看到好多千篇一律的文章,关于【短视频运营策划方案】这一块,基本都是在讲账号的内容本身。 你内容做得再好,却不掌握算法的规律,能有…

      2022年11月25日
    • 闲鱼上怎么卖东西快?闲鱼赚钱必知技巧(怎么在闲鱼里面卖东西)

      自从闲鱼这个平台出现以后,随着这些年的发展,闲鱼也成为了很多人开店赚钱的重要途径。一些新人也想在闲鱼上试试水,看看能不能赚到钱。如今闲鱼上既可以卖二手闲置物品,也可以卖一些新品,那…

      2022年11月25日
    • 淘宝直播带货技巧分享(淘宝直播复制粘贴技巧)

      淘宝是大家喜欢的购物平台,不少人会自己在淘宝选购,也会在主播直播间购买,价格更实惠,现在很多商家都会开通直播带货功能,增加订单量,那么淘宝直播带货有哪些?下面小编为大家带来淘宝直播…

      2022年11月24日

    联系我们

    联系邮箱:admin#wlmqw.com
    工作时间:周一至周五,10:30-18:30,节假日休息