此篇主要手写 Vue2.0 源码-组件原理
上一篇咱们主要介绍了 Vue Mixin原理 是Vue初始化选项合并核心的api 大家都知道 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 的原型方法
// 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 上面
// 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 进行了合并
// 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; }); 复制代码
这里又使用到了原型继承的方式来进行组件合并 组件内部优先查找自己局部定义的组件 找不到会向上查找原型中定义的组件
// 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 源码(三)-初始渲染原理
// 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 返回
至此 Vue 的 组件源码已经完结 其实每一个组件都是一个个 Vue 的实例 都会经历 init 初始化方法 建议学习组件之前先把前面的系列搞懂 组件就比较容易理解了 大家可以看着思维导图自己动手写一遍核心代码哈 遇到不懂或者有争议的地方欢迎评论留言
作者:Big shark@LX
链接:https://juejin.cn/post/6954173708344770591
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。