vue 组件学习笔记

引入vue时,调用renderMixin初始化渲染用的相关方法到vue的prototype上:nextTick,render方法。通过newVue()创建一个vue实例,合并选项,然后调用initRenderintRender里为实例对象添加.c.nextTick,_render方法。 通过new Vue()创建一个vue实例,合并选项,然后调用initRender。intRender里为实例对象添加._c和.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.el=vm.el = vm.patch(vm.el,vnode,hydrating,false,vm.el, vnode, hydrating, false, vm.options._parentElm, vm.option.refElm),然后执行vm.option._refElm), 然后执行vm.options._parentElm = vm.$option._refElm = null,赋值为null

如果有,证明是修改旧的节点。传参如下:
vm.el=vm.el = 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()去创建构造函数,而是走异步组件的创建。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享