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

vue数据渲染

时间:2023-04-01 10:53:59 vue.js

前言vue是如何将编译器中的代码转化为页面真正的元素的?这个过程涉及四个过程:模板编译成AST语法树、AST语法树构建渲染函数、渲染函数生成虚拟dom、虚拟dom编译成真实dom。前两个过程我们在上一期的Vue源码解读系列中已经介绍过,所以本篇继续解读上一篇,着重分析后两个过程。在解读整体流程中的代码之前,先来看一个vue编译渲染的整体流程图:vue会将用户编写的代码中标签中的代码解析成AST语法树,并然后转换处理后的AST生成对应的render函数。执行render函数后,会得到模板代码对应的虚拟dom。最后,通过对比更新虚拟dom中的新旧vnode节点,渲染得到最终的真实dom。有了这个整体的概念,下面我们结合源码??分析一下具体的数据渲染过程。从vm.$mount开始,vue使用$mount实例方法挂载vm,数据渲染过程发生在vm.$mount阶段。在这个方法中,最终调用了mountComponent方法,完成了数据的渲染。下面结合源码来看一下关键的几行代码:updateComponent=()=>{vm._update(vm._render(),hydrating)//生成虚拟dom和更新真实dom}这是在里面mountComponent方法,会定义一个updateComponent方法,其中vue会通过vm._render()函数生成一个虚拟dom,并将生成的vnode作为第一个参数传入vm._update()函数,完成从虚拟dom到真实dom渲染。第二个参数hydrating是服务端渲染相关的,在浏览器中不需要关心。最后这个函数会作为getter函数作为参数传递给vuewatch实例,用于在数据更新时触发依赖收集,完成数据响应式的实现。此过程超出了本文的范围。这里只要明白,当后续vue中的data数据发生变化时,就会触发updateComponent方法,完成页面数据的渲染更新。具体关键代码如下:newWatcher(vm,updateComponent,noop,{before(){if(vm._isMounted&&!vm._isDestroyed){//触发beforeUpdatehookcallHook(vm,'beforeUpdate')}}},true/*isRenderWatcher*/)hydrating=false//手动挂载的实例,调用mountedonself//mounted在其插入的钩子中为渲染创建的子组件调用if(vm.$vnode==null){vm._isMounted=true//触发挂载钩子callHook(vm,'mounted')}returnvm}代码中还有一点需要注意的是,在代码的最后,会进行判断。当vm挂载成功后,vue的挂载生命会调用Cycle钩子函数。这就是为什么当我们执行mountedhook中的代码时,vm已经挂载了。vm._render()接下来详细分析vue生成虚拟dom的过程。如上所述,这个过程是通过调用vm._render()方法完成的。该方法的核心逻辑是调用vm.$createElement方法生成vnode。代码如下:vnode=render.call(vm._renderProxy,vm.$createElement)其中vm.renderProxy是代理,代理vm,做一些错误处理,vm.$createElement是真正创建vnode的方法,方法定义如下:vm.$createElement=(a,b,c,d)=>createElement(vm,a,b,c,d,true)可以看出最终还是调用了createElement方法来实现vnode的生成逻辑。在进一步介绍createElement方法之前,先明确两个关键点,1.render函数的来源,2.vnode的来源是什么?render方法的来源其实在Vue内部定义了两个render方法的来源,一个是如果用户手写render方法,那么vue会调用用户自己写的render方法,也就是vm.$createElement中的以下代码;另一种是用户没有手写render方法,那么vue内部会把模板编译成render方法,也就是下面代码中的vm._c。然而,这两个渲染方法最终会调用createElement方法来生成一个虚拟dom//将createElementfn绑定到这个实例//以便我们在其中获得适当的渲染上下文。//argsorder:tag,data,children,normalizationType,alwaysNormalize//内部版本由从模板编译的渲染函数使用vm._c=(a,b,c,d)=>createElement(vm,a,b,c,d,false)//规范化始终应用于公共版本,用于//用户编写的渲染函数。vm.$createElement=(a,b,c,d)=>createElement(vm,a,b,c,d,true)vnode类vnode是用一个原生的js对象来描述dom节点的类。因为在浏览器中操作dom的成本非常高,所以使用vnode生成虚拟dom的成本比创建真实dom要低很多。vnode类的定义如下:exportdefaultclassVNode{tag:string|空白;//当前节点数据的标签名称:VNodeData|空白;//当前节点对应的对象children:?Array;//当前节点的子节点text:string|空白;//当前节点的文本elm:Node|空白;//当前虚拟节点对应的真实dom节点..../*创建一个空的VNode节点*/exportconstcreateEmptyVNode=(text:string='')=>{constnode=newVNode()node.text=textnode.isComment=truereturnnode}/*创建一个文本节点*/exportfunctioncreateTextVNode(val:string|number){returnnewVNode(undefined,undefined,undefined,String(val))}....你可以看到vnode类中定义了很多节点属性和一系列模仿真实dom生成各种节点的方法。通过操作这些属性和方法来达到模拟真实dom变化的目的。createElement有前面两点的知识储备,再返回分析createElement生成虚拟dom。createElement方法中的代码比较多,这里只介绍生成虚拟dom相关的代码。该方法一般创建并返回一个vnode节点。这个过程可以分为三件事:1.子节点的规范化处理;2、根据不同情况创建不同的vnode节点类型;3、vnode创建后的处理。下面开始分析这3个步骤:子节点的归一化处理过程是因为传入的参数中的子节点是任意类型的,vue最终生成的虚拟dom其实是一个树结构。每个vnode可能有几个子节点,这些子节点也应该是vnode类型。所以需要对子节点进行处理,将子节点处理成一个vnode类型的数组。同时需要根据render函数的来源,对子节点的数据结构做相应的处理。创建vnode节点的逻辑是为了处理不同情况下的tag。下面把具体的判断案例梳理一下:如果传入的标签是字符串,则进一步进行下面第二点和第三点的判断,如果不是字符串则创建一个组件类型的vnode节点。如果是内置标签,则创建对应的内置标签vnode节点。如果是组件标签,创建一个组件类型的vnode节点。否则,创建一个具有未定义命名空间的vnode。letvnode,nsif(typeoftag==='string'){letCtor//获取tag的命名空间ns=(context.$vnode&&context.$vnode.ns)||config.getTagNamespace(tag)//判断是否为内置标签,如果为内置标签,创建对应节点if(config.isReservedTag(tag)){//平台内置元素if(process.env.NODE_ENV!=='production'&&isDef(data)&&isDef(data.nativeOn)){warn(`v-on的.native修饰符仅在组件上有效,但它用于<${tag}>.`,context)}vnode=newVNode(config.parsePlatformTagName(tag),data,children,undefined,undefined,context)//如果是组件,创建组件类型节点//从中找到标签vm实例option的components,如果存在则为组件,并创建对应的Node,Ctor为组件的构造类}elseif((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options,'components',tag))){//componentvnode=createComponent(Ctor,data,context,children,tag)}else{//unkknownorunlistednamespacedelements//在运行时检查,因为当它的父组件规范化子组件时,它可能会被分配一个命名空间//其他情况,在运行时检查,因为父组件可能在序列化子组件时分配一个命名空间vnode=newVNode(tag,data,children,undefined,undefined,context)}}else{//直接组件选项/构造函数//当tag不是字符串时,为组件的构造类,创建组件节点vnode=createComponent(tag,data,context,children)}vnode创建后的处理也是根据情况进行一些if/else处理逻辑:如果vnode创建成功,并且是数组类型,如果vnode创建成功,则返回创建的vnode节点创建成功,并且有命名空间,递归地将命名空间应用到所有子节点。如果vnode没有创建成功,创建并返回一个空的vnode节点if(Array.isArray(vnode)){//如果vnode创建成功,并且是数组类型,返回创建的vnode节点returnvnode}elseif(isDef(vnode)){//如果vnode创建成功并有命名空间,递归地将命名空间应用到所有子节点if(isDef(ns))applyNS(vnode,ns)if(isDef(data))registerDeepBindings(data)returnvnode}else{//如果vnode没有成功创建,创建一个空节点returncreateEmptyVNode()}vm._update()vm._update()所做的就是让vm._render()复活生成的虚拟dom被呈现为真实的dom_update()方法。内部会调用vm.__patch__方法完成视图更新,最后调用createPatchFunction方法。这个方法有很多代码和逻辑。它在src/core/vdom/patch.js文件中定义。下面介绍具体的patch过程和过程中用到的关键方法:关键方法createElm:该方法会根据传入的虚拟dom节点创建一个真实的dom,并插入到它的父节点中sameVnode:判断新旧节点是否存在是同一个节点。patchVnode:当新旧节点为同一个节点时,调用该方法直接修改节点。在这个过程中,diff算法会循环比较子节点,然后重用或替换相应的节点。updateChildren方法:diff算法的具体实现过程。补丁流程第一步:判断老节点是否存在。如果不存在,则调用createElm()创建一个新的dom节点,否则进入第二步判断。if(isUndef(oldVnode)){//空挂载(可能作为组件),创建新的根元素isInitialPatch=truecreateElm(vnode,insertedVnodeQueue)}步骤2:使用sameVnode()判断新旧节点是否相同节点,如果是同一个节点,调用patchVnode()直接修改已有节点,否则进入第三步判断constisRealElement=isDef(oldVnode.nodeType)if(!isRealElement&&sameVnode(oldVnode,vnode)){//patchexistingrootnode/*当是同一个节点时,直接修改现有节点*/patchVnode(oldVnode,vnode,insertedVnodeQueue,null,null,removeOnly)}第三步:如果新旧节点不相同节点,调用createElm()创建一个新的dom,并在删除旧节点的同时更新父节点的占位符。else{....createElm(vnode,insertedVnodeQueue,//极其罕见的边缘情况:如果旧元素处于//离开转换中,则不要插入。只有在组合转换+//keep-alive+HOC时才会发生。(#4590)oldElm._leaveCb?null:parentElm,nodeOps.nextSibling(oldElm))//更新父占位节点元素,递归/*更新父亲的占位符节点*/if(isDef(vnode.parent)){letancestor=vnode.parentconstpatchable=isPatchable(vnode)while(ancestor){for(leti=0;i