浅谈vue的第一个commit分析
为什么写这篇vue的分析文章?
对于天资愚钝的前端(我)来说,阅读源码是件不容易的事情,毕竟有时候看源码分析的文章都看不懂。每次看到大佬们用了1~2年的vue就能掌握原理,甚至精通源码,再看看自己用了好几年都还在基本的使用阶段,心中总是羞愧不已。如果一直满足于基本的业务开发,怕是得在初级水平一直待下去了吧。所以希望在学习源码的同时记录知识点,可以让自己的理解和记忆更加深刻,也方便将来查阅。
目录结构
本文以vue的第一次commita879ec06作为分析版本
├──build │└──build.js//`rollup`打包配置 ├──dist │└──vue.js ├──package.json ├──src//vue源码目录 │├──compiler//将vue-template转化为render函数 ││├──codegen.js//递归ast提取指令,分类attr,style,class,并生成render函数 ││├──html-parser.js//通过正则匹配将html字符串转化为ast ││├──index.js//compile主入口 ││└──text-parser.js//编译{{}} │├──config.js//对于vue的全局配置文件 │├──index.js//主入口 │├──index.umd.js//未知(应该是umd格式的主入口) │├──instance//vue实例函数 ││└──index.js//包含了vue实例的初始化,compile,data代理,methods代理,watch数据,执行渲染 │├──observer//数据订阅发布的实现 ││├──array.js//实现array变异方法,$set$remove实现 ││├──batcher.js//watch执行队列的收集,执行 ││├──dep.js//订阅中心实现 ││├──index.js//数据劫持的实现,收集订阅者 ││└──watcher.js//watch实现,订阅者 │├──util//工具函数 ││├──component.js ││├──debug.js ││├──dom.js ││├──env.js//nexttick实现 ││├──index.js ││├──lang.js ││└──options.js │└──vdom │├──dom.js//dom操作的封装 │├──h.js//节点数据分析(元素节点,文本节点) │├──index.js//vdom主入口 │├──modules//不同属性处理函数 ││├──attrs.js//普通attr属性处理 ││├──class.js//class处理 ││├──events.js//event处理 ││├──props.js//props处理 ││└──style.js//style处理 │├──patch.js//node树的渲染,包括节点的加减更新处理,及对应attr的处理 │└──vnode.js//返回最终的节点数据 └──webpack.config.js//webpack配置
从template到html的过程分析
我们的代码是从newVue()开始的,Vue的构造函数如下:
constructor(options){ //options就是我们对于vue的配置 this.$options=options this._data=options.data //获取元素html,即template constel=this._el=document.querySelector(options.el) //编译模板->render函数 constrender=compile(getOuterHTML(el)) this._el.innerHTML='' //实例代理data数据 Object.keys(options.data).forEach(key=>this._proxy(key)) //将method的this指向实例 if(options.methods){ Object.keys(options.methods).forEach(key=>{ this[key]=options.methods[key].bind(this) }) } //数据观察 this._ob=observe(options.data) this._watchers=[] //watch数据及更新 this._watcher=newWatcher(this,render,this._update) //渲染函数 this._update(this._watcher.value) }
当我们初始化项目的时候,即会执行构造函数,该函数向我们展示了vue初始化的主线:编译template字符串=>代理data数据/methods的this绑定=>数据观察=>建立watch及更新渲染
1.编译template字符串
constrender=compile(getOuterHTML(el))
其中compile的实现如下:
exportfunctioncompile(html){ html=html.trim() //对编译结果缓存 consthit=cache[html] //parse函数在parse-html中定义,其作用是把我们获取的html字符串通过正则匹配转化为ast,输出如下{tag:'div',attrs:{},children:[]} returnhit||(cache[html]=generate(parse(html))) }
接下来看看generate函数,ast通过genElement的转化生成了构建节点html的函数,在genElement将对iffor等进行判断并转化(指令的具体处理将在后面做分析,先关注主流程代码),最后都会执行genData函数
//生成节点主函数 exportfunctiongenerate(ast){ constcode=genElement(ast) //执行code代码,并将this作为code的global对象。所以我们在template中的变量将指向为实例的属性{{name}}->this.name returnnewFunction(`with(this){return$[code]}`) } //解析单个节点->genData functiongenElement(el,key){ letexp //指令的实现,实际就是在模板编译时实现的 if(exp=getAttr(el,'v-for')){ returngenFor(el,exp) }elseif(exp=getAttr(el,'v-if')){ returngenIf(el,exp) }elseif(el.tag==='template'){ returngenChildren(el) }else{ //分别为tag自身属性子节点数据 return`__h__('${el.tag}',${genData(el,key)},${genChildren(el)})` } }
我们可以看看在genData中都做了什么。上面的parse函数将html字符串转化为ast,而在genData中则将节点的attrs数据进一步处理,例如class->renderClassstyleclasspropsattr分类。在这里可以看到bind指令的实现,即通过正则匹配:和bind,如果匹配则把相应的value值转化为(value)的形式,而不匹配的则通过JSON.stringify()转化为字符串('value')。最后输出attrs的(key-value),在这里得到的对象是字符串形式的,例如(value)等也仅仅是将变量名,而在generate中通过newFunction进一步通过(this.value)得到变量值。
functiongenData(el,key){ //没有属性返回空对象 if(!el.attrs.length){ return'{}' } //key letdata=key?`{key:${key},`:`{` //class处理 if(el.attrsMap[':class']||el.attrsMap['class']){ data+=`class:_renderClass(${el.attrsMap[':class']},"${el.attrsMap['class']||''}"),` } //attrs letattrs=`attrs:{` letprops=`props:{` lethasAttrs=false lethasProps=false for(leti=0,l=el.attrs.length;i而对于genChildren,我们可以猜到就是对ast中的children进行遍历调用genElement,实际上在这里还包括了对文本节点的处理。
//遍历子节点->genNode functiongenChildren(el){ if(!el.children.length){ return'undefined' } //对children扁平化处理 return'__flatten__(['+el.children.map(genNode).join(',')+'])' } functiongenNode(node){ if(node.tag){ returngenElement(node) }else{ returngenText(node) } } //解析{{}} functiongenText(text){ if(text===''){ return'""' }else{ constexp=parseText(text) if(exp){ return'String('+escapeNewlines(exp)+')' }else{ returnescapeNewlines(JSON.stringify(text)) } } }genText处理了text及换行,在parseText函数中利用正则解析{{}},输出字符串(value)形式的字符串。
现在我们再看看__h__('${el.tag}',${genData(el,key)},${genChildren(el)})中__h__函数
//h函数利用上面得到的节点数据得到vNode对象=>虚拟dom exportdefaultfunctionh(tag,b,c){ vardata={},children,text,i if(arguments.length===3){ data=b if(isArray(c)){children=c} elseif(isPrimitive(c)){text=c} }elseif(arguments.length===2){ if(isArray(b)){children=b} elseif(isPrimitive(b)){text=b} else{data=b} } if(isArray(children)){ //子节点递归处理 for(i=0;i到此为止,我们分析了constrender=compile(getOuterHTML(el)),从el的html字符串到render函数都是怎么处理的。
2.代理data数据/methods的this绑定
//实例代理data数据 Object.keys(options.data).forEach(key=>this._proxy(key)) //将method的this指向实例 if(options.methods){ Object.keys(options.methods).forEach(key=>{ this[key]=options.methods[key].bind(this) }) }实例代理data数据的实现比较简单,就是利用了对象的setter和getter,读取this数据时返回data数据,在设置this数据时同步设置data数据
_proxy(key){ if(!isReserved(key)){ //needtostorereftoselfhere //becausethesegetter/settersmight //becalledbychildscopesvia //prototypeinheritance. varself=this Object.defineProperty(self,key,{ configurable:true, enumerable:true, get:functionproxyGetter(){ returnself._data[key] }, set:functionproxySetter(val){ self._data[key]=val } }) } }3.Obaerve的实现
Observe的实现原理在很多地方都有分析,主要是利用了Object.defineProperty()来建立对数据更改的订阅,在很多地方也称之为数据劫持。下面我们来学习从零开始建立这样一个数据的订阅发布体系。
从简单处开始,我们希望有个函数可以帮我们监听数据的改变,每当数据改变时执行特定回调函数
functionobserve(data,callback){ if(!data||typeofdata!=='object'){ return } //遍历key Object.keys(data).forEach((key)=>{ letvalue=data[key]; //递归遍历监听深度变化 observe(value,callback); //监听单个可以的变化 Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ returnvalue; }, set(val){ if(val===value){ return } value=val; //监听新的数据 observe(value,callback); //数据改变的回调 callback(); } }); }); } //使用observe函数监听data constdata={}; observe(data,()=>{ console.log('data修改'); })上面我们实现了一个简单的observe函数,只要我们将编译函数作为callback传入,那么每次数据更改时都会触发回调函数。但是我们现在不能为单独的key设置监听及回调函数,只能监听整个对象的变化执行回调。下面我们对函数进行改进,达到为某个key设置监听及回调。同时建立调度中心,让整个订阅发布模式更加清晰。
//首先是订阅中心 classDep{ constructor(){ this.subs=[];//订阅者数组 } addSub(sub){ //添加订阅者 this.subs.push(sub); } notify(){ //发布通知 this.subs.forEach((sub)=>{ sub.update(); }); } } //当前订阅者,在getter中标记 Dep.target=null; //订阅者 classWatch{ constructor(express,cb){ this.cb=cb; if(typeofexpress==='function'){ this.expressFn=express; }else{ this.expressFn=()=>{ returnnewFunction(express)(); } } this.get(); } get(){ //利用Dep.target存当前订阅者 Dep.target=this; //执行表达式->触发getter->在getter中添加订阅者 this.expressFn(); //及时置空 Dep.taget=null; } update(){ //更新 this.cb(); } addDep(dep){ //添加订阅 dep.addSub(this); } } //观察者建立观察 classObserve{ constructor(data){ if(!data||typeofdata!=='object'){ return } //遍历key Object.keys(data).forEach((key)=>{ //key=>dep对应 constdep=newDep(); letvalue=data[key]; //递归遍历监听深度变化 constobserve=newObserve(value); //监听单个可以的变化 Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ if(Dep.target){ constwatch=Dep.target; watch.addDep(dep); } returnvalue; }, set(val){ if(val===value){ return } value=val; //监听新的数据 newObserve(value); //数据改变的回调 dep.notify(); } }); }); } } //监听数据中某个key的更改 constdata={ name:'xiaoming', age:26 }; constobserve=newObserve(data); constwatch=newWatch('data.age',()=>{ console.log('ageupdate'); }); data.age=22现在我们实现了订阅中心,订阅者,观察者。观察者监测数据的更新,订阅者通过订阅中心订阅数据的更新,当数据更新时,观察者会告诉订阅中心,订阅中心再逐个通知所有的订阅者执行更新函数。到现在为止,我们可以大概猜出vue的实现原理:
- 建立观察者观察data数据的更改(newObserve)
- 在编译的时候,当某个代码片段或节点依赖data数据,为该节点建议订阅者,订阅data中某些数据的更新(newWatch)
- 当dada数据更新时,通过订阅中心通知数据更新,执行节点更新函数,新建或更新节点(dep.notify())
上面是我们对vue实现原理订阅发布模式的基本实现,及编译到更新过程的猜想,现在我们接着分析vue源码的实现:
在实例的初始化中
//... //为数据建立数据观察 this._ob=observe(options.data) this._watchers=[] //添加订阅者执行render会触发getter订阅者订阅更新,数据改变触发setter订阅中心通知订阅者执行update this._watcher=newWatcher(this,render,this._update) //...vue中数据观察的实现
//observe函数 exportfunctionobserve(value,vm){ if(!value||typeofvalue!=='object'){ return } if( hasOwn(value,'__ob__')&& value.__ob__instanceofObserver ){ ob=value.__ob__ }elseif( shouldConvert&& (isArray(value)||isPlainObject(value))&& Object.isExtensible(value)&& !value._isVue ){ //为数据建立观察者 ob=newObserver(value) } //存储关联的vm if(ob&&vm){ ob.addVm(vm) } returnob } //=>Observe函数 exportfunctionObserver(value){ this.value=value //在数组变异方法中有用 this.dep=newDep() //observer实例存在__ob__中 def(value,'__ob__',this) if(isArray(value)){ varaugment=hasProto ?protoAugment :copyAugment //数组遍历,添加变异的数组方法 augment(value,arrayMethods,arrayKeys) //对数组的每个选项调用observe函数 this.observeArray(value) }else{ //walk->convert->defineReactive->setter/getter this.walk(value) } } //=>walk Observer.prototype.walk=function(obj){ varkeys=Object.keys(obj) for(vari=0,l=keys.length;iconvert Observer.prototype.convert=function(key,val){ defineReactive(this.value,key,val) } //重点看看defineReactive exportfunctiondefineReactive(obj,key,val){ //key对应的的订阅中心 vardep=newDep() varproperty=Object.getOwnPropertyDescriptor(obj,key) if(property&&property.configurable===false){ return } //兼容原有setter/getter //caterforpre-definedgetter/setters vargetter=property&&property.get varsetter=property&&property.set //实现递归监听属性val=obj[key] //深度优先遍历先为子属性设置reactive varchildOb=observe(val) //设置getter/setter Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:functionreactiveGetter(){ varvalue=getter?getter.call(obj):val //Dep.target为当前watch实例 if(Dep.target){ //dep为obj[key]对应的调度中心dep.depend将当前wtcher实例添加到调度中心 dep.depend() if(childOb){ //childOb.dep为obj[key]值val对应的observer实例的dep //实现array的变异方法和$set方法订阅 childOb.dep.depend() } //TODO:此处作用未知? if(isArray(value)){ for(vare,i=0,l=value.length;i 订阅中心的实现
letuid=0 exportdefaultfunctionDep(){ this.id=uid++ //订阅调度中心的watch数组 this.subs=[] } //当前watch实例 Dep.target=null //添加订阅者 Dep.prototype.addSub=function(sub){ this.subs.push(sub) } //移除订阅者 Dep.prototype.removeSub=function(sub){ this.subs.$remove(sub) } //订阅 Dep.prototype.depend=function(){ //Dep.target.addDep(this)=>this.addSub(Dep.target)=>this.subs.push(Dep.target) Dep.target.addDep(this) } //通知更新 Dep.prototype.notify=function(){ //stablizethesubscriberlistfirst varsubs=this.subs.slice() for(vari=0,l=subs.length;iwatch.update() subs[i].update() } } 订阅者的实现
exportdefaultfunctionWatcher(vm,expOrFn,cb,options){ //mixinoptions if(options){ extend(this,options) } varisFn=typeofexpOrFn==='function' this.vm=vm //vm的_watchers包含了所有watch vm._watchers.push(this) this.expression=expOrFn this.cb=cb this.id=++uid//uidforbatching this.active=true this.dirty=this.lazy//forlazywatchers //deps一个watch实例可以对应多个dep this.deps=[] this.newDeps=[] this.depIds=Object.create(null) this.newDepIds=null this.prevError=null//forasyncerrorstacks //parseexpressionforgetter/setter if(isFn){ this.getter=expOrFn this.setter=undefined }else{ warn('vue-liteonlysupportswatchingfunctions.') } this.value=this.lazy ?undefined :this.get() this.queued=this.shallow=false } Watcher.prototype.get=function(){ this.beforeGet() varscope=this.scope||this.vm varvalue try{ //执行expOrFn,此时会触发getter=>dep.depend()将watch实例添加到对应obj[key]的dep value=this.getter.call(scope,scope) } if(this.deep){ //深度watch //触发每个key的getterwatch实例将对应多个dep traverse(value) } //... this.afterGet() returnvalue } //触发getter,实现订阅 Watcher.prototype.beforeGet=function(){ Dep.target=this this.newDepIds=Object.create(null) this.newDeps.length=0 } //添加订阅 Watcher.prototype.addDep=function(dep){ varid=dep.id if(!this.newDepIds[id]){ //将新出现的dep添加到newDeps中 this.newDepIds[id]=true this.newDeps.push(dep) //如果已在调度中心,不再重复添加 if(!this.depIds[id]){ //将watch添加到调度中心的数组中 dep.addSub(this) } } } Watcher.prototype.afterGet=function(){ //切除key的getter联系 Dep.target=null vari=this.deps.length while(i--){ vardep=this.deps[i] if(!this.newDepIds[dep.id]){ //移除不在expOrFn表达式中关联的dep中watch的订阅 dep.removeSub(this) } } this.depIds=this.newDepIds vartmp=this.deps this.deps=this.newDeps //TODO:既然newDeps最终会被置空,这边赋值的意义在于? this.newDeps=tmp } //订阅中心通知消息更新 Watcher.prototype.update=function(shallow){ if(this.lazy){ this.dirty=true }elseif(this.sync||!config.async){ this.run() }else{ //ifqueued,onlyoverwriteshallowwithnon-shallow, //butnottheotherwayaround. this.shallow=this.queued ?shallow ?this.shallow :false :!!shallow this.queued=true //recordbefore-pusherrorstackindebugmode /*istanbulignoreif*/ if(process.env.NODE_ENV!=='production'&&config.debug){ this.prevError=newError('[vue]asyncstacktrace') } //添加到待执行池 pushWatcher(this) } } //执行更新回调 Watcher.prototype.run=function(){ if(this.active){ varvalue=this.get() if( ((isObject(value)||this.deep)&&!this.shallow) ){ //setnewvalue varoldValue=this.value this.value=value varprevError=this.prevError //... this.cb.call(this.vm,value,oldValue) } this.queued=this.shallow=false } } Watcher.prototype.depend=function(){ vari=this.deps.length while(i--){ this.deps[i].depend() } }wtach回调执行队列
在上面我们可以发现,watch在收到信息更新执行update时。如果非同步情况下会执行pushWatcher(this)将实例推入执行池中,那么在何时会执行回调函数,如何执行呢?我们一起看看pushWatcher的实现。
//batch.js varqueueIndex varqueue=[] varuserQueue=[] varhas={} varcircular={} varwaiting=false varinternalQueueDepleted=false //重置执行池 functionresetBatcherState(){ queue=[] userQueue=[] //has避免重复 has={} circular={} waiting=internalQueueDepleted=false } //执行执行队列 functionflushBatcherQueue(){ runBatcherQueue(queue) internalQueueDepleted=true runBatcherQueue(userQueue) resetBatcherState() } //批量执行 functionrunBatcherQueue(queue){ for(queueIndex=0;queueIndexconfig._maxUpdateCount){ warn( 'Youmayhaveaninfiniteupdateloopforwatcher'+ 'withexpression"'+watcher.expression+'"', watcher.vm ) break } } } } //添加到执行池 exportfunctionpushWatcher(watcher){ varid=watcher.id if(has[id]==null){ if(internalQueueDepleted&&!watcher.user){ //aninternalwatchertriggeredbyauserwatcher... //let'srunitimmediatelyaftercurrentuserwatcherisdone. userQueue.splice(queueIndex+1,0,watcher) }else{ //pushwatcherintoappropriatequeue varq=watcher.user ?userQueue :queue has[id]=q.length q.push(watcher) //queuetheflush if(!waiting){ waiting=true //在nextick中执行 nextTick(flushBatcherQueue) } } } } 4.patch实现
上面便是vue中数据驱动的实现原理,下面我们接着回到主流程中,在执行完watch后,便执行this._update(this._watcher.value)开始节点渲染
//_update=>createPatchFunction=>patch=>patchVnode=>(domapi) //vtree是通过compile函数编译的render函数执行的结果,返回了当前表示当前dom结构的对象(虚拟节点树) _update(vtree){ if(!this._tree){ //第一次渲染 patch(this._el,vtree) }else{ patch(this._tree,vtree) } this._tree=vtree } //在处理节点时,需要针对class,props,style,attrs,events做不同处理 //在这里注入针对不同属性的处理函数 constpatch=createPatchFunction([ _class,//makesiteasytotoggleclasses props, style, attrs, events ]) //=>createPatchFunction返回patch函数,patch函数通过对比虚拟节点的差异,对节点进行增删更新 //最后调用原生的domapi更新html returnfunctionpatch(oldVnode,vnode){ vari,elm,parent varinsertedVnodeQueue=[] //prehook for(i=0;i结尾
以上分析了vue从template到节点渲染的大致实现,当然也有某些地方没有全面分析的地方,其中template解析为ast主要通过正则匹配实现,及节点渲染及更新的patch过程主要通过节点操作对比来实现。但是我们对编译template字符串=>代理data数据/methods的this绑定=>数据观察=>建立watch及更新渲染的大致流程有了个比较完整的认知。
到此这篇关于浅谈vue的第一个commit分析的文章就介绍到这了,更多相关vuecommit内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!