当前位置: 首页 > Web前端 > vue.js

自己在vue3中实现createApp和mount

时间:2023-04-01 02:07:00 vue.js

更好的阅读体验,请参考博客原文。话不多说,先来一张vue3中从创建应用实例到挂载整个dom树的流程图。本文的目标是简单实现vue3中的createApp和mountAPI。虽然只有两个API,但这两个API实现了vue3根实例的创建、组件的解析、vnode编号的构建……通过这两个API的实现,可以了解整个的整体挂载流程vue3项目,可以说是vue3的核心API。本文代码github仓库地址//定义根组件import{h}from'vue'//本文只有h方法使用vue官方提供import{reactive}from'./vic/reactive'//自己实现的reactiveimport{createApp}from'./vic'//自己实现的createAppconstApp={components:{SubCom},setup(){constcount=reactive({value:0})constinc=()=>{count.value++}return()=>h('div',[h('h1',count.value),h('button',{onClick:inc},'++'),h(SubCom)])}}//定义组件constSubCom={setup(){return()=>h('div',[h('h2','我是一个子组件')])}}//挂载createApp(App).mount(document.getElementById('app'))代码实现(结合流程图看代码注释)import{effect}from'./reactive'letuid=0/***1.创建一个上下文来承载一些全局配置和全局注册的指令,组件...*2.创建一个app实例(vue项目的唯一实例,curre只关注它提供的挂载方法)*/exportconstcreateApp=(rootComponent,rootProps=null)=>{constcontext={config:{devtools:true,performance:false,globalProperties:{},optionMergeStrategies:{},错误处理程序:undefined,warnHandler:undefined},mixins:[],components:{},directives:{},provides:Object.create(null)}constapp={/***1.创建根组件的vnode*2.调用render方法,将vnode渲染到真正的dom*/mount:(rootContainer)=>{constvnode=createVNode(rootComponent,rootProps)vnode.appContext=contextrender(vnode,rootContainer)},components:{}}returnapp}/***极简版的vnode创建*@paramtype当type为对象时,表示这是一个组件,当type为字符串时,表示这是一个原生DOM标签*/functioncreateVNode(type,props){constvnode={__v_isVNode:true,type,props,key:props,scopeId:1,children:null,component:null,el:null,shapeFlag:4,//表示组件,因为只有在创建vnode的时候根组件的是用到的,所以写下来patchFlag:0,appContext:null}returnvnode}//render什么都不做,只是调用patch方法//patch的第一个参数传入nullto表示这是第一次渲染,没有之前的vnodefordifffunctionrender(vnode,container){patch(null,vnode,container)}//递归挂载整个vue应用起点//根据shapeFlag,选择挂载组件还是挂载dom元素functionpatch(n1,n2,container,){const{shapeFlag}=n2if(typeofn2.type==="symbol"){processText(n1,n2,container)return}if(shapeFlag===17||shapeFlag===9){processElement(n1,n2,container)}elseif(shapeFlag===4){//第一次挂载这是分支processComponent(n1,n2,container)}}//什么都没做,调整了mountComponent方法functionprocessComponent(n1,n2,container){mountComponent(n2,container)}/***1.根据vnode创建创建它的实例*2.对实例进行各种处理,包装props,数据,调用组件的setup方法,生成组件的render方法*3.调用实例的render方法得到组件的vnodecomponent下的第一个realdom,递归调用patch(此时patch传入realdom的vnode)*/functionmountComponent(initialVNode,container){constinstance=(initialVNode.component=createComponentInstance(initialVNode,null,null))setupComponent(instance)setupRenderEffect(instance,initialVNode,container)}//根据组件vnode创建对应的实例//这里需要注意两个属性:appContext,组件,这两个属性继承根实例对应的属性//这样有利于在各个组件中获取项目共享的一些属性和方法,任何组件都可以获得全局注册的组件函数createComponentInstance(vnode,parent,suspense){//继承父应用程序上下文-或者-如果是根,则采用根vnodeconstappContext=(parent?parent.appContext:vnode.appContext)||{}constinstance={uid:uid++,vnode,parent,appContext,type:vnode.type,root:null,//立即设置next:null,subTree:null,//将在创建更新后同步设置:null,//将在创建后同步设置render:null,//statectx:{},data:{},props:{},attrs:{},slots:{},refs:{},setupState:{},setupContext:null,//每个实例资产存储(在选项解析期间可变)components:Object.create(appContext.components||[])}instance.ctx={_:instance}instance.root=parent?parent.root:instancereturninstance}//这个主要是调用组件的setup方法获取实例的render方法(其实还有很多任务,比如处理数据,props……这里先不提)functionsetupComponent(instance){constComponent=instance.typeconst{setup}=ComponentconstsetupResult=setup(instance.props,{})instance.render=setupResult}/***1.使用effect包裹响应式操作*2.响应式操作主要有两个东西*2.1调用render获取子树实例的方法*2.2递归调用patch(subTree,container)*/functionsetupRenderEffect(instance,initialVNode,container){//createreactiveeffectforrendering//Viccreatesanupdatefunctioninstance.update=effect(functioncomponentEffect(){const{el}=initialVNode//Vic在这里创建组件根dom的vNode树(这棵树中的孩子也创建vnode)constsubTree=(instance.subTree=renderComponentRoot(instance))el&&container.removeChild(el)//Vic的方法它实际上开始了s在内存中构建dom树patch(null,subTree,container)initialVNode.el=subTree.el})}//调用实例的render方法获取组件实例下根dom节点的vnode(通过要生成的h函数)functionrenderComponentRoot(instance){const{props,slots,attrs,emit,render}=instanceletresult//函数式组件result=render(props,{attrs,slots,emit})returnresult}//话不多说,调用mountElement方法functionprocessElement(n1,n2,container){if(n1==null){mountElement(n2,container)}}/***前言:能来这里说明vnode是domvnode(这里的type必须是astring)*1.根据类型创建dom节点*2.将创建的dom节点添加到容器中*3.处理props(属性和事件)*4.mountChildren(遍历children,递归调用patch)*/functionmountElement(vnode,container){const{type,props}=vnodeletel=vnode.el=document.createElement(type)container.appendChild(el)Object.entries(props||{}).forEach(([key,val])=>{if(key.startsWith('on')){el.addEventListener(key.substr(2).toLocaleLowerCase(),val)}else{el[key]=val}})if(!Array.isArray(vnode.children)){//递归终止点el.appendChild(document.createTextNode(vnode.children))}else{mountChildren(vnode.children,el,null)}}//每个孩子的补丁函数)}}//lackluster(递归的终止点)functionprocessText(n1,n2,container){n2.el=document.createTextNode(n2.children)container.appendChild(n2.el)}最后,本文旨在阐明mount递归挂载vue3应用整个过程的函数名与vue-next源码一致,但是隐藏了很多与mount无关的细节和分支。