<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <my-button></my-button>
  </div>
  <script src="/dist/vue.js"></script>
  <script>
    Vue.component('my-button', {
      template: '<button>按钮1</button>'
    })
    const vm = new Vue({
      el: '#app',
      components: {
        'my-button': {
          template: `<button>按钮2</button>`
        }
      }
    })
  </script>
</body>

</html>

运行代码可以看到我们自定义的组件并没有显示到页面呢,查看 DOM 元素,发现它是以 <my-button></my-button>渲染的:

因为 html 并没有这个标签,所以当然显示不出来了,我们应该将 my-button 替换为它里面的内容,也就是 template 所指代的内容 <button>按钮2</button>,下面我们来实现这个功能。






































 
 
 
 
 
 
 
 












// src/global-api/index.js
import { mergeOptions } from '../util/index.js';
export function initGlobalAPI(Vue) {
	Vue.options = {}; // 用来存储全局的配置,如多次调用了Vue.mixin(),将里面的数据合并到这个对象

	Vue.mixin = function(mixin) {
		// 将属性合并到Vue.options上
		this.options = mergeOptions(this.options, mixin);
		return this;
	};

	// 这个变量永远指向vue的构造函数,保证this永远指向大Vue
	Vue.options._base = Vue;
	// 用来存放组件的定义
	Vue.options.components = {};
	// id 组件名 definition 组件定义
	Vue.component = function(id, definition) {
		/* 
    Vue.component('xxx', {
      name: 'xxx'
      template: '<div>xxx</div>'
    })
    如果有name优先取name
    */
		definition.name = definition.name || id;
		definition = this.options._base.extend(definition) // 通过对象产生一个构造函数
		this.options.components[id] = definition;
	};
  let cid = 0;
	Vue.extend = function(options) {
		// 子组件初始化时会new VueComponent(options)
    // Super 永远指向大Vue
		const Super = this;
		const Sub = function VueComponent(options) {
			this._init(options);
		};
    
    /* 
    防止不同的构造函数产生的组件名字是相同的情况,比如我们在页面调用了三次:
    <my-button></my-button>
    <my-button></my-button>
    <my-button></my-button>
    会生成三个同名的组件
    */
    Sub.cid = cid++;

		Sub.prototype = Object.create(Super.prototype); // 都是通过大 Vue 继承的
		Sub.prototype.constructor = Sub;
		Sub.component = Super.component;
    // 这里的目的是把 Vue.options 和当前 new 的时候传入的选项做一个合并,这样合并完成之后,当初始化也就是创建这个组件实例的时候,会再拿当前子的选项和用户传入的选项再做一个合并
    // 每次声明一个组件,都会把父级的定义在自己身上
		Sub.options = mergeOptions(Super.options, options);
		
    return Sub; // 这个构造函数是由对象产生来的
	};
}

# 创建组件的虚拟节点

在创建虚拟节点时我们要判断当前这个标签是否是组件,普通标签的虚拟节点和组件的虚拟节点有所不同


 













 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 







 



// src/vdom/index.js
import { isObject, isReservedTag } from '../util/index';

/* 
虚拟dom可以随意添加属性,ast是针对语法解析出来的,不能随意添加属性
*/
export function createTextNode(vm, text) {
	return vnode(vm, undefined, undefined, undefined, undefined, text);
}
export function createElement(vm, tag, data = {}, ...children) {
	// 如果是列表元素需要添加key属性,这里对key进行处理
	let key = data.key;
	if (key) {
		delete data.key;
	}
	// 需要对标签名做过滤,因为有可能标签名是一个自定义组件
	if (isReservedTag(tag)) {
		return vnode(vm, tag, data, key, children);
	} else {
		// Ctor 可能是对象,也可能是函数
		/* 
    const vm = new Vue({
      el: '#app',
      // 这种情况 extend 方法包裹,返回的是对象
      components: {
        'my-button': {
          template: `<button>按钮2</button>`
        }
      }
    })

    // 这种情况是函数
    Vue.component('my-button', {
      template: '<button>按钮1</button>'
    })
    */
		const 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 是对象,将其转为函数
		Ctor = vm.$options._base.extend(Ctor);
	}
	// 给组件增加生命周期
	data.hook = {
		// 初始化钩子
		init(vnode) {
			// 调用子组件的构造函数
			// 当调用组件构造函数的时候,会调用init方法,因为没有传el,所以不会走$mount方法,需要手动调用下
			const child = (vnode.componentInstance = new vnode.componentOptions.Ctor({}));
			// 手动挂载,挂载的时候会走到渲染watcher, 所以每个组件都会有一个watcher
			// 手动挂载完成后 child会有一个 $el 属性,也就是 vnode.componentInstance 会有一个$el 属性,指向真实元素
			child.$mount();
		}
	};
	// 组件的虚拟节点拥有hook和组件的 componentOptions, componentOptions 中存放了组件的构造函数
	// data指的就是元素属性, 如 <div id="app"></div>, data就是 {id:"app"}

	// 返回的格式:
	// {vm: Vue, tag: "vue-component-1-my-button", data: {…}, key: undefined, children: undefined, …}
	return vnode(vm, `vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined, { Ctor });
}
function vnode(vm, tag, data, key, children, text, componentOptions) {
	return {
		vm,
		tag,
		data,
		key,
		children,
		text,
		componentOptions
	};
}

# 创建组件的真实节点







 
 
 
 

















 
 
 
 
 
 
 
 
 
 
 
 






 
 
 
 
 



































// src/observer/patch.js
/* 
oldVnode 是一个真实节点
*/
export function patch(oldVnode, vnode) {
	// 如果挂载的时候没有元素,直接创建元素
	// 组件调用patch方法会产生$el属性,这个属性对应的就是真实元素
	if (!oldVnode) {
		return createElm(vnode); // 根据虚拟节点创建元素
	}

	// 是否是真实的 DOM 节点
	const isRealElement = oldVnode.nodeType;
	if (isRealElement) {
		// 如果oldVnode是一个真实DOM,表示是初次渲染
		const oldElm = oldVnode;
		const parentElm = oldElm.parentNode;

		// 根据虚拟节点创建真实节点
		let el = createElm(vnode);
		// 将创建的节点插入到原有节点下面
		parentElm.insertBefore(el, oldElm.nextSibling);
		parentElm.removeChild(oldVnode);
		// 把新创建的 el 替换成 vm.$el
		return el;
	}
}
function createComponent(vnode) {
	let i = vnode.data;
	if ((i = i.hook) && (i = i.init)) {
		// 调用组件的初始化方法,vnode.componentInstance 会有一个$el 属性
		i(vnode);
	}
  // 如果虚拟节点上有组件的实例,说明当前这个 vnode 是组件
	if (vnode.componentInstance) {
		return true;
	}
	return false;
}

// 根据虚拟节点创建真实节点
function createElm(vnode) {
	let { tag, children, key, data, text } = vnode;
	// 如果是标签,有可能是组件,这里忽略
	if (typeof tag === 'string') {
		// tag可能是组件,如果是组件,就直接根据组件创建出组件对应的真实节点
		if (createComponent(vnode)) {
			// 如果返回true, 说明这个虚拟节点是组件,就将组件渲染后的真实元素给我
			return vnode.componentInstance.$el;
		}

		vnode.el = document.createElement(tag);
		// 更新属性
		updateProperties(vnode);
		// 如果有子节点,需要进行递归操作
		children.forEach((child) => {
			vnode.el.appendChild(createElm(child));
		});
	} else {
		// 处理文本节点
		vnode.el = document.createTextNode(text);
	}
	return vnode.el;
}
// 更新节点属性
function updateProperties(vnode) {
	let newProps = vnode.data || {}; // 获取当前老节点中的属性
	let el = vnode.el; // 当前的真实节点
	for (let key in newProps) {
		// 处理样式
		if (key === 'style') {
			// 设置样式
			for (let styleName in newProps.style) {
				el.style[styleName] = newProps.style[styleName];
			}
		} else if (key === 'class') {
			// 设置类名
			el.className = newProps.class;
		} else {
			// 给这个元素添加属性 值就是对应的值
			el.setAttribute(key, newProps[key]);
		}
	}
}





























































































 
 
 
 
 
 
 
 
 
 

// src/util/index.js
export const LIFECYCLE_HOOKS = [
	'beforeCreate',
	'created',
	'beforeMount',
	'mounted',
	'beforeUpdate',
	'updated',
	'beforeDestroy',
	'destroyed'
];
export const isObject = (value) => typeof value === 'object' && value !== null;
const strats = {};

// 核心就是把生命周期钩子函数变成一个数组
function mergeHook(parentVal, childValue) {
	if (childValue) {
		if (parentVal) {

      return parentVal.concat(childValue);
		} else {
			// 儿子有父亲没有
			return [ childValue ];
		}
	} else {
		// 如果儿子没有就用父亲的
		return parentVal;
	}
}
// 把这些钩子都放到策略上
LIFECYCLE_HOOKS.forEach((hook) => {
	strats[hook] = mergeHook;
});
strats.components = function(parentVal, childVal) {
	const res = Object.create(parentVal);
	if (childVal) {
		for (let key in childVal) {
			res[key] = childVal[key];
		}
	}
	return res;
};
export function mergeOptions(parent, child) {
	/* 
  合并策略:
  如果父亲有的儿子也有,应该用儿子替换父亲, 如 父元素的数据是{a:1} 子元素的数据是{a:2}合并后,应该是 {a:2}

  如果父元素有值子元素没有,则用父元素的 如 父{a:1} 子{} 合并后应该是 {a: 1}
  */

	const options = {};
	for (let key in parent) {
		mergeField(key);
	}
	for (let key in child) {
		// 父亲没有儿子有
		if (!parent.hasOwnProperty(key)) {
			mergeField(key);
		}
	}
	function mergeField(key) {
		// if (strats[key]) {
		// 	options[key] = strats[key](parent[key], child[key]);
		// } else {
		// 	if (typeof parent[key] == 'object' && typeof child[key] == 'object') {
		// 		options[key] = {
		// 			...parent[key],
		// 			...child[key]
		// 		};
		// 	} else {
		// 		options[key] = child[key];
		// 	}
		// }

		// 策略模式
		if (strats[key]) {
			return (options[key] = strats[key](parent[key], child[key]));
		}

		if (isObject(parent[key]) && isObject(child[key])) {
			options[key] = { ...parent[key], ...child[key] };
		} else {
			if (child[key]) {
				// 如果儿子有值
				options[key] = child[key];
			} else {
				options[key] = parent[key];
			}
		}
	}
	return options;
}

function makeMap(str) {
	const map = {};
	const list = str.split(',');
	for (let i = 0; i < list.length; i++) {
		map[list[i]] = true;
	}
	return (key) => map[key];
}

export const isReservedTag = makeMap('a,div,img,image,text,span,input,p,button');

完成上面操作后,再次运行下面代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <my-button></my-button>
  </div>
  <script src="/dist/vue.js"></script>
  <script>
    Vue.component('my-button', {
      template: '<button>按钮1</button>'
    })
    const vm = new Vue({
      el: '#app',
      components: {
        'my-button': {
          template: `<button>按钮2</button>`
        }
      }
    })
    /* 
    1. 调用 Vue.component的时候会调用Vue.extend方法,把对象变成一个构造函数(生成子类的构造函数)
    2. 如果是组件添加了 init hook() 还添加了componentOptions
    3. 创建真实节点的时候,会调用 init 钩子,这个 init 方法会做一个初始化操作,new Ctor,当我们去 new 的时候会产生一个组件实例,然后调用$mount方法进行挂载操作,内部会再给组件添加一个 watcher,所以每个组件都有一个自己的 watcher,会将渲染的节点放到当前实例上并返回
    */
  </script>
</body>

</html>

可以看到页面被正常渲染了: