vue数据控制视图源码解析
分析vue是如何实现数据改变更新视图的.
前记
三个月前看了vue源码来分析如何做到响应式数据的,文章名字叫vue源码之响应式数据,最后分析到,数据变化后会调用Watcher的update()方法.那么时隔三月让我们继续看看update()做了什么.(这三个月用react-native做了个项目,也无心总结了,因为好像太简单了).
本文叙事方式为树藤摸瓜,顺着看源码的逻辑走一遍,查看的vue的版本为2.5.2.我fork了一份源码用来记录注释.
目的
明确调查方向才能直至目标,先说一下目标行为:数据变化以后执行了什么方法来更新视图的.那么准备开始以这个方向为目标从vue源码的入口开始找答案.
从之前的结论开始
先来复习一下之前的结论:
vue构造的时候会在data(和一些别的字段)上建立Observer对象,getter和setter被做了拦截,getter触发依赖收集,setter触发notify.
另一个对象是Watcher,注册watch的时候会调用一次watch的对象,这样触发了watch对象的getter,把依赖收集到当前Watcher的deps里,当任何dep的setter被触发就会notify当前Watcher来调用Watcher的update()方法.
那么这里就从注册渲染相关的Watcher开始.
找到了文件在src/core/instance/lifecycle.js中.
newWatcher(vm,updateComponent,noop,null,true/*isRenderWatcher*/)
mountComponent
渲染相关的Watcher是在mountComponent()这个方法中调用的,那么我们搜一下这个方法是在哪里调用的.只有2处,分别是src/platforms/web/runtime/index.js和src/platforms/weex/runtime/index.js,以web为例:
Vue.prototype.$mount=function(
el?:string|Element,
hydrating?:boolean
):Component{
el=el&&inBrowser?query(el):undefined
returnmountComponent(this,el,hydrating)
}
原来如此,是$mount()方法调用了mountComponent(),(或者在vue构造时指定el字段也会自动调用$mount()方法),因为web和weex(什么是weex?之前别的文章介绍过)渲染的标的物不同,所以在发布的时候应该引入了不同的文件最后发不成不同的dist(这个问题留给之后来研究vue的整个流程).
下面是mountComponent方法:
exportfunctionmountComponent(
vm:Component,
el:?Element,
hydrating?:boolean
):Component{
vm.$el=el//放一份el到自己的属性里
if(!vm.$options.render){//render应该经过处理了,因为我们经常都是用template或者vue文件
//判断是否存在render函数,如果没有就把render函数写成空VNode来避免红错,并报出黄错
vm.$options.render=createEmptyVNode
if(process.env.NODE_ENV!=='production'){
/*istanbulignoreif*/
if((vm.$options.template&&vm.$options.template.charAt(0)!=='#')||
vm.$options.el||el){
warn(
'Youareusingtheruntime-onlybuildofVuewherethetemplate'+
'compilerisnotavailable.Eitherpre-compilethetemplatesinto'+
'renderfunctions,orusethecompiler-includedbuild.',
vm
)
}else{
warn(
'Failedtomountcomponent:templateorrenderfunctionnotdefined.',
vm
)
}
}
}
callHook(vm,'beforeMount')
letupdateComponent
/*istanbulignoreif*/
if(process.env.NODE_ENV!=='production'&&config.performance&&mark){
//不看这里的代码了,直接看else里的,行为是一样的
updateComponent=()=>{
constname=vm._name
constid=vm._uid
conststartTag=`vue-perf-start:${id}`
constendTag=`vue-perf-end:${id}`
mark(startTag)
constvnode=vm._render()
mark(endTag)
measure(`vue${name}render`,startTag,endTag)
mark(startTag)
vm._update(vnode,hydrating)
mark(endTag)
measure(`vue${name}patch`,startTag,endTag)
}
}else{
updateComponent=()=>{
vm._update(vm._render(),hydrating)
}
}
//wesetthistovm._watcherinsidethewatcher'sconstructor
//sincethewatcher'sinitialpatchmaycall$forceUpdate(e.g.insidechild
//component'smountedhook),whichreliesonvm._watcherbeingalreadydefined
//注册一个Watcher
newWatcher(vm,updateComponent,noop,null,true/*isRenderWatcher*/)
hydrating=false
//manuallymountedinstance,callmountedonself
//mountediscalledforrender-createdchildcomponentsinitsinsertedhook
if(vm.$vnode==null){
vm._isMounted=true
callHook(vm,'mounted')
}
returnvm
}
这段代码其实只做了3件事:
- 调用beforeMount钩子
- 建立Watcher
- 调用mounted钩子
(哈哈哈)那么其实核心就是建立Watcher了.
看一下Watcher的参数:vm是this,updateComponent是一个函数,noop是空,null是空,true代表是RenderWatcher.
在Watcher里看了isRenderWatcher:
if(isRenderWatcher){
vm._watcher=this
}
是的,只是复制了一份用来在watcher第一次patch的时候判断一些东西(从注释里看的,我现在还不知道是干嘛的).
那么只有一个问题没解决就是updateComponent是个什么东西.
updateComponent
在Watcher的构造函数的第二个参数传了function,那么这个函数就成了watcher的getter.聪明的你应该已经猜到,在这个updateComponent里一定调用了视图中所有的数据的getter,才能在watcher中建立依赖从而让视图响应数据的变化.
updateComponent=()=>{
vm._update(vm._render(),hydrating)
}
那么就去找vm._update()和vm._render().
在src/core/instance/render.js找到了._render()方法.
Vue.prototype._render=function():VNode{
constvm:Component=this
const{render,_parentVnode}=vm.$options//todo:render和_parentVnode的由来
//reset_renderedflagonslotsforduplicateslotcheck
if(process.env.NODE_ENV!=='production'){
for(constkeyinvm.$slots){
//$flow-disable-line
vm.$slots[key]._rendered=false
}
}
if(_parentVnode){
vm.$scopedSlots=_parentVnode.data.scopedSlots||emptyObject
}
//setparentvnode.thisallowsrenderfunctionstohaveaccess
//tothedataontheplaceholdernode.
vm.$vnode=_parentVnode
//renderself
letvnode
try{
vnode=render.call(vm._renderProxy,vm.$createElement)
}catch(e){
//catch其实不需要看了,都是做异常处理,_vnode是在vm._update的时候保存的,也就是上次的状态或是null(init的时候给的)
handleError(e,vm,`render`)
//returnerrorrenderresult,
//orpreviousvnodetopreventrendererrorcausingblankcomponent
/*istanbulignoreelse*/
if(process.env.NODE_ENV!=='production'){
if(vm.$options.renderError){
try{
vnode=vm.$options.renderError.call(vm._renderProxy,vm.$createElement,e)
}catch(e){
handleError(e,vm,`renderError`)
vnode=vm._vnode
}
}else{
vnode=vm._vnode
}
}else{
vnode=vm._vnode
}
}
//returnemptyvnodeincasetherenderfunctionerroredout
if(!(vnodeinstanceofVNode)){
if(process.env.NODE_ENV!=='production'&&Array.isArray(vnode)){
warn(
'Multiplerootnodesreturnedfromrenderfunction.Renderfunction'+
'shouldreturnasinglerootnode.',
vm
)
}
vnode=createEmptyVNode()
}
//setparent
vnode.parent=_parentVnode
returnvnode
}
}
这个方法做了:
- 根据当前vm的render方法来生成VNode.(render方法可能是根据template或vue文件编译而来,所以推论直接写render方法效率最高)
- 如果render方法有问题,那么首先调用renderError方法,再不行就读取上次的vnode或是null.
- 如果有父节点就放到自己的.parent属性里.
- 最后返回VNode
所以核心是这句:
vnode=render.call(vm._renderProxy,vm.$createElement)
其中的render(),vm._renderProxy,vm.$createElement都不知道是什么.
先看vm._renderProxy:是initMixin()的时候设置的,在生产环境返回vm,开发环境返回代理,那么我们认为他是一个可以debug的vm(就是vm),细节之后再看.
vm.$createElement的代码在vdom文件夹下,看了下是一个方法,返回值一个VNode.
render有点复杂,能不能以后研究,总之就是把template或者vue单文件和mount目标parse成render函数.
小总结:vm._render()的返回值是VNode,根据当前vm的render函数
接下来看vm._update()
Vue.prototype._update=function(vnode:VNode,hydrating?:boolean){
constvm:Component=this
if(vm._isMounted){
callHook(vm,'beforeUpdate')
}
//记录update之前的状态
constprevEl=vm.$el
constprevVnode=vm._vnode
constprevActiveInstance=activeInstance
activeInstance=vm
vm._vnode=vnode
//Vue.prototype.__patch__isinjectedinentrypoints
//basedontherenderingbackendused.
if(!prevVnode){//初次加载,只有_update方法更新vm._vnode,初始化是null
//initialrender
vm.$el=vm.__patch__(//patch创建新dom
vm.$el,vnode,hydrating,false/*removeOnly*/,
vm.$options._parentElm,
vm.$options._refElm
)
//noneedfortherefnodesafterinitialpatch
//thispreventskeepingadetachedDOMtreeinmemory(#5851)
vm.$options._parentElm=vm.$options._refElm=null
}else{
//updates
vm.$el=vm.__patch__(prevVnode,vnode)//patch更新dom
}
activeInstance=prevActiveInstance
//update__vue__reference
if(prevEl){
prevEl.__vue__=null
}
if(vm.$el){
vm.$el.__vue__=vm
}
//ifparentisanHOC,updateits$elaswell
if(vm.$vnode&&vm.$parent&&vm.$vnode===vm.$parent._vnode){
vm.$parent.$el=vm.$el
}
//updatedhookiscalledbytheschedulertoensurethatchildrenare
//updatedinaparent'supdatedhook.
}
我们关心的部分其实就是__patch()的部分,__patch()做了对dom的操作,在_update()里判断了是否是初次调用,如果是的话创建新dom,不是的话传入新旧node进行比较再操作.
结论
vue的视图渲染是一种特殊的Watcher,watch的内容是一个函数,函数运行的过程调用了render函数,render又是由template或者el的dom编译成的(template中含有一些被observe的数据).所以template中被observe的数据有变化触发Watcher的update()方法就会重新渲染视图.
遗留
render函数是在哪里被编译的
vue源码发布时引入不同平台最后打成dist的流程是什么
__patch__和VNode的分析