引入vue时,调用renderMixin初始化渲染用的相关方法到vue的prototype上:createElement方法。
判断实例选项中是否有el属性,如果有,调用实例的$mount方法进行挂载。
如果是在线编译版本,需要先编译模版,会先调用编译方法,如果不是,则直接调用lifecycle.js中的mountComponent方法。
在这个方法里,首先调用了beforeMount生命周期钩子函数,然后定义了updateComponent方法,这个方法如下:
Let updateCompoent = () => {
vm._update(vm._render(), hydrating)
}
复制代码
然后实例化一个Watcher对象,
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
复制代码
第一个参数是vue实例,第二个参数就是定义的updateCompoent方法。
实例化watcher的时候,会把第二个参数updateComponent赋值给watcher实例的getter属性上。紧接着就是调用watcher实例的get方法。
get方法先把Dep.target设置为当前的watcher实例。然后调用watcher实例的getter属性,其实就是调用刚才定义的updateComponent。
现在就来看看updateComponent的实现。
普通html元素
首先是执行了vm._render()函数。_render函数先还是调用了该vue实例的render方法(这个render方法在编译中的版本里,是在$mount挂载之前,编译模版,编译成功后,把结果赋值给vue实例的render属性的)。
_render方法是用call的方式调用的,
vnode = render.call(vm._renderProxy, vm.$createElement),
第一个参数是vm._renderProxy,可以通过代理做一次校验。第二个参数是给用户手写render用的,比如:
new Vue({
el: ‘#app’,
render: (createElement) => {
return createElement(‘div’, {}, this.message)
},
data () {
return {
message: ‘dom'
}
}
})
复制代码
如果实例选项中有_parentNode,就赋值给vnode.parent。
通过_render生成vnode后,再执行_update方法。update方法会先调用beforeUpdate钩子函数,然后进行__patch_。
首先判断这个vue实例有没有preVnode,这个preVnode取自vm._vnode。
如果没有的,证明是新增的节点。传参如下:
vm.patch(vm.options._parentElm, vm.options._parentElm = vm.$option._refElm = null,赋值为null
如果有,证明是修改旧的节点。传参如下:
vm.patch(prevVnode, vnodev)
那么现在就来看下__patch__方法。
我们知道,vnode的好处就是可以在不同平台渲染,可以在web,也可以直接渲染为原生代码(weex)。最终渲染成什么,其实就是调用不同的__patch__方法进行渲染了。所以我们分别在src/platforms/weex/runtime/index.js和src/platforms/web/runtime/index.js中找到了__patch__的定义。它们分别给__patch__赋值了不同平台文件夹下的path函数。而且如果不是web平台而是服务器渲染的话,只是赋值一个空函数给__patch__。
其实每个平台的path函数都会调用core/vdom/patch.js下的createPatchFunction方法,只是传入的参数不同。这样做既能满足不同平台的实现,又能使代码得到复用。
接下来就看下createPatchFunction方法。这个方法传入两个参数,第一个参数是nodeOps是平台对dom操作的方法的封装对象。第二个参数modules是平台的一些模块。
可以根据上面的例子看下如何调用patch的。
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
复制代码
#app节点下新挂一个div。
第一个参数vm.el是#app对应的DOM对象,是在模版编译前的this.mount中赋值的。就是节点要挂在的根节点。第二个参数就是新增的vnode了。第三个参数不是服务端渲染就是false。第三个参数是父节点,第四个参数是兄弟节点。
这里可能会疑惑,新创建一个节点,为什么第一个参数要传父节点呢?我们可以看下patch的逻辑。先判断第一个参数oldVnode是否有值,这里是有值的,紧接着判断oldVnode.nodeType是否有值,如果有,证明是个真实的节点,而不是个VNode。那么当前这个例子,oldVNode肯定是有值的,就是要挂载的父节点。
然后再创建一个空节点赋值给oldVNode,把之前的要挂载的父节点赋值给变量parentElm。最后调用createElm方法进行新节元素创建。
createElm(vnode,
insertedVnodeQueue,
oldElm._leaveCB ? null : parentElm, // 父节点
nodeOps.nextSibling(oldElm) // 兄弟节点
)
复制代码
createElm方法,会先调用createComponent方法,通过vnode.data是否存在来判断该vnode是否是组件,如果不是,跳出该方法,继续普通html节点的创建。
处理普通html节点,会根据vnode.tag,vnode.isComment判断是普通元素还是注释或是文本节点。
如果有tag,再通过vnode.ns判断是调用nodeOps.createElementNS还是nodeOps.createElement。紧接着会调用setScope方法,在这个新增元素上添加fnScopeId属性,是调用nodeOps.setStyleScope方法,其实就是node.setAttribute(scopeId, ‘’)。
设置之后,我们的class就会根据这个属性id找到对应的样式,实现了样式的作用域。
如果vnode.isComment是true,证明是注释节点,直接调用document.createComment()创建注释节点。然后判断这个节点是否有父节点,如果有,再判断是否有兄弟节点,就把该注释节点插入到兄弟节点前parentNode.insertBefore(newNode,referenceNode),如果没有兄弟节点,就插在父节点之后node.appendChild(child)。
其他情况就是创建文本节点了,直接调用document.createTextNode()创建文本节点。然后判断这个节点是否有父节点,如果有,再判断是否有兄弟节点,就把该注释节点插入到兄弟节点前parentNode.insertBefore(newNode,referenceNode),如果没有兄弟节点,就插在父节点之后node.appendChild(child)。
下面再来看下,修改普通旧节点的patch的逻辑。
oldVnode和vnode如果不是相同节点,取出oldVnode的真实元素的父元素(oldVnode.elm.parentNode),调用createElm,传入vnode和parentElm创建新元素就可以了。
oldVnode和vnode如果是相同节点,就比较麻烦了,需要比对进行替换。这里就调用patchVnode方法进行对比。这里的对比会优先处理其子节点。
首先会根据新节点是否是文本节点来做处理。
如果不是文本节点:
新旧节点都有子节点,就调用updateChildren对子节点比较。
如果只是新节点有子节点,就新建一个子节点的vnode,
如果只有旧节点有子节点,就删除这个旧的子节点。如果这个旧的子节点是文本节点,就把引用的真实dom元素elm设置为空字符
如果当前节点是文本节点(vnode.text有定义),就把新vnode的text值设置到elm里。
下面就要看下子节点对比逻辑了,就是updateChildren函数。
这个函数如此实现的主要目的是为了尽量减少Dom元素的处理。能不移动就不动,能不创建和删除就不创建和删除。
先看下比较的整体思路:
创建四个指针,分别为新头指针,新尾指针,旧头指针,旧尾指针。
新头与旧头相比,相同的话再调用patchVnode比较递归它们的子节点,新头指针和旧头指针右移一位。
新尾与旧尾相比,相同的话再调用patchVnode比较递归它们的子节点,新头指针左移一位和旧头指针左移一位。
新头与旧尾相比,相同的话再调用patchVnode比较递归它们的子节点,新头指针右移一位和旧头指针左移一位。
新尾与旧头相比,相同的话再调用patchVnode比较递归它们的子节点,新尾指针左移一位和旧头指针右移一位。
以上都不匹配的话,拿新vnode和旧vnode列表比较,如果vnode存在在旧vnode中,再调用patchVnode比较递归它们的子节点,移动dom到旧头位置,如果不存在,就创建新节点插入旧头位置。
组件
组件在创建VNode时,和普通的html就不同了。相关代码在_createElement方法里,如果是组件会调用vdom/create-component.js里的createComponent方法。
这个方法做了几件事:
通过baseCtor.extend方法给组件创构造函数。
如果是异步组件,也在这里做处理。
调用resolveConstructorOptions,获取最新的options。因为会有之后混入的情况。
转换组件的v-model数据为props和events里。
把父类的props加入。
处理函数式组件
创建组件钩子函数,init, insert,prepatch,destory
然后组件在最后的patch.js里的createElm中,会调用本文件的createComponent。在这里会判断组件是否是keepAlive的,如果不是,就会调用之前创建的组件的钩子函数init,对组件进行实例化和手动挂载,后面的逻辑就和父组件相同了。
组件注册
Vue组件可以全局注册,局部注册和异步注册。
全局注册组件
全局注册组件是运用Vue.component()方法,把要注册的组件的构造函数添加到vue构造函数上。
首先,在initGlobalAPI中,给Vue构造函数通过Object.create(null)创建options属性,然后给option加components属性,然后把自带的创建keepAlive构造函数所需的参数添加到这个属性里。然后通过vue.extend()创建要注册组件的构造函数。最后把创建好的组件挂载到vue构造函数上options属性的components属性里。
以上,就把需要全局注册的组件的构造函数挂载到vue的构造函数上了。
最后在_createComponent的时候,会根据标签名对组件进行实例化继而进行渲染,那如何获取到其构造函数然后实例化呢?其实就是获取当前上下文对象的$options对象里的以该标签名的组件构造函数,然后实例化。不用担心获取不到,因为原型链的,如果注册到全局组件,也是能获取到的。
我们用的第三方库里的组件,Vue.use也是全局引入。
全局引入就是在vue加载时,就把组件的构造函数创建出并挂载到vue构造器上,这样就要对性能进行权衡了。现在的组件库如vant和element-ui都支持按需引入了。
局部组件注册
局部注册就更简单了,创建的时候直接从当前参数中获取到进行实例化就可以啦。
异步组件
为了优化单页面应用的首屏的加载速度,我们可以和webpack配合实现组件的异步加载,其实就是按需引入。只有页面需要展示时,才需向服务器发送请求,获取当文件。
首先异步组件是用一个函数定义组件。如
Vue.component(‘asyncComponent’, function(resolve, reject){ require(['./demo.vue’], resolve)})
或是() => import(‘./demo.vue’'),
再或是高级异步函数。
在执行createComponent方法时,会判断要创建的组件是函数还是对象,如果是函数,就不会走Vue.extend()去创建构造函数,而是走异步组件的创建。