当前位置: 首页 > 科技观察

深入Vue2.0底层思考——模板渲染

时间:2023-03-19 02:27:24 科技观察

初衷是在使用vue2.0的过程中,有时候光看API很难理解vue作者的想法,这就提示我想深入了解vue的底层思想,理解一些底层的Thinking,才能更好的使用框架,虽然网上已经有很多源码分析文档,但是我觉得只有自己动手才能你会更感动。vue2.0和1.0模板渲染的区别Vue2.0中的模板渲染与Vue1.0完全不同。1.0使用的是DocumentFragment(想了解更多的可以看这篇文章),而2.0使用的是React的VirtualDOM。基于VirtualDOM,2.0还可以支持服务器端渲染(SSR)和JSX语法。知识普及在开始阅读源码之前,先了解一些相关知识:AST数据结构、VNode数据结构、createElement问题、render函数。AST数据结构AST的全称是抽象语法树(AbstractSyntaxTree),是对源代码抽象语法结构的一种树状表示,是计算机科学中编译原理的概念。而vue就是将模板代码映射成AST数据结构进行语法分析。我们看一下Vue2.0源码中AST数据结构的定义:declaretypeASTNode=ASTElement|ASTText|ASTExpressiondeclaretypeASTElement={//相关元素的一些定义type:1;tag:string;attrsList:Array{name:string;value:string}>;attrsMap:{[key:string]:string|null};parent:ASTElement|void;children:ArrayASTNode>;//...}declaretypeASTExpression={type:2;expression:string;text:细绳;static?:boolean;}declaretypeASTText={type:3;text:string;static?:boolean;}我们看到ASTNode有三种形式:ASTElement、ASTText、ASTExpression。使用属性类型来区分。VNode数据结构下面是Vue2.0源码中VNode数据结构的定义(下面介绍内容相关注释):constructor{this.tag=tag//elementtagthis.data=data//attributethis.children=children//子元素列表this.text=textthis.elm=elm//对应的真实DOM元素this.ns=undefinedthis.context=contextthis.functionalContext=undefinedthis.key=data&&data.keythis.componentOptions=componentOptionsthis.componentInstance=undefinedthis.parent=undefinedthis.raw=falsethis.isStatic=false//是否标记为静态节点?不是直接使用原生DOM元素,而是使用真实DOM元素的简化VNode。最大的原因是document.createElement创建的是真正的DOM元素,会造成性能损失。我们来看一个document.createElement方法的例子letdiv=document.createElement('div');for(letkindiv){console.log(k);}打开控制台运行上面的代码,你会发现打印出来属性多达228个,其中90%以上的属性对我们来说是无用的。VNode是真实DOM元素的简化版,与真实的dom相关联,比如属性elm,里面只包含了我们需要的属性,并且增加了一些diff过程中需要用到的属性,比如isStatic。render函数是通过编译模板文件得到的,其运行结果为VNode。渲染功能类似于JSX。除了Template,Vue2.0还支持JSX编写。您可以使用Vue.compile(template)方法编译以下模板。divid="app">header>h1>Iamatemplate!/h1>/header>pv-if="message">{{message}}/p>pv-else>Nomessage./p>/div>方法会返回一个Object,对象中有render和staticRenderFns两个值。看一下生成的渲染函数(function(){with(this){return_c('div',{//创建一个div元素attrs:{"id":"app"}//div添加属性id},[_m(0),//静态节点头,这里对应staticRenderFns数组索引为0的render函数_v(""),//空文本节点(message)//三元表达式,判断message是否存在//如果存在,创建一个p元素,里面有文本,值为toString(message)?_c('p',[_v("\n"+_s(message)+"\n")])//如果不存在,创建p元素,元素中有文本,值为Nomessage.:_c('p',[_v("\nNomessage.\n")])])}})上面的render函数,你只需要了解_c,_m,_v,_s这几个函数的定义,其中_c是createElement(创建元素),_m是renderStatic(渲染静态节点),_v是createTextVNode(创建文本dom),_s就是toString(转换成字符String)除了render函数,还有一个staticRenderFns数组。该数组中的函数与VDOM中diff算法的优化有关。我们将在编译阶段稍后用静态标签true标记不会更改的VNode节点。那些标记为静态节点的AVNode会单独生成一个staticRenderFns函数(上面render函数中的function(){//_m(0)会调用这个方法with(this){return_c('header',[_c('h1',[_v("I'matemplate!")])])}})模板渲染过程(重要函数介绍)了解了一些基础知识后,我们来讲解一下模板渲染过程$mount函数,主要是获取模板,以及然后进入compileToFunctions函数。compileToFunctions函数主要是将模板编译成render函数。首先读取缓存,如果没有缓存,则调用compile方法获取render函数的字符串形式,然后通过newFunction的方式生成render函数。//如果有缓存,直接在缓存中取template,options)//后面会详细讲解Compileres.render=makeFunction(compiled.render)//通过newFunction生成渲染函数并缓存constl=compiled.staticRenderFns.lengthres.staticRenderFns=newArray(l)for(leti=0;il;i++){res.staticRenderFns[i]=makeFunction(compiled.staticRenderFns[i])}...}return(cache[key]=res)//记录在缓存中的compile函数就是编译的templateintorender函数的字符串形式,我们将在下一节中详细讨论。render方法生成后,会进入_mount进行DOM更新。该方法核心逻辑如下://触发beforeMount生命周期钩子callHook(vm,'beforeMount')//关键点:新建一个Watcher并赋值给vm._watchervm._watcher=newWatcher(vm,functionupdateComponent)(){vm._update(vm._render(),hydrating)},noop)hydrating=false//manuallymountedinstance,callmountedonself//mountediscalledforrender-createdchildcomponentsinitsinsertedhookif(vm.$vnode==null){vm._isMounted=truecallHook(vm,'mounted')}returnvmfirst会创建一个新的watcher对象(主要是连接模板和数据)。watcher对象创建完成后,会运行传入的方法vm._update(vm._render(),hydrating)。vm._render()的主要作用是运行前面编译器生成的render方法,返回一个vNode对象。vm.update()会将新的vdom与当前的vdom进行比较,并将差异渲染到真实的DOM树。推荐一张图,响应式工程流程(如果想了解watcher背后的实现原理,可以看这篇Vue2.0源码阅读:响应式原理)函数字符串形式。exportfunctioncompile(template:string,options:CompilerOptions):CompiledResult{constAST=parse(template.trim(),options)//1.parseoptimize(AST,options)//2.optimizeconstcode=generate(AST,options)//3.generatereturn{AST,render:code.render,staticRenderFns:code.staticRenderFns}}这个函数主要分为解析、优化和生成三个步骤,分别输出一个包含AST、staticRenderFns对象和render函数的字符串。parse函数,主要作用是将模板字符串解析成AST。上面定义了ASTElement的数据结构,parse函数就是将模板中的结构(指令、属性、标签等)转换成AST形式存储在ASTElement中,最后解析生成AST。optimize函数(src/compiler/optimizer.js)的主要作用是标记静态节点,以优化后续patch过程中新旧VNode树结构的对比。标记为static的节点在后续的diff算法中会直接忽略,不做详细比较。generate函数(src/compiler/codegen/index.js)的主要作用是根据AST结构拼接生成render函数的字符串。constcode=AST?genElement(AST):'_c("div")'staticRenderFns=prevStaticRenderFnsonceCount=prevOnceCountreturn{render:`with(this){return${code}}`,//最外层包含一个with(this)然后返回staticRenderFns:currentStaticRenderFns}其中genElement函数(src/compiler/codegen/index.js)会根据AST的属性调用不同的方法生成字符串并返回。functiongenElement(el:ASTElement):string{if(el.staticRoot&&!el.staticProcessed){returngenStatic(el)}elseif(el.once&&!el.onceProcessed){returngenOnce(el)}elseif(el.for&&!el.forProcessed){returgenFor(el)}elseif(el.if&&!el.ifProcessed){returgenIf(el)}elseif(el.tag==='template'&&!el.slotTarget){returngenChildren(el)||'void0'}elseif(el.tag==='slot'){}returncode}}以上就是编译函数中三个核心步骤的介绍。编译后得到render函数的字符串形式,再通过newFunction函数得到真正的渲染。数据发生变化后,会执行Watcher中的_update函数(src/core/instance/lifecycle.js)。_update函数将执行渲染函数并输出一个新的VNode树结构数据。然后调用patch函数比较新的VNode和旧的VNode,只有变化的节点才会更新到真正的DOM树中。patchpatch.js是新旧VNode对比的diff函数,主要是优化dom,通过算法最小化操作dom的行为。diff算法来源于snabbdom,它是VDOM思想的核心。snabbdom算法针对DOM操作跨层添加和删除更少节点的目标进行了优化。只会在同级进行,不会跨级比较。如果想深入了解VNodediff算法的原理,可以看(vue2.0的diff算法解析)总结编译函数将模板转成AST,优化AST,再转AST进入渲染功能;渲染函数和数据通过Watcher关联;当数据发生变化时,调用patch函数,执行render函数,生成新的VNode,与旧的VNode进行diff,最后更新DOM树。