petite-vue源码分析-从静态视图开始
代码库结构介绍示例各种使用示例脚本包发布脚本测试测试用例srcdirectivesv-ifapp.tscreateAppfunctionblock.ts等内置指令的实现块对象上下文。ts上下文对象eval.ts提供表达式计算函数如v-if="count===1"scheduler.ts调度器utils.ts工具函数walk.ts模板解析如果要自己构建版本,在里面执行即可控制台npmrunbuild就可以了。深入理解静态视图的渲染过程静态视图是指第一次渲染后,不会因为UI状态改变而触发重新渲染。其中,视图不包含任何UI状态,第一次渲染后不再根据UI状态更新状态。本文将解释前者。示例:
第一次进入它是createApp方法,它的作用是创建根上下文对象(rootcontext)、全局作用域对象(rootscope)并返回mount、unmount和directive方法。然后使用mount方法找到具有[v-scope]属性的子节点(不包括匹配[v-scope][v-scope]的后代节点),并为其创建根块对象。源码如下(基于这个例子,为了方便阅读,我截取了部分源码)://file./src/app.tsexportconstcreateApp=(initialData:any)=>{//创建根上下文objectconstctx=createContext()//全局作用域对象,作用域对象实际上是一个反应对象ctx.scope=reactive(initialData)/*将作用域的函数成员的this绑定到作用域。*如果使用箭头函数给函数成员赋值,则上述操作对该函数成员无效。*/bindContextMethods(ctx.scope)/*根块对象集合*petite-vue支持多个根块对象,但是这里我们可以简化为只支持一个根块对象。*/letrootBlocks:Block[]return{//简化为必须挂载在带有`[v-scope]`的元素下mount(el:Element){letroots=el.hasAttribute('v-scope')?[el]:[]//创建根块对象rootBlocks=roots.map(el=>newBlock(el,ctx,true))returnthis},unmount(){//当节点被卸载(removeChild)时执行块对象清理。注意:该动作不会在界面刷新时触发。rootBlocks.forEach(block=>block.teardown())}}}虽然代码很短,但是引出了三个核心对象:上下文对象(context)、作用域(scope)和块对象(block)。它们之间的关系是:上下文对象(context)和作用域(scope)是1对1的关系;上下文对象(context)和块对象(block)是多对多的关系,其中块对象(block)指向当前的上下文对象(context),并通过指向父上下文对象(context)父Ctx;作用域(scope)和块对象(block)是一对多的关系。具体结论是:根上下文对象(context)可以通过ctx被多个根块对象引用;当创建块对象(block)时,会在当前上下文对象(context)的基础上创建一个新的上下文对象(context),并由parentCtx指向原来的上下文对象(context);在解析过程中,v-scope会在当前作用域对象的基础上构建一个新的作用域对象,并复制当前的上下文对象(context),形成一个新的上下文对象(context),供子节点解析和渲染,但不影响当前块对象指向的上下文。下面我们一一了解。作用域(scope)这里的作用域与我们写JavaScript时所说的作用域是一致的。用于限制函数和变量的可用范围,减少命名冲突。它具有以下特点:作用域之间存在父子关系和兄弟关系,整体形成一棵作用域树;子作用域中的变量或属性可以覆盖祖先作用域中具有相同名称的变量或属性的可访问性;如果只有祖先作用域存在对变量或属性的赋值,将赋值给祖先作用域的变量或属性。//全局范围varglobalVariable='hello'varmessage1='there'varmessage2='bye'(()=>{//局部范围Aletmessage1='localscopeA'message2='seeyou'console.log(globalVariable,message1,message2)})()//echo:hellolocalscopeAseeyou(()=>{//localscopeBconsole.log(globalVariable,message1,message2)})()//Echo:你好,看到你了,范围是附加到上下文的,所以范围的创建和销毁自然位于上下文的实现中(./src/context.ts)。另外,petite-vue中的scope并不是普通的JavaScript对象,而是经过@vue/reactivity处理的响应式对象。目的是一旦作用域成员被修改,触发相关副作用函数的执行,从而重新渲染界面。块对象(block)作用域(scope)用于管理JavaScript变量和函数的可用作用域,而块对象(block)用于管理DOM对象。//文件./src/block.ts//基于示例,我截断了代码exportclassBlock{template:Element|DocumentFragment//不是指向$template,而是当前解析的模板元素ctx:Context//有一个块对象创建的上下文对象parentCtx?:Context//当前块对象所属的上下文对象,根块objecthasnocontextobject//基于上面的例子,没有使用
元素,静态视图不包含任何UI状态,所以我简化了代码construct(template:Element,parentCtx:Context,isRoot=false){if(isRoot){//对于根块对象,直接使用挂载点元素作为模板元素this.template=template}if(isRoot){this.ctx=parentCtx}//使用深度优先策略解析元素(解析过程会将渲染任务推入异步任务队列)walk(this.template,this.ctx)}}//file./src/walk.ts//基于上面的例子,静态视图做不包含任何UI状态,所以我简化了代码exportconstwalk=(node:Node,ctx:Context):ChildNod电子|空|void=>{常量类型=节点。nodeTypeif(type===1){//节点是元素类型constel=nodeasElementletexp:string|nullif((exp=checkAttr(el,'v-scope'))||exp===''){//具有`v-scope`的元素将计算最新的目标。如果`v-scope`的值为空,则最新的作用域对象是一个空对象constscope=exp?evaluate(ctx.scope,exp):{}//更新当前上下文的作用域ctx=createScopedContext(ctx,scope)//如果`$template`存在于当前作用域中并作为在线模板渲染到DOM树,后面会递归解析//注意:这里不会读取父scope的`$template`属性,必须是当前角色Domain的if(scope.$template){resolveTemplate(el,scope.$template)}}walkChildren(el,ctx)}}//先解析第一个子节点,如果没有子节点,则解析兄弟节点constwalkChildren=(node:Element|DocumentFragment,ctx:Context)=>{letchild=node.firstChildwhile(child){child=walk(child,ctx)||}child.nextSibling}}//我在上面例子的基础上简化了代码constresolveTemplate=(el:Element,template:string)=>{//感谢Vue使用的模板完全符合HTML规范,所以直接简单渲染成HTML元素后,@click和:value等属性名还是不会丢失el.innerHTML=template}为了更易读,我简化了表达式计算代码(开发阶段去掉hint和缓存机制)//file./src/eval.tsexportconstevaluate=(scope:any,exp:string,el?Node)=>execute(scope,exp,el)constexecute=(scope:any,exp:string,el?Node)=>{constfn=toFunction(exp)returnfn(scope,el)}consttoFunction=(exp:string):Function=>{try{returnnewFunction('$data','$el',`with($data){返回(${exp})}`)}catch(e){return()=>{}}}上下文对象(context)上面我们了解到作用域(scope)是用来管理JavaScript变量和函数可用作用域的,而block对象(block)用于管理DOM对象,上下文对象(context)是连接作用域(scope)和块对象(block)的载体,也是将多个块对象组成树的连接点结构([根块对象.ctx]->[根上下文对象,根上下文对象.块]->[子块对象]->[子上下文对象])//File./src/context.tsexportinterfaceContext{scope:Record//当前上下文对应的scope对象cleanups:(()=>void)[]//当前上下文指令:Block[]//属于当前上下文的块对象effect:typeofrawEffect//类似@vue/reactivity的effect方法,但是可以根据条件选择调度方式effects:ReativeEffectRunner[]//当前context持有sideeffectmethod,用于在context销毁时回收sideeffectmethod释放资源}/***由Block构造函数调用创建新的context对象,特性如下:*1.新上下文对象的作用域与父上下文对象一致*2.新上下文对象拥有全新的Effects、blocks和cleanups成员*结论:Block构造函数发起的上下文对象的创建确实不影响作用域对象ct,但是上下文对象会独立管理它的副作用方法、块对象和指令*/exportconstcreateContext=(parent?上下文):上下文=>{constctx:上下文={...父级,范围:父级?parent.scope:reactive({}),//指向父上下文作用域对象effects:[],blocks:[],cleanups:[],effect:fn=>{//当解析遇到`v-once`属性,`inOnce`设置为`true`,副作用函数`fn`被直接压入异步任务队列执行一次,即使它依赖的状态发生变化,副作用函数也不会被触发.if(inOnce){queueJob(fn)returnfnasany}//生成状态改变时自动触发的副作用函数conste:ReactiveEffectRunner=rawEffect(fn,{scheduler:()=>queueJob(e)})ctx。effects.push(e)returne}}returnctx}/***解析时遇到`v-scope`属性且有有效值时,会调用该方法,根据currentscope,并复制当前的context属性构造新的context对象,用于子节点的解析和渲染。*/exportconstcreateScopedContext=(ctx:Context,data={}):Context=>{constparentScope=ctx.scope/*构造scope对象原型链*此时如果当前scope不存在set属性,该属性将在当前范围内创建并分配一个值。*/cosntmergeScope=Object.create(parentScope)Object.defineProperties(mergeScope,Object.getOwnPropertyDescriptors(data))//构造ref对象原型链mergeScope.$ref=Object.create(parentScope.$refs)//构造scopeChainconstreactiveProxy=reactive(newProxy(mergeScope,{set(target,key,val,receiver){//如果当前作用域中不存在set属性,则将值设置到父作用域,因为父作用域创建于同理,所以递归找到拥有该属性的祖先作用域并赋值(target,key,val,receiver)}}))/*将作用域的函数成员的this绑定到作用域。*如果使用箭头函数给函数成员赋值,则上述操作对该函数成员无效。*/bindContextMethods(reactiveProxy)return{...ctx,scope:reactiveProxy}}人肉单步调试调用createApp根据入参生成全局作用域rootScope,创建根上下文rootCtx;callmountas 构造根块对象rootBlock,并以此为模板进行解析处理;解析时,识别v-scope属性,根据全局作用域rootScope获取局部作用域scope,以根上下文rootCtx为蓝图,共同构建新的上下文ctx,用于子节点的解析和渲染;获取$template属性值并生成HTML元素;深度优先遍历解析子节点。未完待续通过简单的例子,我们对petite-vue的解析、调度、渲染过程有了一定的了解。在下一篇文章中,我们将再次通过静态视图看到v-if和v-for是如何根据状态改变DOM树的。结构化的。另外,有的朋友可能会有以下疑问:Proxy的receiver是什么?newFunction和eval的区别?这些后续会在专题文章中介绍,敬请期待:)《Petite-Vue源码剖析》小册子《Petite-Vue源码剖析》从在线渲染、响应式系统和沙箱模型逐行解读源码,并在响应式中使用JS系统引擎SMI优化依赖清理算法进行详细分析。绝对是Vue3源码入门前的绝佳敲门砖。喜欢的话记得转发和欣赏哦!