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

Vue.js源码(一):HelloWorld的背后

时间:2023-03-12 20:29:02 科技观察

以下代码会在页面输出HelloWorld,但是这个newVue()和页面渲染之间发生了什么。本文希望通过最简单的例子来了解Vue源码流程。这里分析的源码版本为Vue.version='1.0.20'{{message}}

varvm=newVue({el:'#mountNode',data:function(){返回{消息:'HelloWorld'};}});本文将解决几个问题:在newVue()的过程中,内部有哪些步骤,如何收集依赖,如何计算表达式,如何在DOM中体现表达式的值简单来说,以上过程为如下:观察:把{message:'HelloWorld'}变成一个反应式编译:compileTextNode"{{message}}",解析出指令(directive=v-text)和表达式(expression=message),创建一个片段(newTextNode)准备替换链接:实例化指令,将创建的片段与指令链接,替换DOM上的片段bind:通过指令对应的watcher获取依赖(消息)的值(“HelloWorld”),v-text更新值到fragment上的详细过程,再往下看。构造函数文件路径:src/instance/vue.jsfunctionVue(options){this._init(options)}Initialization这里我们只分析最关键的步骤来理解例子。文件路径:src/instance/internal/init.jsVue.prototype._init=function(options){...//mergeoptions.options=this.$options=mergeOptions(this.constructor.options,options,this)...//initializeddataobservationandscopeinheritance.this._initState()...//如果`el`option是通过的,startcompilation.if(options.el){this.$mount(options.el)}}mergeoptionsmergeOptions()定义在src/util在/options.js文件中,这里主要定义了options中各种属性的合并,比如:props、methods、computed、watch等。此外,这里定义了每个属性合并的默认算法(策略)。可以配置这些策略。请参阅自定义选项合并策略。在本文的例子中,主要是数据选项的合并。合并后,放到$options.data里,基本等价于:我们定义的options中的HelloWorld'}}//数据函数在绑定vm实例后执行,执行结果为:{message:'HelloWorld'}varinstanceData=childVal.call(vm)//对象间合并,类似$.extend,结果一定是:{message:'HelloWorld''}returnmergeData(instanceData,parentVal)}initdata_initData()发生在_initState()中,主要做了两件事:代理数据中的属性观察数据文件路径:src/instance/internal/state.jsVue.prototype。_initState=function(){this._initProps()this._initMeta()this._initMethods()this._initData()//这里this._initComputed()}属性代理(proxy)将data的结果赋值给内部属性:文件路径:src/instance/internal/state.jsvardataFn=this.$options.data//我们上面得到的mergedInstanceDataFn函数vardata=this._data=dataFn?dataFn():{}propertyinproxy(proxy)datato_data,makingvm.message===vm._data.message:文件路径:src/instance/internal/state.js/***Proxyaproperty,sothat*vm.prop===vm._data.prop*/Vue.prototype._proxy=function(key){if(!isReserved(key)){varself=thisObject.defineProperty(self,key,{configurable:true,enumerable:true,get:functionproxyGetter(){returnself._data[key]},set:functionproxySetter(val){self._data[key]=val}})}}observe这里是我们的第一个重点,observeprocess在_initData()***中,调用observe(data,this)观察数据。在helloworld例子中,observe()函数主要是为{message:'HelloWorld'}创建一个Observer对象。文件路径:src/observer/index.jsvarob=newObserver(value)//value=data={message:'HelloWorld'}在observe()函数中,也做了一些判断是否观察的条件。这些条件是:NoneObserved(被观察的对象会加上__ob__属性)只能是普通对象(toString.call(ob)==="[objectObject]")或者数组不能是Vue实例(obj._isVue!==true)Theobjectisextensible(Object.isExtensible(obj)===true)ReactivityinDepthofObserver官网上有这样一句话:当你将一个普通的JavaScript对象作为它的数据选项传递给一个Vue实例时,Vue.js将遍历其所有属性并将它们转换为getter/setter。getter/setter对用户是不可见的,但在底层,它们使Vue.js能够在属性被访问或修改时执行依赖跟踪和更改通知Observer就是做那些事情,让data成为“发布者”,watcher成为订阅者,订阅数据的变化。例子中,创建观察者的过程是:newObserver({message:'HelloWorld'})实例化一个Dep对象,用于收集walk(Observer.prototype.walk())数据依赖的各个属性,这里只有message将属性变为reactive(Observer.protoype.convert())convert()调用defineReactive(),在data的message属性中添加reactiveGetter和reactiveSetter文件路径:src/observer/index.jsexportfunctiondefineReactive(obj,key,value){...Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:functionreactiveGetter(){...if(Dep.target){dep.depend()//这里是集合依赖...}returnvalue},set:functionreactiveSetter(newVal){...if(setter){setter.call(obj,newVal)}else{val=newVal}...dep.notify()//这里是notify观察这个Data依赖(watcher)}})}关于依赖收集和notify,主要是Dep类文件路径:src/observer/dep.jsexportdefaultfunctionDep(){this.id=uid++this.subs=[]}这里是subs一个包含订阅者(即观察者)的数组。当观察到的数据发生变化时,调用setter,然后dep.notify()会循环这里的订阅者,分别调用它们的update方法。但是在getter收集依赖项的代码中,我没有看到watcher被添加到subs中。什么时候添加的?说到Watcher就会回答这个问题。根据mount节点的生命周期图,观察data和一些init之后就是$mount,最重要的是_compile。文件路径:src/instance/api/lifecycle.jsVue.prototype.$mount=function(el){...this._compile(el)...}_compile分为两步:compile和linkcompile编译过程为通过分析给出的元素(el)或模板(template),提取指令(directive)并创建对应的离线DOM元素(文档片段)。文件路径:src/instance/internal/lifecycle.jsVue.prototype._compile=function(el){...varrootLinker=compileRoot(el,options,contextOptions)...varrootUnlinkFn=rootLinker(this,el,this._scope)...varcontentUnlinkFn=compile(el,options)(this,el)...}以编译#mountNode元素为例,大致过程如下:compileRoot:由于根节点(
)本身没有指令,所以这里也没有编译什么compileChildNode:mountNode的子节点,也就是内容为"{{message}}"的TextNodecompileTextNode:3.1parseText:其实就是tokenization(tokenization:从字符串、Statements等有意义的元素中提取符号),结果是tokens3.2processTextToken:从tokens中分析指令类型、表达式和过滤器,新建一个空的TextNode3.3创建一个片段,追加新的TextNode转化为parseText时使用正则表达式(/\{\{\{(.+?)\}\}\}|\{\{(.+?)\}\}/g)匹配字符串"{{消息e}}”,得到的token包含以下信息:“这是一个标签,而且是文本(text)而不是HTML标签,不是一次性插值(one-timeinterpolation),并且内容是标签是“消息”。这里用于匹配的正则表达式是根据delimiters和unsafeDelimiters的配置动态生成的。processTextToken之后,其实得到了创建一个command所需要的所有信息:command类型v-text,表达式“message”,没有filter,command负责跟进的DOM就是新创建的TextNode。接下来是实例化指令。链接在每个编译函数之后返回一个链接函数(linkFn)。linkFn是实例化指令,将指令与新创建的元素链接起来,然后将元素替换到DOM树中。每个linkFn函数返回一个取消链接函数(unlinkFn)。unlinkFn在vm销毁时使用,这里不做介绍。Instantiatedirective:newDirective(description,vm,el)description是编译结果token中保存的信息,内容如下:description={name:'text',//textdirectiveexpression:'message',filters:undefined,def:vTextDefinition}def属性是文本指令的定义。和CustomDirective一样,text指令也有bind和update方法,定义如下:文件路径:src/directives/public/text.jsexportdefault{bind(){this.attr=this.el.nodeType===3?'data':'textContent'},update(value){this.el[this.attr]=_toString(value)}}newDirective()constructor只是一些内部属性的赋值。真正的绑定过程还需要调用Directive.prototype._bind,在Vue实例方法_bindDir()中调用。在_bind中会创建一个watcher,第一次通过watcher获取表达式“message”的计算值,更新为之前创建的TextNode,页面上会渲染“HelloWorld”。watcher对于模板中的每个指令/数据绑定,都会有一个相应的watcher对象,它将在评估期间“接触”的任何属性记录为依赖项。稍后当依赖项的设置器被调用时,它会触发观察者重新评估,进而导致其相关指令执行DOM更新。每个数据绑定指令都有一个观察者来帮助它监视表达式的值,如果有变化,通知它更新它负责的DOM。说过的依赖收集就发生在这里。在Directive.prototype._bind()中会加入newWatcher(expression,update),传入表达式和指令的update方法。Watcher会去parseExpression:文件路径:src/parsers/expression.jsexportfunctionparseExpression(exp,needSet){exp=exp.trim()//trycachevarhit=expressionCache.get(exp)if(hit){if(needSet&&!hit.set){hit.set=compileSetter(hit.exp)}returnhit}varres={exp:exp}res.get=isSimplePath(exp)&&exp.indexOf('[')<0//optimizedsupersimplegetter?makeGetterFn('scope.'+exp)//dynamicgetter:compileGetter(exp)if(needSet){res.set=compileSetter(exp)}expressionCache.put(exp,res)returnres}其中expression是“message”,一个单一的变量,被认为是简单的数据访问路径(simplePath)。如何计算simplePath的值,如何通过“message”字符串得到data.message的值?要获取字符串对应的变量的值,除了eval之外,还可以使用Function。上面的makeGetterFn('scope.'+exp)返回:vargetter=newFunction('scope','return'+body+';')//newFunction('scope','returnscope.message;')Watch.prototype.get()获取表达式值时,varscope=this.vmgetter.call(scope,scope)//执行vm.message因为数据在initState期间被代理,其中vm.message是vm._data.message是“HelloWorld”定义的在数据选项中。获取到值,什么时候将消息设置为依赖?这与上面观察数据中提到的reactiveGetter相结合。文件路径:src/watcher.jsWatcher.prototype.get=function(){this.beforeGet()//->Dep.target=thisvarscope=this.scope||this.vm...varvaluevalue=this.getter.call(scope,scope)...this.afterGet()//->Dep.target=nullreturnvalue}watcher分三步获取表达式的值:beforeGet:setDep.target=this调用表达式的getter,读取(getter的值)vm.message进入message的reactiveGetter。由于Dep.target是有值的,所以会执行dep.depend()将target,也就是当前的watcher放入dep.subs数组中。afterGet:setDep.target=null这里值得注意的是,Dep.target由于JS的单线程特性,同时只能有一个watcher获取数据的值,所以只需要成为全球环境中的一个目标。文件路径:src/observer/dep.js//thecurrenttargetwatcherbeingevaluated.//thisisgloballyuniquebecausetherecouldbeonlyone//watcherbeingevaluateddatanytime.Dep.target=null这样,命令通过watcher去触及表达式中涉及的数据,同时,数据(反应性数据)被保存为其更改的订阅者。当数据发生变化时,通过dep.notify()->watcher.update()->directive.update()->textDirective.update()完成DOM更新。至此,页面上如何渲染“HelloWorld”的过程就基本结束了。最简单的使用,选取核心步骤进行分析,更多内部细节稍后分享。