Vue.js源码(二):列表渲染初探
时间:2023-03-14 10:35:49
科技观察
以下示例来自官网。虽然看起来比HelloWorld多了一个v-for,但是内部处理过程却多了很多。但这就是框架,只给你留下最美好的东西,让生活变得简单。
varvm=newVue({el:'#mountNode',data:{todos:[{text:'LearnJavaScript'},{text:'LearnVue.js'},{text:'BuildSomethingAwesome'}]}})本文将一起分析:观察数组终端指令v-for指令这里的流程recap先用几张图回顾整理一下之前的Vue.js源码(一):HelloWorld背后的内容,有助于理解本文的编译、链接、绑定流程:copmile阶段:main是获取指令的descriptorlink阶段:实例化指令,替换DOMbind阶段:调用指令的bind函数,创建watcher并用图片表示:observe数组初始化中的mergeoptions,proxy流程和HelloWorld流程基本一致,所以这里直接从observe开始分析。//filepath:src/observer/index.jsvarob=newObserver(value)//value=data={todos:[{message:'LearnJavaScript'},...]}//filepath:src/observer/index.jsexportfunctionObserver(value){this.value=valuethis.dep=newDep()def(value,'__ob__',this)if(isArray(value)){//数组分支varaugment=hasProto?protoAugment:copyAugment//选择增强方法augment(value,arrayMethods,arrayKeys)//增强数组this.observeArray(value)}else{//plainobjectbranchthis.walk(value)}}增强数组增广(augment)数组,即扩充数组使其可以检测到变化。这里面有两个东西,一个是拦截数组的mutation方法(导致数组本身发生变化的方法),另一个是提供两个方便的方法$set和$remove。拦截方式有两种,如果浏览器实现了__proto__则使用protoAugment,否则使用copyAugment。//filepath:src/util/evn.jsexportconsthasProto='__proto__'in{}//filepath:src/observer/index.js//拦截原型链函数protoAugment(target,src){target.__proto__=src}//filepath:src/observer/index.js//定义属性functioncopyAugment(target,src,keys){for(vari=0,l=keys.length;i
{{todo.text}}元素,替换开始和结束锚点(anchor)。锚点用于帮助插入最后的li节点创建一个FragmentFactory:工厂会编译移除的li节点,获取并缓存链接器,稍后使用链接器创建一个Fragment//filepath:/src/directives/public/for.jsbind(){//查找别名,赋值expression="todos"varinMatch=this.expression.match(/(.*)(?:in|of)(.*)/)if(inMatch){varitMatch=inMatch[1].match(/\((.*),(.*)\)/)if(itMatch){this.iterator=itMatch[1].trim()this.alias=itMatch[2].trim()}else{this.alias=inMatch[1].trim()}this.expression=inMatch[2]}...//创建锚点并移除LI元素this.start=createAnchor('v-for-start')this.end=createAnchor('v-for-end')replace(this.el,this.end)before(this.start,this.end)...//创建FragmentFactorythis.factory=newFragmentFactory(this.vm,this.el)}Fragment&FragmentFactory这里的Fragment并不是指DocumentFragment,而是Vue内部实现的一个类。源代码注释解释为:部分编译片段的抽象。可以选择使用子范围编译内容。FragmentFactory将编译{{todo.text}},并保存返回的链接器。在v-for中,当数组变化时,会创建作用域,会克隆模板,即{{todo.text}},会使用链接器,会实例化Fragment,然后挂在端锚上。在Fragment中调用linker时,是link和bind{{todo.text}},和HelloWorld一样,创建v-text实例和watcher。为什么scope可以通过v-for指令中的别名(alias)todo来访问循环变量?为什么会有$index、$key等特殊变量?因为使用了子作用域。还记得HelloWorld中的watcher是如何识别simplePath的吗?vargetter=newFunction('scope','returnscope.message;')这里说白了就是访问scope对象的todo、$index或者$key属性。在v-for指令中,它的父范围将被扩展,在这种情况下,父范围对象是vm本身。在调用工厂创建每个片段时,将通过以下方式为其创建一个合适的子作用域://filepath:/src/directives/public/for.jscreate(value,alias,index,key){//index是遍历数组时的下标//value是下标对应的数组元素//alias='todo'//key是遍历对象时的属性名...varparentScope=this._scope||this.vmvarscope=Object.create(parentScope)//以父作用域为原型链创建子作用域...withoutConversion(()=>{defineReactive(scope,alias,value)//给子作用域添加别名})defineReactive(scope,'$index',index)//Add$indextochildscope...varfrag=this.factory.create(host,scope,this._frag)...}detectchange在这里,基本上“初步”了ListRendering的过程,有很多概念在里面。深入一点,打算后面结合其他用到这些概念的地方来分析,应该能体会到它的巧妙设计。***举两个例子,回顾上面的内容例1:vm.todos[0].text='LearnJAVASCRIPT';更改是数组元素中的文本属性,因为v-text命令观察工厂创建的片段的待办事项。text,所以这里li元素对应的TextNode内容是直接通过v-text命令更新的。示例2:vm.todos.push({text:'LearnVueSourceCode'});添加了一个数组元素,v-for命令的watcher通知它更新,diff算法判断添加了一个元素,于是创建了scope,工厂clone了模板,创建了一个新的fragment,追加在前面#end-anchor的,片段中的v-text指令观察到新增元素的text属性,并将值更新为TextNode。在diff算法中查看更多数组操作。至此,你应该对官网上的这句话有了更深的理解:Vue.js不是使用虚拟DOM,而是使用实际的DOM作为模板,并保留对实际节点的引用以进行数据绑定。