<!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>
可以看到页面被正常渲染了: