首先知道两个全局api,Vue.extend和Vue.component。
功能上来说,Vue.extend(options)会返回一个拥有Vue所有方法的类(构造函数),实例化的时候会调用继承来的init方法对数据进行劫持,手动调$mount会执行渲染watcher创建dom。
Vue.component(key,options),这个其实底层也是会调用Vue.extend(也可以手动在外边调用),然后以key=>value的形式保存到Vue.options上。
因为Vue.component会创建出全局组件并保存在Vue.options上,用extend创建子组件时传进来的options,template可能会使用组件,这个组件有自己定义的则使用自己定义的没有则去全局找,这用到了原型链,进行一次components的合并。
先简单实现这两个方法
// 手动创造组件进行挂载
Vue.extend = function(options) {
// 就是实现根据用户的参数 返回一个构造函数
function Sub(options = {}) { // 最终使用一个组件 就是new一个实例
this._init(options) // 默认对子类初始化
}
Sub.prototype = Object.create(Vue.prototype)
Sub.prototype.constructor = Sub
// 威望用户传递的参数和全局的Vue.options来合并
Sub.options = mergeOptions(Vue.options, options)
return Sub
}
Vue.options.components = {}
Vue.component = function(id, definition) {
// 如果已经是一个函数了 说明用户自己调用了Vue.extend
definition = typeof definition === 'function'? definition: Vue.extend(definition)
Vue.options.components[id] = definition
}
mergeOptions策略模式新增合并component策略
// 将父上的选项放到 res的__proto__上 然后将子的赋值,这样就会先从子上找找不到会从父上找
strats.components = function(parentVal, childVal) {
const res = Object.create(parentVal)
if(childVal) {
for (const key in childVal) {
res[key] = childVal[key]
}
}
return res
}
创建vnode的时候调用createElementVNode,之前我们默认都是原生标签,这里判断下tag如果不是原生的标签,调用createComponentVnode,并且从options上获取到当前tag对应的component选项作为参数传进去。
createComponentVnode这个函数主要是给data新增hook方法,这个方法里有init方法,init方法会生成子组件的实例并保存在vnode.componentInstance上。
说下实际流程:
// 例如<div> <my-button></my-button> </div>
Vue.component('my-button',{
template:'<button>按钮</button>'
})
- 渲染watcher=>patch调用createElm生成真实dom,子节点递归调用createElm插入父节点
- createElm 判断data里有没有hook,有的话执行hook里的init方法,生成子组件实例并保存在vnode.componentInstance,然后通过是否有vnode.componentInstance判断是否是组件,组件就将vnode.componentInstance.$el返回,获取到组件对应的真实节点
下边是获取/$el的流程 - vnode.componentInstance.$el是init方法里调用$mount时会调用update方法将patch结果赋值给$el
- patch方法对组件进行处理,我们调用$mount时没有传el,所以掉patch的时候没有旧节点的,通过这一点我们判断出是组件直接执行createElm,注意此时的vnode是组件内的button的vnode 而不是my-button的vnode所以此时是原生标签,所以走老逻辑生成真实节点并返回,所以$el赋值到了组件的真实节点。 具体代码如下
// src/vdom/index.js
const isReservedTag = (tag) => {
return ['a', 'div', 'p', 'button', 'ul', 'li', 'span'].includes(tag)
}
// _c
export function createElementVNode(vm, tag, data, ...children) {
if(data == null) {
data = {}
}
let key = data.key
if(key) {
delete data.key
}
if(isReservedTag(tag)) {
return vnode(vm, tag, key ,data, children)
} else {
// 创造一个组件虚拟节点
let Ctor = vm.$options.components[tag]
// Ctor 就是组件的定义 可能是一个Sub类 还有可能是obj选项
return createComponentVnode(vm, tag, key, data, children,Ctor)
}
}
function createComponentVnode(vm, tag, key, data, children, Ctor) {
if(typeof Ctor === 'object') {
// 这里要获取到Vue构造函数 所以在_base里先保存下Vue
Ctor = vm.$options._base.extend(Ctor)
}
data.hook = {
init(vnode) { // 创造真实节点的时候, 如果是组件则调用此init方法
let instance = vnode.componentInstance = new vnode.componentOptions.Ctor
instance.$mount()
}
}
return vnode(vm,tag, key, data, children, null, {Ctor})
}
// _v
export function createTextVNode(vm,text) {
return vnode(vm, undefined, undefined, undefined, undefined, text)
}
// ast做的语法层面的转化 描述的是语法本身
// 虚拟dom是描述dom元素,可以增加一些自定义属性
function vnode(vm, tag, key, data, children, text, componentOptions) {
return {
vm,
tag,
key,
data,
children,
text,
componentOptions
}
}
export function isSameVnode(vnode1, vnode2) {
return vnode1.tag === vnode2.tag && vnode1.key === vnode2.key
}
// src/vdom/patch.js
import { isSameVnode } from ".";
function createComponente(vnode) {
let i = vnode.data
if((i = i.hook) && (i = i.init)) {
i(vnode)
}
if(vnode.componentInstance) {
return true
}
}
export function createElm(vnode) {
let { tag, data, children, text } = vnode;
if (typeof tag === "string") {
// 创建真实元素 也要问问区分是组件还是元素
if (createComponente(vnode)) {
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag); // 真实节点和虚拟节点对应起来
patchProps(vnode.el, {}, data); // 增加属性
children.forEach((child) => {
vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
export function patchProps(el, oldProps = {}, props = {}) {
// 老的属性中有 新的没有 要删除老的
let oldStyles = oldProps.style || {};
let newStyle = props.style || {};
for (const key in oldStyles) {
if (!newStyle[key]) {
el.style[key] = "";
}
}
for (const key in oldProps) {
if (!props[key]) {
el.removeAttribute(key);
}
}
// 用新的覆盖老的
for (let key in props) {
if (key === "style") {
for (const styleName in props.style) {
el.style[styleName] = props.style[styleName];
}
} else {
el.setAttribute(key, props[key]);
}
}
}
export function patch(oldVnode, vnode) {
if(!oldVnode) {
return createElm(vnode)
}
// 初渲染流程
const isRealElement = oldVnode.nodeType; // nodeType 原生方法
if (isRealElement) {
const elm = oldVnode; //真实元素
const parentElm = elm.parentNode; //父元素
let newEle = createElm(vnode);
parentElm.insertBefore(newEle, elm.nextSibling);
parentElm.removeChild(elm);
return newEle;
} else {
patchVnode(oldVnode, vnode);
}
}
function patchVnode(oldVnode, vnode) {
// diff 算法
// 1、非相同节点 直接删除老的换新的 没有对比
// 2、两个节点是同一节点(判断节点的tag和key)比较两个节点的属性是否有差异
// 3、节点比较完毕后需要比较两个儿子
if (!isSameVnode(oldVnode, vnode)) {
// tag === tag key === key
let el = createElm(vnode);
oldVnode.el.parentNode.replaceChild(el, oldVnode.el);
return el;
}
// 文本的情况 文本我们期望比较内容
let el = (vnode.el = oldVnode.el);
if (!oldVnode.tag) {
// 是文本
if (oldVnode.text !== vnode.text) {
el.textContent = vnode.text;
}
}
// 是标签 比对属性
patchProps(el, oldVnode.data, vnode.data);
// 比较子节点 一方有儿子一方没儿子
// 两方都有儿子
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
// 完整diff算法
updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) {
// 没有老的,新的
mountChildren(el, newChildren);
} else if (oldChildren.length > 0) {
// 新的没有 老的有 删除
// unmountChildren(el, oldChildren)
el.innerHTML = "";
}
return el;
}
function mountChildren(el, newChildren) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
function updateChildren(el, oldChildren, newChildren) {
// 我们为了比较两个儿子的时候, 提升性能 有些优化手段
// vue2 中通过双指针方式进行比较
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[0];
let newStartVnode = newChildren[0];
let oldEndVnode = oldChildren[oldEndIndex];
let newEndVnode = newChildren[newEndIndex];
function makeIndexByKey(children) {
let map = {};
children.forEach((child, index) => {
map[child.key] = index;
});
return map;
}
let map = makeIndexByKey(oldChildren);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex];
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex];
// 双方有一方头指针大于尾指针 则停止循环
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
// 比较开头节点
patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 比较结尾节点
patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
// 交叉比对 abcd dabc
patchVnode(oldEndVnode, newStartVnode);
el.insertBefore(oldEndVnode.el, oldStartVnode.el);
newStartVnode = newChildren[++newStartIndex];
oldEndVnode = oldChildren[--oldEndIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
// 交叉比对 abcd dabc
patchVnode(oldStartVnode, newEndVnode);
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
newEndVnode = newChildren[--newEndIndex];
oldStartVnode = oldChildren[++oldStartIndex];
} else {
// 乱序比对
// 根据老的列表做一个映射关系,用新的去找,找到则移动,找不到则添加,最后多余的就删除
let moveIndex = map[newStartVnode.key]; //如果拿到则说明是要移动的索引
if (moveIndex !== undefined) {
let moveVnode = oldChildren[moveIndex];
el.insertBefore(moveVnode.el, oldStartVnode.el);
oldChildren[moveIndex] = undefined; // 标识这个子节点已经移走了
patchVnode(moveVnode, newStartVnode);
} else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
}
newStartVnode = newChildren[++newStartIndex];
}
}
if (newStartIndex <= newEndIndex) {
// 多余一个插入进去
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i]);
// 可能是向后追加 也可能是向前追加
// el.appendChild(childEl)
let anchor = newChildren[newEndIndex + 1]
? newChildren[newEndIndex + 1].el
: null;
el.insertBefore(childEl, anchor);
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
let childEl = oldChildren[i].el;
el.removeChild(childEl);
}
}
}
}
看下效果
这套流程非常复杂,绕来绕去的,但是理解了就会觉得脉络清晰。