前言

本文是vue2.x源码分析的第十一篇,主要看component,props,slot的处理过程!

实例代码

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Vue</title>
  6. <script src="./vue11.js" type="text/javascript" charset="utf-8" ></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <child :name='message'>
  11. <!-- <span>span from parent</span> -->
  12. </child>
  13. </div>
  14. <script>
  15. debugger;
  16. var child=Vue.component('child',{
  17. template:'<div>{{name}}<slot></slot></div>',
  18. props:['name']
  19. })
  20. var vm=new Vue({
  21. el:'#app',
  22. name:'app',
  23. data:{
  24. message:'message from parent'
  25. },
  26. });
  27. </script>
  28. </body>
  29. </html>

1、三个全局API(Vue.component、Vue.directive、Vue.filter)

initAssetRegisters函数在initGlobalAPI时被调用

  1. function initAssetRegisters (Vue) {
  2. //config._assetTypes=['component','directive','filter']
  3. config._assetTypes.forEach(function (type) {
  4. Vue[type] = function (id,definition) {
  5. if (!definition) {
  6. return this.options[type + 's'][id]
  7. } else {
  8. {
  9. if (type === 'component' && config.isReservedTag(id)) {
  10. warn(
  11. 'Do not use built-in or reserved HTML elements as component ' +
  12. 'id: ' + id
  13. );
  14. }
  15. }
  16. if (type === 'component' && isPlainObject(definition)) {
  17. definition.name = definition.name || id;
  18. definition = this.options._base.extend(definition);
  19. }
  20. if (type === 'directive' && typeof definition === 'function') {
  21. definition = { bind: definition, update: definition };
  22. }
  23. this.options[type + 's'][id] = definition;
  24. return definition
  25. }
  26. };
  27. });
  28. }

首先要知道,当vue库文件加载完后,vue的初始化中已有这个东西:

  1. Vue.options={
  2. components:{
  3. KeepAlive:Object,
  4. Transition:Object,
  5. TransitionGroup:Object
  6. },
  7. directives:{
  8. show:Object,
  9. model:Object
  10. },
  11. filter:{},
  12. _base:function Vue$3(options){...}
  13. }

这些都是vue库内置的组件和指令,当执行Vue.component、Vue.directive、Vue.filter时就是在对这些内置组件、指令、过滤器进行扩充,所以:

  1. var child=Vue.component('child',{
  2. template:'<div>child</div>',
  3. props:['name']
  4. })

执行完后

  1. Vue.options.components={
  2. KeepAlive:Object,
  3. Transition:Object,
  4. TransitionGroup:Object,
  5. child:function VueComponent(options)
  6. }

child的配置项存放在VueComponent.options中,在该函数中还对props,computed进行了处理:

  1. //对child的props的处理,key='name'
  2. proxy(VueComponent.prototype, "_props", key);
  3. //proxy函数如下:
  4. function proxy (target, sourceKey, key) {
  5. sharedPropertyDefinition.get = function proxyGetter () {
  6. return this[sourceKey][key]
  7. };
  8. sharedPropertyDefinition.set = function proxySetter (val) {
  9. this[sourceKey][key] = val;
  10. };
  11. //在VueComponent.prototype定义存取器属性'name'
  12. Object.defineProperty(target, key, sharedPropertyDefinition);
  13. }

顺便提一句,当定义全局指令时

  1. Vue.directive('v-focus',function(){...})
  2. //会将定义的函数当作bind和update函数,运行完是这样:
  3. Vue.options.directives={
  4. show:Object,
  5. model:Object,
  6. v-focus:{
  7. bind:function(){...},
  8. update:function(){...},
  9. }
  10. }

2、详细分析

vue的渲染过程可分为四步:
Vue源码分析(11)--实例分析component,props,slot - 图1
当child=Vue.component(‘child’,…)执行完后,开始new Vue的过程,经过一系列的处理,得到根节点的AST结构:

  1. attrs:Array(1) //这里存放了id:'app'
  2. attrsList:Array(1)
  3. attrsMap:Object
  4. children:Array(1)
  5. parent:undefined
  6. plain:false
  7. static:false
  8. staticRoot:false
  9. tag:"div"
  10. type:1
  11. __proto__:Object
  12. //其中的children[0],即child的AST如下:
  13. attrs:Array(1) //这里存放了name:'message'
  14. attrsList:Array(1)
  15. attrsMap:Object
  16. children:Array(0)
  17. hasBindings:true
  18. parent:Object
  19. plain:false
  20. static:false
  21. staticRoot:false
  22. tag:"child"
  23. type:1 //得到AST时,vue将child看成普通html标签,未做特殊处理
  24. __proto__:Object

此时得到的render函数如下:

  1. with(this){
  2. return _c(
  3. 'div',
  4. {attrs:{"id":"app"}},
  5. [_c(
  6. 'child',
  7. {attrs:{"name":message}}
  8. )
  9. ],
  10. 1)
  11. }

render函数执行时,message取到了’message from parent’,下面主要看_c函数的处理:

  1. vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
  2. //createElement函数如下:
  3. function createElement (context,tag,data,children,normalizationType,alwaysNormalize) {
  4. if (Array.isArray(data) || isPrimitive(data)) {
  5. normalizationType = children;
  6. children = data;
  7. data = undefined;
  8. }
  9. if (alwaysNormalize) { normalizationType = ALWAYS_NORMALIZE; }
  10. return _createElement(context, tag, data, children, normalizationType)
  11. }
  1. function _createElement (context,tag,data,children,normalizationType) {
  2. ...
  3. // support single function children as default scoped slot
  4. //
  5. ... 暂时略过slot的处理
  6. //
  7. var vnode, ns;
  8. if (typeof tag === 'string') {
  9. var Ctor;
  10. ns = config.getTagNamespace(tag);
  11. if (config.isReservedTag(tag)) {
  12. // platform built-in elements
  13. vnode = new VNode(
  14. config.parsePlatformTagName(tag), data, children,
  15. undefined, undefined, context
  16. );
  17. } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
  18. //这里取到了child的构造函数
  19. // 创建child的vnode
  20. vnode = createComponent(Ctor, data, context, children, tag);
  21. } else {
  22. // unknown or unlisted namespaced elements
  23. // check at runtime because it may get assigned a namespace when its
  24. // parent normalizes children
  25. vnode = new VNode(
  26. tag, data, children,
  27. undefined, undefined, context
  28. );
  29. }
  30. } else {
  31. // direct component options / constructor
  32. vnode = createComponent(tag, data, context, children);
  33. }
  34. if (vnode) {
  35. if (ns) { applyNS(vnode, ns); }
  36. return vnode
  37. } else {
  38. return createEmptyVNode()
  39. }
  40. }

看下createComponent函数

  1. function createComponent (Ctor,data,context,children,tag) {
  2. ...
  3. // 异步组件处理
  4. ...
  5. // 处理option
  6. resolveConstructorOptions(Ctor);
  7. data = data || {};
  8. // 将组件v-model的data转成props & events
  9. ...
  10. // 提取props
  11. var propsData = extractProps(data, Ctor, tag);
  12. // 函数组件
  13. ...
  14. // 提取事件监听
  15. var listeners = data.on;
  16. // 用.native modifier替换
  17. data.on = data.nativeOn;
  18. // 合并组件管理钩子到placeholder vnode
  19. mergeHooks(data);
  20. // 返回placeholder vnode
  21. var name = Ctor.options.name || tag;
  22. var vnode = new VNode(
  23. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  24. data, undefined, undefined, undefined, context,
  25. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
  26. );
  27. return vnode
  28. }

最终child的Vnode结构如下:

  1. children:undefined
  2. componentInstance:undefined
  3. componentOptions:Object
  4. context:Vue$3
  5. data:Object
  6. elm:undefined
  7. functionalContext:undefined
  8. isCloned:false
  9. isComment:false
  10. isOnce:false
  11. isRootInsert:true
  12. isStatic:false
  13. key:undefined
  14. ns:undefined
  15. parent:undefined
  16. raw:false
  17. tag:"vue-component-1-child"
  18. text:undefined
  19. child:(...)
  20. __proto__:Object
  21. //componentOptions对象如下:
  22. Ctor:function VueComponent(options)
  23. children:undefined
  24. listeners:undefined
  25. propsData:Object //包含name:'message from parent'
  26. tag:"child"
  27. __proto__:Object

组件的Vnode和普通html标签的Vnode的主要差异在componentOptions
在得到child的Vnode后,再得到根节点的Vnode,然后根节点的render函数执行完毕,返回根节点的Vnode(children里存放了child的Vnode)。然后执行vm.update函数,该函数调用vm._patch,patch又调用createElm函数:

  1. function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
  2. vnode.isRootInsert = !nested; // for transition enter check
  3. //注意createComponent函数,对于普通html标签对应的vnode会返回false,但对于组件的vnode会返回true
  4. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  5. return
  6. }
  7. var data = vnode.data;
  8. var children = vnode.children;
  9. var tag = vnode.tag;
  10. if (isDef(tag)) {
  11. ...
  12. vnode.elm = vnode.ns
  13. ? nodeOps.createElementNS(vnode.ns, tag)
  14. : nodeOps.createElement(tag, vnode);
  15. setScope(vnode);
  16. /* istanbul ignore if */
  17. {
  18. createChildren(vnode, children, insertedVnodeQueue);
  19. if (isDef(data)) {
  20. invokeCreateHooks(vnode, insertedVnodeQueue);
  21. }
  22. insert(parentElm, vnode.elm, refElm);
  23. }
  24. ...
  25. }
  26. ...
  27. }

首先是处理根节点的Vnode,当执行到createChildren(vnode, children, insertedVnodeQueue)时,开始处理child的Vnode,createChildren是直接调用createElm函数的,所以createElm函数再次执行,不同的是这次处理child的Vnode,来看下createComponent函数:

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. var i = vnode.data; //
  3. if (isDef(i)) {
  4. var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
  5. if (isDef(i = i.hook) && isDef(i = i.init)) {
  6. //这里开始执行vnode.data.hook.init函数
  7. i(vnode, false /* hydrating */, parentElm, refElm);
  8. }
  9. // after calling the init hook, if the vnode is a child component
  10. // it should've created a child instance and mounted it. the child
  11. // component also has set the placeholder vnode's elm.
  12. // in that case we can just return the element and be done.
  13. if (isDef(vnode.componentInstance)) {
  14. initComponent(vnode, insertedVnodeQueue);
  15. if (isTrue(isReactivated)) {
  16. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  17. }
  18. return true
  19. }
  20. }
  21. }

看下vnode.data.hook.init函数

  1. init: function init (vnode,hydrating,parentElm,refElm) {
  2. if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
  3. //这里开始创建vnode的组件实例
  4. var child = vnode.componentInstance = createComponentInstanceForVnode(
  5. vnode,
  6. activeInstance,
  7. parentElm,
  8. refElm
  9. );
  10. child.$mount(hydrating ? vnode.elm : undefined, hydrating);
  11. } else if (vnode.data.keepAlive) {
  12. // kept-alive components, treat as a patch
  13. var mountedNode = vnode; // work around flow
  14. componentVNodeHooks.prepatch(mountedNode, mountedNode);
  15. }
  16. }

看下createComponentInstanceForVnode函数

  1. function createComponentInstanceForVnode (
  2. vnode, // we know it's MountedComponentVNode but flow doesn't
  3. parent, // activeInstance in lifecycle state
  4. parentElm,
  5. refElm
  6. ) {
  7. // vnode.componentOptions如下:
  8. // Ctor:function VueComponent(options)
  9. // children:undefined
  10. // listeners:undefined
  11. // propsData:Object
  12. // tag:"child"
  13. // __proto__:Object
  14. var vnodeComponentOptions = vnode.componentOptions;
  15. var options = {
  16. _isComponent: true,
  17. parent: parent,
  18. propsData: vnodeComponentOptions.propsData,
  19. _componentTag: vnodeComponentOptions.tag,
  20. _parentVnode: vnode,
  21. _parentListeners: vnodeComponentOptions.listeners,
  22. _renderChildren: vnodeComponentOptions.children,
  23. _parentElm: parentElm || null,
  24. _refElm: refElm || null
  25. };
  26. // check inline-template render functions
  27. var inlineTemplate = vnode.data.inlineTemplate;
  28. if (inlineTemplate) {
  29. options.render = inlineTemplate.render;
  30. options.staticRenderFns = inlineTemplate.staticRenderFns;
  31. }
  32. //这里的Ctor就是 function VueComponent(options){
  33. // this._init(options);
  34. // }
  35. return new vnodeComponentOptions.Ctor(options)
  36. }

接着就进入了和new Vue()一样的处理过程了

  1. ...
  2. initLifecycle(vm);
  3. initEvents(vm);
  4. initRender(vm);
  5. callHook(vm, 'beforeCreate');
  6. initInjections(vm); // resolve injections before data/props
  7. initState(vm);
  8. initProvide(vm); // resolve provide after data/props
  9. callHook(vm, 'created');
  10. //这里没有vm.$options.el还未生成,故不执行
  11. if (vm.$options.el) {
  12. vm.$mount(vm.$options.el);
  13. }
  14. ...

此时createComponentInstanceForVnode函数执行完毕,接着执行vnode.data.hook.init中的 child.$mount(hydrating ? vnode.elm : undefined, hydrating),然后又开始了这个过程:

Vue源码分析(11)--实例分析component,props,slot - 图2

这个过程结束后vnode.data.hook.init函数执行完毕,接着执行createComponent函数中的 initComponent(vnode, insertedVnodeQueue)函数,然后createComponent函数执行完毕,从而createChildren执行完,然后
执行invokeCreateHooks(vnode, insertedVnodeQueue)和insert(parentElm, vnode.elm, refElm),页面就渲染完毕了。

3、总结component的处理过程

  • 解析模板得到根节点的AST(根节点的AST的children包含了组件节点的AST);
  • 由根节点的AST和组件节点的AST得到render函数;
  • render函数在执行时,_c函数对普通html标签和组件标签的处理不一样,不过最终都返回了Vnode结构;
  • 执行createElm函数,该函数处理两种情况:组件Vnode和普通Vnode
  • 对于组件Vnode,会再次经历模板解析->AST->render->Vnode->createElm->真实DOM

4、props和slot的原理

从component的处理过程不难发现props和slot的原理

4.1 props

  • Vue.component(child,{props:[‘name’]})执行中执行了initProps$1(Sub),该函数执行了proxy(VueComponent.prototype, “_props”, ‘name’),即把name定义成VueComponent.prototype的存取器属性,其中get函数是这样的:
  1. sharedPropertyDefinition.get = function proxyGetter () {
  2. return this[sourceKey][key]
  3. };

注:这里尚未实现this._props

  • 组件Vnode在执行this._init过程中执行了initState,该函数又执行了initProps,这个函数实现了将’name’作为this._props的存取器属性,于是在子组件中vm.name相当于vm._props.name

4.2 slot

  • 根节点AST
  • 根节点render
  • 根节点Vnode的children是子节点的Vnode,子节点Vnode的children应该是span节点,但实际上并不是,做了特殊处理,将span的Vnode放入了子节点Vnode.componentOptions.children中
  • 子节点模板的AST
  • 子节点render又做了特殊处理,slot应该是_v(‘slot’,…),但实际被处理成_t(‘default’)
  1. with(this){
  2. return _c('div',[_v(_s(name)),_t("default")],2)
  3. }
  • 子节点模板Vnode的children中已经有两个Vnode,_t(‘default’)被转成了
    从this.Vue源码分析(11)--实例分析component,props,slot - 图3slots是在initRender函数中初始化的