最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写Vue2.0源码(八)-组件原理

    正文概述 掘金(Big shark@LX)   2021-04-24   466

    前言

    此篇主要手写 Vue2.0 源码-组件原理

    上一篇咱们主要介绍了 Vue diff 算法原理 是对渲染更新的优化 大家都知道 Vue 的一大特色就是组件化 此篇主要介绍整个组件创建和渲染流程 其中 Vue.extend 这一 api 是创建组件的核心

    适用人群:

    1.想要深入理解 vue 源码更好的进行日常业务开发

    2.想要在简历写上精通 vue 框架源码(再也不怕面试官的连环夺命问 哈哈)

    3.没时间去看官方源码或者初看源码觉得难以理解的同学


    正文

    <script>
      // 全局组件
      Vue.component("parent-component", {
        template: `<div>我是全局组件</div>`,
      });
      // Vue实例化
      let vm = new Vue({
        el: "#app",
        data() {
          return {
            aa: 1,
          };
        },
        // render(h) {
        //   return h('div',{id:'a'},'hello')
        // },
        template: `<div id="a">
          hello 这是我自己写的Vue{{aa}}
          <parent-component><parent-component>
          <child-component></child-component>
          </div>`,
        // 局部组件
        components: {
          "child-component": {
            template: `<div>我是局部组件</div>`,
          },
        },
      });
    </script>
    

    上面演示了最基础的全局组件和局部组件的用法 其实我们每一个组件都是一个继承自 Vue 的子类 能够使用 Vue 的原型方法

    1.全局组件注册

    // src/global-api/index.js
    
    import initExtend from "./initExtend";
    import initAssetRegisters from "./assets";
    const ASSETS_TYPE = ["component", "directive", "filter"];
    export function initGlobalApi(Vue) {
      Vue.options = {}; // 全局的组件 指令 过滤器
      ASSETS_TYPE.forEach((type) => {
        Vue.options[type + "s"] = {};
      });
      Vue.options._base = Vue; //_base指向Vue
    
      initExtend(Vue); // extend方法定义
      initAssetRegisters(Vue); //assets注册方法 包含组件 指令和过滤器
    }
    

    initGlobalApi方法主要用来注册Vue的全局方法 比如之前写的Vue.Mixin 和今天的Vue.extend Vue.component等

    // src/global-api/asset.js
    
    const ASSETS_TYPE = ["component", "directive", "filter"];
    export default function initAssetRegisters(Vue) {
      ASSETS_TYPE.forEach((type) => {
        Vue[type] = function (id, definition) {
          if (type === "component") {
            //   this指向Vue
            // 全局组件注册
            // 子组件可能也有extend方法  VueComponent.component方法
            definition = this.options._base.extend(definition);
          }
          this.options[type + "s"][id] = definition;
        };
      });
    }
    

    this.options._base 就是指代 Vue 可见所谓的全局组件就是使用 Vue.extend 方法把传入的选项处理之后挂载到了 Vue.options.components 上面

    2.Vue.extend 定义

    //  src/global-api/initExtend.js
    
    import { mergeOptions } from "../util/index";
    export default function initExtend(Vue) {
      let cid = 0; //组件的唯一标识
      // 创建子类继承Vue父类 便于属性扩展
      Vue.extend = function (extendOptions) {
        // 创建子类的构造函数 并且调用初始化方法
        const Sub = function VueComponent(options) {
          this._init(options); //调用Vue初始化方法
        };
        Sub.cid = cid++;
        Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
        Sub.prototype.constructor = Sub; //constructor指向自己
        Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
        return Sub;
      };
    }
    

    Vue.extend 核心思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

    3.组件的合并策略

    // src/init.js
    
    Vue.prototype._init = function (options) {
      const vm = this;
      vm.$options = mergeOptions(vm.constructor.options, options); //合并options
    };
    

    还记得我们 Vue 初始化的时候合并 options 吗 全局组件挂载在 Vue.options.components 上 局部组件也定义在自己的 options.components 上面 那我们怎么处理全局组件和局部组件的合并呢

    // src/util/index.js
    
    const ASSETS_TYPE = ["component", "directive", "filter"];
    // 组件 指令 过滤器的合并策略
    function mergeAssets(parentVal, childVal) {
      const res = Object.create(parentVal); //比如有同名的全局组件和自己定义的局部组件 那么parentVal代表全局组件 自己定义的组件是childVal  首先会查找自已局部组件有就用自己的  没有就从原型继承全局组件  res.__proto__===parentVal
      if (childVal) {
        for (let k in childVal) {
          res[k] = childVal[k];
        }
      }
      return res;
    }
    
    // 定义组件的合并策略
    ASSETS_TYPE.forEach((type) => {
      strats[type + "s"] = mergeAssets;
    });
    

    这里又使用到了原型继承的方式来进行组件合并 组件内部优先查找自己局部定义的组件 找不到会向上查找原型中定义的组件

    4.创建组件 Vnode

    // src/util/index.js
    
    export function isObject(data) {
      //判断是否是对象
      if (typeof data !== "object" || data == null) {
        return false;
      }
      return true;
    }
    
    export function isReservedTag(tagName) {
      //判断是不是常规html标签
      // 定义常见标签
      let str =
        "html,body,base,head,link,meta,style,title," +
        "address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section," +
        "div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul," +
        "a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby," +
        "s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video," +
        "embed,object,param,source,canvas,script,noscript,del,ins," +
        "caption,col,colgroup,table,thead,tbody,td,th,tr," +
        "button,datalist,fieldset,form,input,label,legend,meter,optgroup,option," +
        "output,progress,select,textarea," +
        "details,dialog,menu,menuitem,summary," +
        "content,element,shadow,template,blockquote,iframe,tfoot";
      let obj = {};
      str.split(",").forEach((tag) => {
        obj[tag] = true;
      });
      return obj[tagName];
    }
    

    上诉是公用工具方法 在创建组件 Vnode 过程会用到

    // src/vdom/index.js
    
    import { isObject, isReservedTag } from "../util/index";
    // 创建元素vnode 等于render函数里面的 h=>h(App)
    export function createElement(vm, tag, data = {}, ...children) {
      let key = data.key;
    
      if (isReservedTag(tag)) {
        // 如果是普通标签
        return new Vnode(tag, data, key, children);
      } else {
        // 否则就是组件
        let Ctor = vm.$options.components[tag]; //获取组件的构造函数
        return createComponent(vm, tag, data, key, children, Ctor);
      }
    }
    
    function createComponent(vm, tag, data, key, children, Ctor) {
      if (isObject(Ctor)) {
        //   如果没有被改造成构造函数
        Ctor = vm.$options._base.extend(Ctor);
      }
      // 声明组件自己内部的生命周期
      data.hook = {
        // 组件创建过程的自身初始化方法
        init(vnode) {
          let child = (vnode.componentInstance = new Ctor({ _isComponent: true })); //实例化组件
          child.$mount(); //因为没有传入el属性  需要手动挂载 为了在组件实例上面增加$el方法可用于生成组件的真实渲染节点
        },
      };
    
      // 组件vnode  也叫占位符vnode  ==> $vnode
      return new Vnode(
        `vue-component-${Ctor.cid}-${tag}`,
        data,
        key,
        undefined,
        undefined,
        {
          Ctor,
          children,
        }
      );
    }
    

    改写 createElement 方法 对于非普通 html 标签 就需要生成组件 Vnode 把 Ctor 和 children 作为 Vnode 最后一个参数 componentOptions 传入

    这里需要注意组件的 data.hook.init 方法 我们手动调用 child.$mount()方法 此方法可以生成组件的真实 dom 并且挂载到自身的 $el 属性上面 对此处有疑问的可以查看小编之前文章 手写 Vue2.0 源码(三)-初始渲染原理

    5.渲染组件真实节点

    // src/vdom/patch.js
    
    // patch用来渲染和更新视图
    export function patch(oldVnode, vnode) {
      if (!oldVnode) {
        // 组件的创建过程是没有el属性的
        return createElm(vnode);
      } else {
        //   非组件创建过程省略
      }
    }
    
    // 判断是否是组件Vnode
    function createComponent(vnode) {
      // 初始化组件
      // 创建组件实例
      let i = vnode.data;
      //   下面这句话很关键 调用组件data.hook.init方法进行组件初始化过程 最终组件的vnode.componentInstance.$el就是组件渲染好的真实dom
      if ((i = i.hook) && (i = i.init)) {
        i(vnode);
      }
      // 如果组件实例化完毕有componentInstance属性 那证明是组件
      if (vnode.componentInstance) {
        return true;
      }
    }
    
    // 虚拟dom转成真实dom
    function createElm(vnode) {
      const { tag, data, key, children, text } = vnode;
      //   判断虚拟dom 是元素节点还是文本节点
      if (typeof tag === "string") {
        if (createComponent(vnode)) {
          // 如果是组件 返回真实组件渲染的真实dom
          return vnode.componentInstance.$el;
        }
        //   虚拟dom的el属性指向真实dom 方便后续更新diff算法操作
        vnode.el = document.createElement(tag);
        // 解析虚拟dom属性
        updateProperties(vnode);
        // 如果有子节点就递归插入到父节点里面
        children.forEach((child) => {
          return vnode.el.appendChild(createElm(child));
        });
      } else {
        //   文本节点
        vnode.el = document.createTextNode(text);
      }
      return vnode.el;
    }
    

    判断如果属于组件 Vnode 那么把渲染好的组件真实 dom ==>vnode.componentInstance.$el 返回

    6.组件的思维导图

    手写Vue2.0源码(八)-组件原理

    小结

    至此 Vue 的 组件源码已经完结 其实每一个组件都是一个个 Vue 的实例 都会经历 init 初始化方法 建议学习组件之前先把前面的系列搞懂 组件就比较容易理解了 大家可以看着思维导图自己动手写一遍核心代码哈 遇到不懂或者有争议的地方欢迎评论留言

    系列链接(后续都会更新完毕)

    • 手写 Vue2.0 源码(一)-响应式数据原理
    • 手写 Vue2.0 源码(二)-模板编译原理
    • 手写 Vue2.0 源码(三)-初始渲染原理
    • 手写 Vue2.0 源码(四)-渲染更新原理
    • 手写 Vue2.0 源码(五)-异步更新原理
    • 手写 Vue2.0 源码(六)-diff 算法原理
    • 手写 Vue2.0 源码(七)-Mixin 混入原理
    • 手写 Vue2.0 源码(八)-组件原理
    • 手写 Vue2.0 源码(九)-计算属性和侦听属性原理
    • 手写 Vue2.0 源码(十)-全局 api 分析
    • vue 面试真题-深入源码解析
    • 手写 vue-router 源码
    • 手写 vuex 源码
    • 手写 vue3.0 源码

    起源地下载网 » 手写Vue2.0源码(八)-组件原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元