vue2源码学习--10组件的渲染

29 阅读3分钟

首先知道两个全局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>'
})
  1. 渲染watcher=>patch调用createElm生成真实dom,子节点递归调用createElm插入父节点
  2. createElm 判断data里有没有hook,有的话执行hook里的init方法,生成子组件实例并保存在vnode.componentInstance,然后通过是否有vnode.componentInstance判断是否是组件,组件就将vnode.componentInstance.$el返回,获取到组件对应的真实节点
    下边是获取/$el的流程
  3. vnode.componentInstance.$el是init方法里调用$mount时会调用update方法将patch结果赋值给$el
  4. 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);
			}
		}
	}
}

看下效果

image.png

image.png

这套流程非常复杂,绕来绕去的,但是理解了就会觉得脉络清晰。