You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这里要注意的是在方法 defineReactive 中 new Dep 形成了闭包,在 getter 时才调用 dep.depend(),setter 时才通知 dep.notify(),这个之后讲
接下来需要一个充当主体的角色,将值多路推送给观察者(Observer),Vuejs 中就是 Dep 了:
Dep
Dep 就是个内容发布者,它依托在一个可观察对象内(Observable)可以被多个观察者订阅(Watcher),多路推送消息。
classDep{statictarget: ?Watcher;id: number;subs: Array<Watcher>;constructor(){this.id=uid++this.subs=[]}addSub(sub: Watcher){this.subs.push(sub)}removeSub(sub: Watcher){remove(this.subs,sub)}depend(){if(Dep.target){Dep.target.addDep(this)}}notify(){// stabilize the subscriber list firstconstsubs=this.subs.slice()if(process.env.NODE_ENV!=='production'&&!config.async){// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort((a,b)=>a.id-b.id)}for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}}// The current target watcher being evaluated.// This is globally unique because only one watcher// can be evaluated at a time.Dep.target=nullconsttargetStack=[]functionpushTarget(target?: Watcher){targetStack.push(target)Dep.target=target}functionpopTarget(){targetStack.pop()Dep.target=targetStack[targetStack.length-1]}
Watcher 作为观察者,其主要作用就是接收改变的通知并作出反应(渲染 or 通知给我们)。它的主要构成是这样的:
classWatcher{vm: Component;cb: Function;deps: Array<Dep>;newDeps: Array<Dep>;depIds: SimpleSet;newDepIds: SimpleSet;getter: Function;value: any;constructor(vm: Component,expOrFn: string|Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean){// optionsthis.cb=cbthis.deps=[]this.newDeps=[]this.depIds=newSet()this.newDepIds=newSet()// parse expression for getterif(typeofexpOrFn==='function'){this.getter=expOrFn}else{this.getter=parsePath(expOrFn)}this.value=this.lazy
? undefined
: this.get()}/** * Evaluate the getter, and re-collect dependencies. */get(){pushTarget(this)letvalueconstvm=this.vmvalue=this.getter.call(vm,vm)traverse(value)popTarget()this.cleanupDeps()returnvalue}/** * Subscriber interface. * Will be called when a dependency changes. */update(){/* istanbul ignore else */if(this.lazy){this.dirty=true}elseif(this.sync){this.run()}else{queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */run(){constvalue=this.get()constoldValue=this.valuethis.value=valuethis.cb.call(this.vm,value,oldValue)}/** * Add a dependency to this directive. */addDep(dep: Dep){constid=dep.idif(!this.newDepIds.has(id)){this.newDepIds.add(id)this.newDeps.push(dep)if(!this.depIds.has(id)){dep.addSub(this)}}}/** * Clean up for dependency collection. */cleanupDeps(){leti=this.deps.lengthwhile(i--){constdep=this.deps[i]if(!this.newDepIds.has(dep.id)){dep.removeSub(this)}}lettmp=this.depIdsthis.depIds=this.newDepIdsthis.newDepIds=tmpthis.newDepIds.clear()tmp=this.depsthis.deps=this.newDepsthis.newDeps=tmpthis.newDeps.length=0}}
我保留了一些我认为重要的部分,我们来看看逐个了解一下:
get :首先构造函数其实干的事很少,做了一些简单的赋值后直接调用了 get 方法,get 上面有提到(Dep getter 触发),就是设置 Dep.target 调用 expOrFn,主要目的就是订阅,也就是我们所说的依赖收集。
constqueue=[]letwaiting=falseletflushing=falsefunctionflushSchedulerQueue(){flushing=true;// 冲刷 queueflushing=waiting=false;}functionqueueWatcher(watcher: Watcher){if(!flushing){queue.push(watcher)}else{// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.leti=queue.length-1while(i>index&&queue[i].id>watcher.id){i--}queue.splice(i+1,0,watcher)}// queue the flushif(!waiting){waiting=truenextTick(flushSchedulerQueue)}}
classVNode{tag: string|void;data: VNodeData|void;children: ?Array<VNode>;text: string|void;elm: Node|void;ns: string|void;context: Component|void;// rendered in this component's scopekey: string|number|void;componentOptions: VNodeComponentOptions|void;componentInstance: Component|void;// component instanceparent: VNode|void;// component placeholder node// strictly internalraw: boolean;// contains raw HTML? (server only)isStatic: boolean;// hoisted static nodeisRootInsert: boolean;// necessary for enter transition checkisComment: boolean;// empty comment placeholder?isCloned: boolean;// is a cloned node?isOnce: boolean;// is a v-once node?asyncFactory: Function|void;// async component factory functionasyncMeta: Object|void;isAsyncPlaceholder: boolean;ssrContext: Object|void;fnContext: Component|void;// real context vm for functional nodesfnOptions: ?ComponentOptions;// for SSR cachingdevtoolsMeta: ?Object;// used to store functional render context for devtoolsfnScopeId: ?string;// functional scope id supportconstructor(tag?: string,data?: VNodeData,children?: ?Array<VNode>,text?: string,elm?: Node,context?: Component,componentOptions?: VNodeComponentOptions,asyncFactory?: Function){this.tag=tagthis.data=datathis.children=childrenthis.text=textthis.elm=elmthis.ns=undefinedthis.context=contextthis.fnContext=undefinedthis.fnOptions=undefinedthis.fnScopeId=undefinedthis.key=data&&data.keythis.componentOptions=componentOptionsthis.componentInstance=undefinedthis.parent=undefinedthis.raw=falsethis.isStatic=falsethis.isRootInsert=truethis.isComment=falsethis.isCloned=falsethis.isOnce=falsethis.asyncFactory=asyncFactorythis.asyncMeta=undefinedthis.isAsyncPlaceholder=false}// DEPRECATED: alias for componentInstance for backwards compat./* istanbul ignore next */getchild(): Component|void{returnthis.componentInstance}}
functionpatch(oldVnode,vnode,hydrating,insertedVnodeQueue){if(isUndef(oldVnode))createElm(vnode,insertedVnodeQueue);else{constisRealElement=isDef(oldVnode.nodeType)if(!isRealElement&&sameVnode(oldVnode,vnode)){// patch existing root nodepatchVnode(oldVnode,vnode,insertedVnodeQueue,null,null,removeOnly)}else{if(isRealElement){// mounting to a real element// check if this is server-rendered content and if we can perform// a successful hydration.if(oldVnode.nodeType===1&&oldVnode.hasAttribute(SSR_ATTR)){oldVnode.removeAttribute(SSR_ATTR)hydrating=true}if(isTrue(hydrating)&&hydrate(oldVnode,vnode,insertedVnodeQueue))returnoldVnode// either not server-rendered, or hydration failed.// create an empty node and replace itoldVnode=emptyNodeAt(oldVnode)}// replacing existing elementconstoldElm=oldVnode.elmconstparentElm=nodeOps.parentNode(oldElm)// create new nodecreateElm(vnode,insertedVnodeQueue,parentElm)}}}
我们一步步来,patch 方法接收两个参数 vnode 就是我们新生成的虚拟DOM, 而 oldVnode 是上一个状态的 DOM element 对象,它也可以是初始化时的 el 指向的元素:
functioncreateElm(vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index){// 递归调用 处理 children 时 vnode 指向正确的 child (ownerArray[index])if(isDef(vnode.elm)&&isDef(ownerArray)){// This vnode was used in a previous render!// now it's used as a new node, overwriting its elm would cause// potential patch errors down the road when it's used as an insertion// reference node. Instead, we clone the node on-demand before creating// associated DOM element for it.vnode=ownerArray[index]=cloneVNode(vnode)}// 处理 Componentif(createComponent(vnode,insertedVnodeQueue,parentElm,refElm)){return}constdata=vnode.dataconstchildren=vnode.childrenconsttag=vnode.tagif(tag){vnode.elm=document.createElement(tag)// style 的 scope 相关setScope(vnode)createChildren(vnode,children,insertedVnodeQueue)if(data){invokeCreateHooks(vnode,insertedVnodeQueue)}insert(parentElm,vnode.elm,refElm)}}functioncreateChildren(vnode,children,insertedVnodeQueue){if(Array.isArray(children)){for(leti=0;i<children.length;++i){createElm(children[i],insertedVnodeQueue,vnode.elm,null,true,children,i)}}elseif(isPrimitive(vnode.text)){// 不是数组直接 document.appendChild 到父元素上。nodeOps.appendChild(vnode.elm,nodeOps.createTextNode(String(vnode.text)))}}functioninvokeCreateHooks(){for(leti=0;i<cbs.create.length;++i){cbs.create[i](emptyNode,vnode)}i=vnode.data.hook// Reuse variableif(isDef(i)){if(isDef(i.create))i.create(emptyNode,vnode)if(isDef(i.insert))insertedVnodeQueue.push(vnode)}}
// inline hooks to be invoked on component VNodes during patchconstcomponentVNodeHooks={init(vnode: VNodeWithData,hydrating: boolean): ?boolean{if(vnode.componentInstance&&!vnode.componentInstance._isDestroyed&&vnode.data.keepAlive){// kept-alive components, treat as a patchconstmountedNode: any=vnode// work around flowcomponentVNodeHooks.prepatch(mountedNode,mountedNode)}else{constchild=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)child.$mount(hydrating ? vnode.elm : undefined,hydrating)}},prepatch(){...},insert(){...},destroy(){...}}consthooksToMerge=Object.keys(componentVNodeHooks)functioninstallComponentHooks(data){consthooks=data.hook||(data.hook={})for(leti=0;i<hooksToMerge.length;i++){constkey=hooksToMerge[i]constexisting=hooks[key]consttoMerge=componentVNodeHooks[key]if(existing!==toMerge&&!(existing&&existing._merged)){hooks[key]=existing ? mergeHook(toMerge,existing) : toMerge}}}
functioncreateComponent(vnode,insertedVnodeQueue,parentElm,refElm){leti=vnode.dataif(isDef(i)){constisReactivated=isDef(vnode.componentInstance)&&i.keepAliveif(isDef(i=i.hook)&&isDef(i=i.init)){i(vnode,false/* hydrating */)}// after calling the init hook, if the vnode is a child component// it should've created a child instance and mounted it. the child// component also has set the placeholder vnode's elm.// in that case we can just return the element and be done.if(isDef(vnode.componentInstance)){initComponent(vnode,insertedVnodeQueue)insert(parentElm,vnode.elm,refElm)if(isTrue(isReactivated)){reactivateComponent(vnode,insertedVnodeQueue,parentElm,refElm)}returntrue}}}functioninitComponent(vnode,insertedVnodeQueue){if(isDef(vnode.data.pendingInsert)){insertedVnodeQueue.push.apply(insertedVnodeQueue,vnode.data.pendingInsert)vnode.data.pendingInsert=null}vnode.elm=vnode.componentInstance.$elif(isPatchable(vnode)){invokeCreateHooks(vnode,insertedVnodeQueue)setScope(vnode)}else{// empty component root.// skip all element-related modules except for ref (#3455)registerRef(vnode)// make sure to invoke the insert hookinsertedVnodeQueue.push(vnode)}}
functionhydrate(elm,vnode,insertedVnodeQueue){leticonst{ tag, data, children }=vnodevnode.elm=elmif(isDef(data)){if(isDef(i=data.hook)&&isDef(i=i.init))i(vnode,true/* hydrating */)if(isDef(i=vnode.componentInstance)){// child component. it should have hydrated its own tree.initComponent(vnode,insertedVnodeQueue)returntrue}}if(isDef(tag)){if(isDef(children)){// empty element, allow client to pick up and populate childrenif(!elm.hasChildNodes()){createChildren(vnode,children,insertedVnodeQueue)}else{// v-html and domProps: innerHTMLif(isDef(i=data)&&isDef(i=i.domProps)&&isDef(i=i.innerHTML)){if(i!==elm.innerHTML)returnfalse}else{// iterate and compare children listsletchildrenMatch=trueletchildNode=elm.firstChildfor(leti=0;i<children.length;i++){if(!childNode||!hydrate(childNode,children[i],insertedVnodeQueue,inVPre)){childrenMatch=falsebreak}childNode=childNode.nextSibling}// if childNode is not null, it means the actual childNodes list is// longer than the virtual children list.if(!childrenMatch||childNode)returnfalse}}}if(isDef(data)){letfullInvoke=falsefor(constkeyindata){if(!isRenderedModule(key)){fullInvoke=trueinvokeCreateHooks(vnode,insertedVnodeQueue)break}}if(!fullInvoke&&data['class']){// ensure collecting deps for deep class bindings for future updatestraverse(data['class'])}}}elseif(elm.data!==vnode.text){elm.data=vnode.text}returntrue}
该方法主要判断两个 vnode 的 key、tag、isComment 这些字段值是否一样(可以都为 undefined),data 字段是否都 存在/不存在,如果 tag 为 input,其 type 也要相等。
只要上述情况满足就会被认定为 sameNode,它不管你 data 是否相等(哪怕 data 中事件、样式等等都不一样),也不管你 children 如何。sameNode 不是指同一个元素,而是相似,也就是可复用。之后就会对两个相似的 vnode 调用 patchVnode 方法。
functionpatchVnode(oldVnode,vnode){// 如果指向同一个对象,直接 return 就好了if(oldVnode===vnode)returnconstelm=vnode.elm=oldVnode.elm// reuse element for static trees.// note we only do this if the vnode is cloned -// if the new node is not cloned it means the render functions have been// reset by the hot-reload-api and we need to do a proper re-render.if(isTrue(vnode.isStatic)&&isTrue(oldVnode.isStatic)&&vnode.key===oldVnode.key&&(isTrue(vnode.isCloned)||isTrue(vnode.isOnce))){vnode.componentInstance=oldVnode.componentInstancereturn}leticonstdata=vnode.data// 处理 componentsif(isDef(data)&&isDef(i=data.hook)&&isDef(i=i.prepatch)){i(oldVnode,vnode)}constoldCh=oldVnode.childrenconstch=vnode.children// 处理dataif(isDef(data)&&isPatchable(vnode)){for(i=0;i<cbs.update.length;++i)cbs.update[i](oldVnode,vnode)if(isDef(i=data.hook)&&isDef(i=i.update))i(oldVnode,vnode)}// 处理 childrenif(isUndef(vnode.text)){if(isDef(oldCh)&&isDef(ch)){if(oldCh!==ch)updateChildren(elm,oldCh,ch,insertedVnodeQueue,removeOnly)}elseif(isDef(ch)){if(isDef(oldVnode.text))nodeOps.setTextContent(elm,'')addVnodes(elm,null,ch,0,ch.length-1,insertedVnodeQueue)}elseif(isDef(oldCh)){removeVnodes(oldCh,0,oldCh.length-1)}elseif(isDef(oldVnode.text)){nodeOps.setTextContent(elm,'')}}elseif(oldVnode.text!==vnode.text){nodeOps.setTextContent(elm,vnode.text)}}
接着,拿到 data (const data = vnode.data), 如果当前节点是组件节点,调用组件的 prepatch hook,该 hook 的作用是处理组件实例,更新属性,并需要的话调用组件更新方法 $forceUpdate 也就是 _update(_render())。接着处理 data 只要 data 存在(isPatchable 主要就是判断 vnode/组件实例有没有 tag 字段)即调用 update hooks 处理,大部分和 create hooks 处理一样,处理属性、样式等,不过这里会对新老 data 中的各项进行相等判断,只有不同的情况才会进行 dom 属性的操作,这里都是些基本类型值的判断,比较简单。
最后是处理 children, 拿到新老 vnode 的 children,如果 vnode 有 text 字段,且与 oldVnode.text 不同则直接将 当前 dom element 的 textContext 设为 vnode.text (nodeOps.setTextContent(elm, vnode.text))。 text 的优先级要大于 children。抛去 text 的特殊情况则有三种情况,oldCh 和 ch 同时存在,则调用 updateChildren 进一步处理,如果 ch 存在 oldCh 不存在,则调用 addVnodes 处理 children 并直接插入当前dom 元素(就是循环对子 vnode 调用 createElm),如果 oldCh 存在而 ch 不存在则调用 removeVnodes 将其子节点从元素中删除。
functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue){letoldStartIdx=0letnewStartIdx=0letoldEndIdx=oldCh.length-1letoldStartVnode=oldCh[0]letoldEndVnode=oldCh[oldEndIdx]letnewEndIdx=newCh.length-1letnewStartVnode=newCh[0]letnewEndVnode=newCh[newEndIdx]letoldKeyToIdx,idxInOld,vnodeToMove,refElmwhile(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(isUndef(oldStartVnode)){oldStartVnode=oldCh[++oldStartIdx]// Vnode has been moved left}elseif(isUndef(oldEndVnode)){oldEndVnode=oldCh[--oldEndIdx]}elseif(sameVnode(oldStartVnode,newStartVnode)){patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx)oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){// Vnode moved rightpatchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx)canMove&&nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldEndVnode,newStartVnode)){// Vnode moved leftpatchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)canMove&&nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)oldEndVnode=oldCh[--oldEndIdx]newStartVnode=newCh[++newStartIdx]}else{if(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)idxInOld=isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx)if(isUndef(idxInOld)){// New elementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}else{vnodeToMove=oldCh[idxInOld]if(sameVnode(vnodeToMove,newStartVnode)){patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)oldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)}else{// same key but different element. treat as new elementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}}newStartVnode=newCh[++newStartIdx]}}if(oldStartIdx>oldEndIdx){refElm=isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elmaddVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx,insertedVnodeQueue)}elseif(newStartIdx>newEndIdx){removeVnodes(oldCh,oldStartIdx,oldEndIdx)}}
vue 源码解读
为了更深入了解 Vuejs 我决定,边读源码边自己实现一些简单的功能。
数据绑定
为什么从数据绑定开始,一是因为它重要,而是因为它比较独立,不依赖框架,可以单独抽出来讲。
Observer
总所周知,Vuejs 是通过 数据劫持(
Object.defineProperty()
) 配合观察者模式来实现的数据绑定,所以首先来看看 Vuejs 实现的观察者:Observer
的功能比较简单,就是将一个对象包装成可观察对象(Observable),将自己(this)绑定在对象的__ob__
字段上,劫持每一个字段。如果是数组,则会进行处理,重写数组方法,并将数组内每个对象继续包装成可观察对象。这里简单插一下 Vuejs 是如何重写数组的:
其实也比较简单,类似继承的方式搞一个新的
prototype
重写数组方法,依旧调用原方法的同时增加通知ob.dep.notify()
,最后重改原型链target.__proto__ = arrayMethods
接下来我们来看看
defineReactive
方法中 Vuejs 如何处理的数据劫持:比较好理解,改写了属性的 getter/setter 方法(同时兼容了本身就有 getter/setter 方法的情况)在 getter/setter 时进行操作。简单的说
Observer
就是改造目标对象,劫持属性。这里要注意的是在方法
defineReactive
中new Dep
形成了闭包,在getter
时才调用dep.depend()
,setter
时才通知dep.notify()
,这个之后讲接下来需要一个充当主体的角色,将值多路推送给观察者(Observer),Vuejs 中就是
Dep
了:Dep
Dep 就是个内容发布者,它依托在一个可观察对象内(Observable)可以被多个观察者订阅(Watcher),多路推送消息。
结构也比较简单,
subs
是用来存放所有的观察者(Watcher),addSub
和removeSub
管理subs
,notify
用于通知所有的观察者(调用所有subs
的update
方法)。那么target
相关的是什么。首先我们知道的
target
装的就是观察者(Watcher),static target
作为全局就有一点单例的味道,啥时候会用到它呢,我们简单梳理一下初始化的过程:$mount
方法,这个时候会new Watcher()
这个
Watcher
接收一个render
函数并在初始化调用(这个Watcher
的作用是要渲染组件的 ,具体的下面讲),调用render
函数时会获取对应的值,这个时候触发了我们重写的getter
,触发的过程类似这样的:那么结合上面的
Observer
,每个子属性(defineReactive
处理过)都有一个dep
,这个子属性本身可能也是个对象被处理成可观察对象(Observable), 它也有个dep
,那么子属性的子属性..(套娃)这些dep
都被同一个Watcher
订阅了(上述的那个)。如果这个时候遇到嵌套的情况,比如
Router
或Vue.extend
这时候上面的流程还会再来一次,这时候会生成一个新的 观察者(Watcher2)来观察新的数据:类似于这样,这个时候
targetStack
就派上用场了,它保留了之前的Watcher
,因为是单例的,新的dep
会被Watcher2
给订阅,完事移除Watcher2
继续完成之前的watcher
的订阅,有一点洋葱圈模型的味道。Watcher
Watcher
作为观察者,其主要作用就是接收改变的通知并作出反应(渲染 or 通知给我们)。它的主要构成是这样的:我保留了一些我认为重要的部分,我们来看看逐个了解一下:
get
:首先构造函数其实干的事很少,做了一些简单的赋值后直接调用了get
方法,get
上面有提到(Dep getter 触发),就是设置Dep.target
调用expOrFn
,主要目的就是订阅,也就是我们所说的依赖收集。expOrFn
:它作为参数传入,其实就是一个getter
触发器,从名字可以看出来它可能是一个表达式,也可能是一个方法。这里就要先提一下哪些情况会新建一个Watcher
:初始化(
$mount
):上面Dep 初始化有提到,这个时候expOrFn
是一个 渲染函数() => vm._update(vm._render(), hydrating)
解析模版渲染页面的同时触发data的getter
(具体后面讲),这个时候生成的Watcher
意义重大,控制着 dom 的更新。watch
、$watch
:这个时候expOrFn
就是 key ,他会被parsePath
处理成方法调用(parsePath
可以处理a.b.c
这种形势的 key), 还会传入一个cb
方法,在接收更新之后调用。computed
:稍稍有点不一样,expOrFn
就是我们定义的方法,方法内所有被触发getter
的属性都会被当前的Watcher
订阅,Watcher
接收到更新消息后还是会调用expOrFn
然后将返回值设为value
(key 会被挂在 vm 上,取的就是这个value
)traverse
: 深度遍历对象,把所有getter
都触发一遍(我不干啥,我就看看)。addDep
:Dep.depend
就是调用此方法,用于把当前Watcher
存入dep
实例(来来回回有点绕)cleanupDeps
:以及deps
、newDeps
、depIds
、newDepIds
这些个玩意,你只要理解他们是防止重复订阅的就可以了。update
、run
:通知更新(dep.notify
)后调用的就是update
方法,如果是同步的,调用run
方法,run
干的事也很简单就是再调一次get
要么重新渲染,要么cb
给我们(此时会重新处理一次订阅)然后更新watch
的value
。如果是异步的 调用queueWatcher
。scheduler
调度器不是很重要,简单提一下(需要了解
nextTick
、event loop
),可跳过。首先我们知道
nextTick
会把flushSchedulerQueue
放进微任务队列(microtask queue
)。那么waiting
状态就比较好理解,他保证了microtask queue
中只有一个flushSchedulerQueue
任务。而flushing
为 true 的情况发生在flushSchedulerQueue
正在执行时,根据watcher.id
往queue
中插任务如果没有 ‘过号’ 那么新插进来的也会被执行掉,如果 ‘过号’ 了那么保留在queue
等待下一次冲刷。整个过程模型和微任务的流程模型简直一毛一样。
ok, 至此数据绑定相关的东西(也就是
core/observer
下的东西)我们都过了一遍了。现在我们把他们串联起来,来看看Vue
的 ‘心路历程’大概就这样,里面很多细枝末节就不提了,这些方法名都是
Vue
里面存在的,一搜就能找到,这里提一下proxy
:此
proxy
不是我们想的es6 proxy
他的作用是将$data
的属性代理到vm
实例上,方便我们this.xxx
调用。Virtual DOM
ok 我们了解完了
Vuejs
数据绑定的过程,接下来我们看看编译渲染 DOM 的过程。之前提到,在实例化方法_init
的最后会调用$mount
来解析模版、渲染,现在我们来仔细看看。Compiler
首先是编译,
$mount
中调用compileToFunctions
将<template>
文本转化成一个render
函数,我们来看看compileToFunctions
都做了什么:提取了其中代码,把生成方法和 option 的处理部分删掉了,完整的可以到
src/compile
中去看,compileToFunctions
方法由createCompiler
方法生成,不过主要的内容在baseCompile
方法中,我们主要来看看这个方法。里面就两步,
parse
,generate
,我们依次来看。parse
parse
方法的代码都在compile/parse
中,其主要功能就是将template
编译成 AST。我们先了解以下语法树的基本结构:因为是将
html
代码转换成 AST 所以结构简单多了,主要是标签名tag
(<tag>
) 和 属性attrsMap
(key: value)以及层级关系children
,parent
。接下来了解下
html-parse
,他是转换 AST 的核心代码,其本身来自于一个开源代码的扩展:simplehtmlparser。代码就不贴了,自行了解下。其实就是正则判断,判断
template
文本的开头是什么,对应有这么几种情况:<!--
开头,也就是注释,这种情况再找到结尾-->
直接删了。<!DOCTYPE
开头,文档类型,和上面一样,删了。<name
开头,开始标签(start tag
),执行parseStartTag
</name
开头,结束标签(end tag
),执行parseEndTag
那么我们回归
Vuejs
本身,其实主要逻辑差不多,先简单看看流程模型:代码精简了很多,主要的解析流程就是如此,首先是
parseHTML
里面是如何解析的,我们结合上面了解的一起看:首先是
),最后得到一个
parseStartTag
方法,其通过正则查找类似<tagName
的文本,记录其位置并通过子表达式得到tagName
,advance
方法用于将匹配到的值剔除,while
循环中不断通过正则查找并记录attribute
然后删除,当匹配到startTagClose
时结束(>
或/>
),记录子表达式(
/
或match
对象,大概长这样:得到
match
对象后调用startHandler
进一步处理,现在的attrs
还是字符串(不过已经把属性名和值都取到了,这个正则还是给力的),先处理成Map
,然后才是创建 AST,然后经过一些处理(v-for
、v-if
之类的属性),最后放入stack
中。这里说明以下,
stack
和currentParent
在父级作用域,也就是parse
方法中,用来处理父子关系。而unary
是用来判断element
是不是非特殊标签 (<br/>
这种),并且是不是/>
结尾。如果是就传入stack
继续处理,直到遇到endTag
:end
的主要功能就是取出stack
栈顶,和其前面一项确认父子关系,把currentParent
设置为取出后的栈顶。简单的说:
startTag
就创建ASTElement
。如果是
/>
就和 stact 最后一项(currentParent
)确认父子关系不是就将
element
推入stack
endTag
就取出栈尾(element
),currentParent
往前移(新的栈尾),element
和currentParent
确认父子关系如此不断的循环。
这些就是
parse
的主要功能了,当然还有其他的,比如text-parse
主要负责文本的处理(主要处理双括号的语法{{expression}}
,转换成_s(expression)
),而filter-parse
负责管道的处理 ({{value | filter}}
=>_s(_f("filter")(value))
),注意这里都是处理成字符串哦,至于_s
、_f
是什么,我们接下来讲。最后看一下例子,之后我们也会按照这个例子走下去:examples
parse
方法得到:可以看到,除了解析
标签
,属性
,父子关系
之外,遇到vue
特性(@click
啥的)会有特殊的处理:{{expression}}
会被解析成_s(expression)
,filter
是_f(expression)
,@name="expression"
则会在ASTElement.event
上多加一个字段name: expression
,v-if
则会在ASTElement
上多加标记(ifProcessed: expression
) 等等,就不一一列了。codegen
parse
方法将模版解析成ast
,那么接下来generate
方法将会把ast
转化成一个render
函数,我们来简单看看:截取了其中一小部分,主要就是通过
genElement
生成类似_c(tagName, Data, [children])
这样的字符串,之后会被当作表达式调用,_c
方法其实就是render
函数中提供的createElement
方法。接触过
render
函数的话应该很好理解,也就是说不管怎么样最终都会回到render
函数,如果你直接用render
函数还能提高性能(省去了parse
的过程)。那么我们看看官方文档。createElement
有三个参数,第一个参数是html
的标签名字,直接通过element.tag
得到(parse
解析取得)。第二个参数是一个数据对象,其值通过genData
方法处理(普通属性、dom属性、style、class、事件等等),第三个参数是子虚拟节点的集合,其也由createElement
构建而成,在genElement
中循环处理element.children
。最终
generate
会将ast
包装成类似with(this){return _c(...)}
的字符串,还是上面的例子,最终会输出这样:最后我们通过
Function
将其转换成一个方法,并绑定在vm.options
上:还是之前的例子,其结果大概是这样:
注意了,之所以这么做,以及使用
with
语法,主要原因是Compiler
并未对表达式进行解析处理,而是完整保留了表达式,通过with
语句保证真正执行render
函数时能够正确执行表达式,并从vm
实例中取到正确的值。ok,至此
Compiler
的任务完成。VNode
之前提到在生成
render
函数之后new Watcher
时会传入一个渲染函数:抛去各种判断,
_render
方法其实就是调用了render.call(vm)
得到vnode
,而render
中调用各种_x()
方法处理各种情况,这些代码都在
core/instance/render-helpers
中,有兴趣可以看下,其中最主要的_c
也就是createElement
,代码位于core/vdom/create-element
中。其主要功能也很简单,说白了就是通过传入的tagName
、Data
和[children]
生成vnode
:这里说一下,如果遇到不认识的
tag
会判断$options.components
中有没有相应name
的对象,返回相应的对象Ctor
,如果有,会执行createComponent
:其实就是执行
Vue.extend(Ctor)
生成一个带有componentOptions
的vnode
。 说到底最终都是生成vnode
那么我们来了解以下它的结构:vnode
构造函数主要接收的就是createElemetn
的参数,其本质也是生成一个结构对象,不过和ast
不同的是,ast
保留了表达式,而vnode
是将表达式执行完的结果,他是静态的,也就是说与之对应的是一个确定的渲染结果。依旧是那个例子:至此,我们已经通过
render
函数和vm
实例的数据生成了vnode
,要注意的是,render
函数是在初始化生成的,是不会变的,但是我们的数据层是会变的,每次调用_render
都有可能生成不同的vnode
,接着就可以通过vnode
去生成真正的dom
节点了。patch
生成
vnode
之后就是将其传入_update
,其内部本质就是调用patch
方法,代码在core/vdom/patch
,我们来大致看一下:我们一步步来,
patch
方法接收两个参数vnode
就是我们新生成的虚拟DOM, 而oldVnode
是上一个状态的DOM element
对象,它也可以是初始化时的el
指向的元素:比如这个时候
oldVnode
就是<div id="app"></div>
。所以哪怕是初始化的时候oldVnode
也是存在的。接着看方法体,有下面几种情况:
首先是判断
oldVnode
是否存在,比如操作对象是Component
时他就是空的,这个时候要创建一个新的根元素createElm(vnode)
。如果
oldVnode
不为空,先拿到oldVnode.nodeType
。注意,oldVnode
他可能是真实的DOM element
对象,也有可能是个虚拟DOMvnode
,这里的 nodeType 是DOM element
的属性,当它值为 1 时代表它是个元素 (element
),我们通过nodeType
来判断oldVnode
是虚拟DOM 还是 真实DOM,也就是isRealElement
。当
oldVnode
是虚拟DOM,且新旧虚拟DOM相同时(!isRealElement && sameVnode(oldVnode, vnode)
)调用patchVnode(oldVnode, vnode)
,一般来讲第一次渲染时oldVnode
是真实DOM,更新节点时oldVnode
是虚拟DOM。当
oldVnode
是真实DOM时,说明是第一次调用,则需要判断一个特殊情况:hydrating
是否为true
,hydrating
是一个是否保持的标记,他的作用是是否沿用oldVnode
。比如说服务端渲染的情况,我们第一次patch
时oldVnode
已经是渲染好了的真实DOM了,此时我们要做的就是存一下真实DOM(oldVnode
)即可。实际调用hydrate
方法。剩下的就是
oldVnode
是真实DOM、hydrating === false
和oldVnode
是虚拟DOM、!sameVnode(oldVnode, vnode)
两种情况,这两种情况直接用新的替换老的 (
createElm(vnode)
)。ok,各种情况我们总结完了,我们来一一细看。
createElm
当处于上述情况 1 和 5 时,会调用
createElm
方法,该方法的作用是将虚拟DOM 转换为真实DOM 并替换当前的真实DOM(oldVnode
)createComponent
: 处理当前是组件的情况,这个我们稍后再讲。document.createElement(tag)
生成一个元素,然后赋值到vnode.elm
,此时元素只是一个空壳,什么都没有。setScope
方法是用来处理样式的scope
属性。createChildren
用来递归调用createElm
处理children
,createElm
的参数parentElm
指的是父元素,ownerArray[index]
为当前子vnode
。invokeCreateHooks
是用来方法处理data
的,其中cbs
存放这一组hooks
:['create', 'activate', 'update', 'remove', 'destroy']
每个hooks
中存放这一组方法,这些方法用与在不同周期处理data
数据,其代码在core/vdom/modules
和platforms/xxx/runtime/modules
中,这里就不展开了,主要是处理class
、attrs
、events
、props
等属性,并合并到vnode.elm
上。而vnode.data.hook
和component
相关,稍后讲。insert
方法就是将全部处理好的元素子元素vnode.elm
放到父元素的内部document.appendChild(parent, vnode.elm)
。注意这里都是真实的 DOM 操作,如果处理的是根节点,操作的parentElm
是页面上的真实DOM。比如情况 5:这种情况就直接将当前元素整个替换。
createComponent
之前有提到,在
_render
时会处理Component
,当时只是处理组件对象,将其继承Vue
实例的属性,生成vnode
,其实此时还会在组件对象的data
上绑定一些hooks
:这些
hooks
封装了组件的添加和销毁方法,我们就只看init
,这里的Vnode
是组件vnode,其对象本身在vnode.componentOptions.Ctor
中,是继承得到的构造方法,这里createComponentInstanceForVnode
实际就是new Ctor()
得到实例,这里会调用_init
方法不过因为组件没有$el
字段,所以不会调用$moumt
方法,所以init hook
手动调用了$moumt
。也就是init hook
封装了组件的初始化操作,并将实例放到了vnode.componentInstance
中。ok,了解了这个 我们再来看
patch
中的createComponent
:createComponent
方法就是直接调用component
的create hook
,生成实例存在vnode.componentInstance
中,如果成功,就调用initComponent
,并插入父元素中insert(parentElm, vnode.elm, refElm)
。值得注意的是
create hook
只是负责组件内部的初始化,而组件使用的地方依旧需要处理当作一个元素处理,即initComponent
中除了将生成的组件实例的真实dom绑定在vnode.elm
,依旧调用了invokeCreateHooks
来处理打他,比如需要传入的dom props
。还有,想象以下,有组件时
patch
方法会执行两次,页面执行一次,组件内部执行一次(init hook$mount
),而组件内部执行patch
是没有oldVnode
的,也就是说组件本身生成的element
仅仅是存在组件的vnode.$el
中,最后通过vnode.elm = vnode.componentInstance.$el
的方法拿到,然后insert
到父元素中去。hydrate
调用
$mount
时会传入一个标记hydrating
,并一直拓传到patch
方法,当oldVnode
是真实dom时,它决定是否沿用真实dom。一个很好的例子就是SSR
服务端渲染,初次调用$mount
时页面已经渲染好了,oldVnode
就是渲染好的元素节点,此时Vue
就会调用hydrate
方法hydrate
会处理vnode
并返回一个boolean
,如果为true,patch
就直接返回oldVnode
,我们来看看里面做了哪些处理:首先是组件,我们之前讲过,真正的组件初始化在
patch
方法中,现在虽然我们的页面已经渲染完了,但vnode
中并没有组件的实例componentInstance
所以我们需要生成它,调用data.hook.init
方法生成,然后调用initComponent
绑定 elm,注意这里调用时传入了hydrating = true
(i(vnode, true)
) 那么init hook
内部调用$mount
时也会带入hydrating
, 保证组件只初始化,不操作dom。然后是在
render
函数有children
的情况下如果页面元素没有子元素(!elm.hasChildNodes()
),这种情况调用createChildren
,循环添加子元素到当前元素中 (createElm
)。还有个特殊情况就是innerHTML
,如果当前元素的innerHTML
和vnode.data.domProps.innerHTML
就会直接返回 false,执行之前的替换流程。再然后是
isRenderedModule(key)
,我们知道invokeCreateHooks
会处理data
将各种标签属性添加到元素中,还会处理props
和event
,但是当页面已经渲染完成时,有些属性不需要再处理了,所以这个isRenderedModule
维护了一个数组attrs,class,staticClass,staticStyle,key
如果data
中没有除这些之外的属性,那么就跳过invokeCreateHooks
毕竟这是多余的操作,但是遇到其他的还是要正常处理,比如event
,服务端只会传html 过来,并不会自己处理事件。最终处理完之后返回
true
,将oldVnode
绑定在vnode.elm
上,patch
直接返回vnode
。这样这个vnode
就会绑定在vm.$el
上。保证下次更新时oldVnode
拿到的是这个我们处理好的vnode
。patchVnode
之前讲的都是替换的情况,最后就是更新的情况,当
oldVnode
不是真实dom 且节点没有替换时,调用patchVnode
。我们先来看看
sameVnode
方法:该方法主要判断两个
vnode
的key
、tag
、isComment
这些字段值是否一样(可以都为 undefined),data
字段是否都 存在/不存在,如果tag
为input
,其type
也要相等。只要上述情况满足就会被认定为
sameNode
,它不管你data
是否相等(哪怕data
中事件、样式等等都不一样),也不管你children
如何。sameNode
不是指同一个元素,而是相似,也就是可复用。之后就会对两个相似的vnode
调用patchVnode
方法。一步一步来,首先拿到
oldVnode.elm
也就是真实的dom元素,而且赋值给vnode.elm
,紧接着是静态节点优化,这里提一下,在ast
转render
函数之间,有一步静态节点优化optimize(ast)
,前面省略了,如果当前节点没有任何动态的元素,就会添加一个静态标记isStatic
,在此时,遇到这个标记就直接 return,复用之间的元素(如果是组件就会复用组件实例vnode.componentInstance = oldVnode.componentInstance
)。接着,拿到
data
(const data = vnode.data
), 如果当前节点是组件节点,调用组件的prepatch hook
,该hook
的作用是处理组件实例,更新属性,并需要的话调用组件更新方法$forceUpdate
也就是_update(_render())
。接着处理data
只要data
存在(isPatchable
主要就是判断 vnode/组件实例有没有tag
字段)即调用update hooks
处理,大部分和create hooks
处理一样,处理属性、样式等,不过这里会对新老data
中的各项进行相等判断,只有不同的情况才会进行 dom 属性的操作,这里都是些基本类型值的判断,比较简单。最后是处理
children
, 拿到新老vnode
的children
,如果vnode
有text
字段,且与oldVnode.text
不同则直接将 当前dom element
的textContext
设为vnode.text
(nodeOps.setTextContent(elm, vnode.text)
)。text
的优先级要大于children
。抛去text
的特殊情况则有三种情况,oldCh
和ch
同时存在,则调用updateChildren
进一步处理,如果ch
存在oldCh
不存在,则调用addVnodes
处理children
并直接插入当前dom 元素(就是循环对子vnode
调用createElm
),如果oldCh
存在而ch
不存在则调用removeVnodes
将其子节点从元素中删除。简单的说,只要新老
vnode
相似,抛去特殊情况,就会拿到旧元素复用,在旧元素上处理data
,也就是说sameNode
判定很简单,而复用也只是简单的复用了dom 元素的壳,其属性还是要处理过,而且只有相似的节点才回去深入比较其children
。接着我们看下如何处理children
:updateChildren
与
patchVnode
不同的是,updateChildren
对比的是两个新/老vnode
数组,老数组中任意元素都有可能被新元素复用:可以看到,
updateChildren
利用了前后指针法来遍历两个数组oldCh
和newCh
,在最一开始分别记录了oldStartIdx
、newStartIdx
新/老数组起始指针位置oldEndIdx
、newEndIdx
新/老数组结束指针位置oldStartVnode
、newStartVnode
新老数组起始位置的vnode
oldEndVnode
、newEndVnode
新老数组结束位置的vnode
就像这样:分别对应四个
vnode
和指针ok,有了这些值就可以开始我们的循环操作了,只要
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
新老数组的前后指针之间还有元素存在就一直执行如下判断,有那么几种情况:oldStartVnode
不存在,那么老数组的前指针后移,oldStartVnode
也指向移动后的vnode
:oldStartVnode = oldCh[++oldStartIdx]
oldEndVnode
不存在,那么老数组的后指针前移,oldStartVnode
也指向移动后的vnode
:oldStartVnode = oldCh[++oldStartIdx]
sameVnode(oldStartVnode, newStartVnode)
也就是新老数组的起始vnode
相似,如此直接调用patchVnode
,复用oldVnode
中存的 dom element,在该元素上直接处理newVnode
。完事后将新老前指针后移,同时新老vnode
也指向移动后的:sameVnode(oldEndVnode, newEndVnode)
类似的,这个情况是新老数组的后指针vnode
相似,调用patchVnode
,复用oldVnode
中存的 dom element 并处理。完事后将新老后指针前移,同时新老vnode
也指向移动后的:sameVnode(oldStartVnode, newEndVnode)
也就是‘老前’ 和 ‘新后’vnode
相似,这种情况不仅要进行patchVnode
处理,还需要将真实dom元素移动到正确位置:parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling
也就是将oldStartVnode.elm
放在oldEndVnode.elm
的后面 (注意不是最后!已经处理好的元素不动,放在还没处理的元素列表的最后)。完事后将oldStartIdx
右移,newEndIdx
左移。此时真实元素会这样排列:
sameVnode(oldEndVnode, newStartVnode)
类似的,这种情况是 ‘新前’ 和 ‘老后’ 相似,同样也是patchVnode
,然后将当前元素oldEndVnode.elm
放在未处理元素oldStartVnode.elm
的最前面parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
。完事将oldEndIdx
左移,newStartIdx
右移。此时真实元素会这样排列:
key
值进行搜索了。首先通过createKeyToOldIdx
方法拿到oldCh
中前后指针之间的元素的 key-index 的 map (oldKeyToIdx
)。可以看到,
map
中收集了剩下元素中所有的key
如果没有就用index
当key
。这里的key
就是我们编写模版时会指定的key
属性(<div key="customerKey">something</div>
),也就是我们自己指定的复用。接着,拿着
newStartVnode
的key
去 map 中找对应的key
,如果newStartVnode.key
为undefinend
,则调用findIdxInOld
方法,也就是在oldch
的剩余vnode
中一项一项的比对 (sameVnode
方法) 直到找到相似的vnode
,得到其指针idxInOld
。如果上述
idxInOld
存在,也就是找到了对应的oldVnode
也要进行相似比较sameVnode()
,因为哪怕key
相同元素类型不同也无法复用,如果不相似就通过createElm
方法替换掉老的元素,如果相似就继续调用patchVnode
处理,同时将该元素oldVnode.elm
插入到oldStartVnode.elm
之前,并且将oldch
中该下标的元素清空oldCh[idxInOld] = undefined
(也就是第1、2种情况)。如果上述
idxInOld
不存在那么直接createElm
方法替换老元素,最后newStartIdx
右移,继续下一个判断。关于 diff
从上面的
patchVnode
和updateChildren
可以看出,vue
的 diff 只对同层级的进行,根节点只对根节点比较,某个节点的子节点只和当前子节点比较,如果父节点不相似则直接替换,根本不用去考虑其子节点,这也是为什么说 diff 大大减少了时间复杂度。那么为什么说 diff 从$O(n^3)$ 优化到了 $O(n)$ 呢,我们要知道这个 $O(n^3)$ 是怎么来的的:首先 dom 节点可以看作一个树的结构,如果将一颗树转换为另一颗树,那么计算其最小编辑距离 (Tree edit distance) 的复杂度大约是 $O(n^3)$ ,这是目前最好的算法,简单了解即可。然而
Vue
并不是对比所有节点,从上面可以看出,它只对比同一层级的且同一父元素的元素:也就是说无论树结构多复杂,都只和节点数量有关系,而递归遍历一遍树的复杂度为$O(n)$ ,也就是说正常情况下前后节点都是 n 时 diff 的复杂度就是 $O(n)$ ,特殊一点的比如上述情况 5个子节点顺序全打乱了,那么其最多也就对比 $!(n-1)$ 次而已(同一层级),假如一个$O(!n)$ 。如果但凡有一个节点不同了,那么该就直接替换了,其子节点也就不比较了,你要是根节点就变了直接 diff 一次就结束了,所以说这种方式就将复杂度降为了 $O(n)$ 。
vnode
的顺序全打乱了 (同一层级) 其复杂度也远远小于所以说为什么需要 diff +
vnode
,如果没有,那么每次更新都会将整个 dom 树干掉,然后插入新的,这样每次都要 dom 操作代价太大,这这个 diff 算法又保证了每次 diff js层不会开销太大。把每次更新的消耗都控制在可接受范围内,这就是 virtual dom + diff 的初衷了。最后
ok, 到这里算是把从初始化到渲染整个过了一遍,主要还是看了遍 数据绑定 和 虚拟dom 相关的代码,其实衍生开来差不多七七八八代码都看的差不多了,和当初自己认为的差别还是蛮大的,果然还是要看一遍源码,接下来会尝试这自己实现一下简单的 mvvm 模式。
The text was updated successfully, but these errors were encountered: