资讯专栏INFORMATION COLUMN

Vue源码解析(四)-components组件

zlyBear / 830人阅读

摘要:组件初始化渲染本文以局部组件的注册方式介绍组件的初始化渲染,如下源码解析一模版渲染介绍过,初始化时根据函数生成函数,本文函数会调用,判断是注册过的组件,因此以组件的方式生成生成的函数会调用本例,在属性中注册过,因此以组件的

组件初始化渲染

本文以局部组件的注册方式介绍组件的初始化渲染,demo如下

new Vue({
  el: "#app",
  template: 
  `
father component!
`, components:{ "my-component": { template: "
children component!
" } } })

1、Vue源码解析(一)-模版渲染介绍过,vue初始化时根据template函数生成render函数,本文render函数会调用vm._c("my-component"),_createElement判断"my-component是注册过的组件,因此以组件的方式生成vnode

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

//template生成的render函数vm._render会调用vm._c("my-component")
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

function _createElement(){
    //本例tag=‘my-component’,‘my-component’在components属性中注册过,因此以组件的方式生成vnode
    if (isDef(Ctor = resolveAsset(context.$options, "components", tag))) {
      vnode = createComponent(Ctor, data, context, children, tag);
    }
}

//本例Ctor参数{template: "
children component1!
"} function createComponent (Ctor){ //Vue构造函数 var baseCtor = context.$options._base; if (isObject(Ctor)) { //生成VuComponent构造函数 //此处相当于Ctor = Vue.extend({template: "
children component1!
"}), Vue.extend后面有介绍; Ctor = baseCtor.extend(Ctor); } //将componentVNodeHooks上的方法挂载到vnode上,组件初次渲染会用到componentVNodeHooks.init var data = {} mergeHooks(data); var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : "")), data, undefined, undefined, undefined, context, { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children } ); } //component初始化和更新的方法,此处先介绍init var componentVNodeHooks = { init(vnode){ //根据Vnode生成VueComponent实例 var child = vnode.componentInstance = createComponentInstanceForVnode(vnode); //将VueComponent实例挂载到dom节点上,本文是挂载到节点 child.$mount(hydrating ? vnode.elm : undefined, hydrating); } }

2、调用vm._update将vnode渲染为浏览器dom,主要方法是遍历vnode的所有节点,根据节点类型调用相关的方法进行解析,本文主要介绍components的解析方法createComponent:根据vnode生成VueComponent(继承Vue)对象,
调用Vue.prototype.$mount方法渲染dom

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    //组件vnode节点渲染方法
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    //Vue源码解析(一)中介绍过普通vnode节点渲染步骤 
    //根据vnode节点生成浏览器Element对象
    vnode.elm = nodeOps.createElement(tag, vnode);
    var children = vnode.children;
    //递归将vnode子节点生成Element对象
    createChildren(vnode, children, insertedVnodeQueue);
    //将生成的vnode.elm插入到浏览器的父节点当中
    insert(parentElm, vnode.elm, refElm);
}

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      //i就是上面的componentVNodeHooks.init方法
      i(vnode, false /* hydrating */, parentElm, refElm);
    }
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
}  

function createComponentInstanceForVnode (){
    var options = {
      _isComponent: true,
      parent: parent,
      propsData: vnodeComponentOptions.propsData,
      _componentTag: vnodeComponentOptions.tag,
      _parentVnode: vnode,
      _parentListeners: vnodeComponentOptions.listeners,
      _renderChildren: vnodeComponentOptions.children,
      _parentElm: parentElm || null,
      _refElm: refElm || null
    };
    //上面提到的VueComponent构造函数Ctor,相当于new VueComponent(options)
    return new vnode.ComponentOptions.Ctor(options)
}

3 、new VueComponent和new Vue的过程类似,本文就不再做介绍

全局注册组件

上文提到过 Vue.extend方法(继承Vue生成VueComponent构造函数)此处多带带介绍一下

  Vue.extend = function (extendOptions) {
    var Super = this;
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    //经典的继承写法
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    return Sub
  };

通过Vue.component也可以全局注册组件,不需要每次new vue的时候多带带注册,demo如下:

var globalComponent = Vue.extend({
    name: "global-component",
    template: "
global component!
" }); Vue.component("global-component", globalComponent); new Vue({ el: "#app", template: `
`, components:{ "my-component": { template: "
children component!
" } } })

vue.js初始化时会先调用一次initGlobalAPI(Vue),给Vue构造函数挂载上一些全局的api,其中又会调用到
initAssetRegisters(Vue),其中定义了Vue.component方法,具体看下其实现

  var ASSET_TYPES = [
      "component",
      "directive",
      "filter"
  ];
  //循环注册ASSET_TYPES中的全局方法
  ASSET_TYPES.forEach(function (type) {
    Vue[type] = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + "s"][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== "production" && type === "component") {
          validateComponentName(id);
        }
        if (type === "component" && isPlainObject(definition)) {
          definition.name = definition.name || id;
          definition = this.options._base.extend(definition);
        }
        if (type === "directive" && typeof definition === "function") {
          definition = { bind: definition, update: definition };
        }
        //全局的组件、指令和过滤器都挂载在Vue.options上
        this.options[type + "s"][id] = definition;
        return definition
      }
    };
  });

  Vue.prototype._init = function (options) {
      vue初始化时将options参数和Vue.options组装为vm.$options
      vm.$options = mergeOptions(
        //Vue.options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
  }

本例组装后的vm.$option.components值如下,proto中前3个属性是内置全局组件

组件通信 prop

在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息.先看看prop是怎么工作的。demo如下:

new Vue({
  el: "#app",
  template:
  `
father component!
`, components:{ "my-component":{ props: ["message"], template: "{{ message }}" } } })

1、template生成的render函数包含:_c("my-component",{attrs:{"message":"hello!"}})]
2、render => vnode => VueComponent,上文提到的VueComponent的构造函数调用了Vue.prototype._init,并且入参option.propsData:{message: "hello!"}
3、双向绑定中介绍过Vue初始化时会对data中的所有属性调用defineReactive方法,对data属性进行监听;
VueComponent对propsData也是类似的处理方法,initProps后propsData中的属性和data一样也是响应式的,propsData变化,相应的view也会发生改变

function initProps (vm, propsOptions) {
    for (var key in propsOptions){
        //defineReactive参照Vue源码解析(二)
        defineReactive(props, key, value);
        //将propsData代理到vm上,通过vm[key]访问propsData[key]
        proxy(vm, "_props", key);
    }
}

4、propsData是响应式的了,但更常用的是动态props,按官网说法:“我们可以用v-bind来动态地将prop绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件”,那么vue是如何将data的变化传到给自组件的呢,先看demo

var vm = new Vue({
  el: "#app",
  template:
  `
`, data(){ return{ parentMsg:"hello" } }, components:{ "my-component":{ props: ["message"], template: "{{ message }}" } } }) vm.parentMsg = "hello world"

5、双向绑定中介绍过vm.parentMsg变化,会触发dep.notify(),通知watcher调用updateComponent;
又回到了updateComponent,之后的dom更新过程可以参考上文的组件渲染逻辑,只是propsData值已经是最新的vm.parentMsg的值了

//又见到了。。所有的dom初始化或更新都会用到
updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

Vue.prototype._update = function (vnode, hydrating) {
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    //Vue源码解析(一)介绍过dom初始化渲染的源码
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // 本文介绍dom更新的方法
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
}

Vue源码解析(一)介绍过vm.__patch__中dom初始化渲染的逻辑,本文再简单介绍下vm.__patch关于component更新的逻辑:

function patchVnode (oldVnode, vnode){
    //上文介绍过componentVNodeHooks.init,此处i=componentVNodeHooks.prepatch
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode);
    }
}
var componentVNodeHooks = {
  init(){},
  prepatch: function prepatch (oldVnode, vnode) {
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    //更新组件
    updateChildComponent(
      child,
      //此时的propsData已经是最新的vm.parentMsg
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  }
}

function updateChildComponent (vm, propsData){
    //将vm._props[key]设置为新的propsData[key]值,从而触发view层的更新
    var props = vm._props;
    props[key] = validateProp(key, vm.$options.props, propsData, vm);
}
emit

子组件向父组件通信需要用到emit,先给出demo

var vm = new Vue({
  el: "#app",
  template:
  `
`, methods:{ receiveFn(msg){ console.log(msg) } }, components:{ "my-component":{ template: "
child
", mounted(){ this.$emit("rf","hello") } } } })

本例中子组件mount结束会触发callHook(vm, "mounted"),调用this.$emit("rf","hello"),从而调用父组件的receiveFn方法

Vue.prototype.$emit = function (event) {
    //本例cbs=vm._events["rf"] = receiveFn,vm._events涉及v-on指令解析,以后有机会详细介绍下
    var cbs = vm._events[event];
    //截取第一位之后的参数
    var args = toArray(arguments, 1);
    //执行cbs
    cbs.apply(vm, args);
}
event bus

prop和emit是父子组件通信的方式,非父子组件可以通过event bus(事件总线)实现

var bus = new Vue();
var vm = new Vue({
  el: "#app",
  template:
  `
`, components:{ "my-component-1":{ template: "
child1
", mounted(){ bus.$on("event",(msg)=>{ console.log(msg) }) } }, "my-component-2":{ template: "
child2
", mounted(){ bus.$emit("event","asd") } } } })

emit方法上文已经介绍过,主要看下on方法,其实就是将fn注册到vm._events上

  Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
  };

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89782.html

相关文章

  • Vue源码解析(六)-vue-router

    摘要:是响应式的,当浏览器路由改变时,的值也会相应的改变的作用是清楚了,但页面内容的变化是怎么实现的呢下面再介绍下的作用。 先上一段简单的demo,本文根据此demo进行解析 Vue.use(VueRouter) const router = new VueRouter({ routes: [ { path: /home, component: {template: ...

    苏丹 评论0 收藏0
  • vue源码解析-事件机制

    摘要:直接写了组件机制。今天看了下的关于事件的机制。源码都是基于最新的。绑定了事件回调函数的。初始化的时候,将中的方法代理到的同时修饰了事件的回调函数。对于事件有两个底层的处理逻辑。 上一章没什么经验。直接写了组件机制。感觉涉及到的东西非常的多,不是很方便讲。今天看了下vue的关于事件的机制。有一些些体会。写出来。大家一起纠正,分享。源码都是基于最新的Vue.js v2.3.0。下面我们来看...

    LuDongWei 评论0 收藏0
  • vue@2.0源码学习---组件究竟是什么

    摘要:定义一个组件如下打印如下再回过头看,可以发现他做的工作就是扩展一个构造函数,并将这个构造函数添加到现在我们已经可以回答最开始的问题的组件是什么的组件其实就是扩展的构造函数,并且在适当的时候实例化为实例。 vue@2.0源码学习---组件究竟是什么 本篇文章从最简单的情况入手,不考虑prop和组件间通信。 Vue.component vue文档告诉我们可以使用Vue.component(...

    PiscesYE 评论0 收藏0
  • vue学习笔记(

    摘要:提供了两种向组件传递参数的方式。子路由项路径不要使用开头,以开头的嵌套路径会被当作根路径。路由实例的方法这里学习两个路由实例的方法和。实际上,是通过不同的将这些资源加载后打包,然后输出打包后文件。 一、vue-router 1、简介 我们经常使用vue开发单页面应用程序(SPA)。在开发SPA过程中,路由是必不可少的部分,vue的官方推荐是vue-router。单页面应用程序看起来好像...

    frank_fun 评论0 收藏0
  • vue学习笔记(

    摘要:提供了两种向组件传递参数的方式。子路由项路径不要使用开头,以开头的嵌套路径会被当作根路径。路由实例的方法这里学习两个路由实例的方法和。实际上,是通过不同的将这些资源加载后打包,然后输出打包后文件。 一、vue-router 1、简介 我们经常使用vue开发单页面应用程序(SPA)。在开发SPA过程中,路由是必不可少的部分,vue的官方推荐是vue-router。单页面应用程序看起来好像...

    lwx12525 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<