从头梳理Vue2.x源码之组件原理 & Diff 算法(三)

600

组件原理解析

一.全局组件的解析

<div id="app">
    <my-component></my-component>
    <my-component></my-component>
</div>
<script>
Vue.component('my-component',{
	template:'<button>点我</button>',
});
let vm = new Vue({
	el:'#app'
});
</script>

我们可以通过Vue.component注册全局组件,之后可以在模板中进行使用

export function initGlobalAPI(Vue){
    // 整合了所有的全局相关的内容
    Vue.options ={}
    initMixin(Vue);

    // _base 就是Vue的构造函数
    Vue.options._base = Vue;
    Vue.options.components = {}

    // 注册API方法
    initAssetRegisters(Vue);
}

1.Vue.component方法

export default function initAssetRegisters(Vue) {
    Vue.component = function (id, definition) {
        definition.name = definition.name || id;
        definition = this.options._base.extend(definition);
        this.options['components'][id] = definition;
    }
}

Vue.component内部会调用Vue.extend方法,将定义挂载到Vue.options.components上。这也说明所有的全局组件最终都会挂载到这个变量上

export function initGlobalAPI(Vue){
    // 整合了所有的全局相关的内容
    Vue.options ={}
    initMixin(Vue);
    // _base 就是Vue的构造函数
    Vue.options._base = Vue;
    Vue.options.components = {}

    // initExtend
+   initExtend(Vue);

    // 注册API方法
    initAssetRegisters(Vue);
}

2.Vue.extend方法

import {mergeOptions} from '../util/index'
export default function initExtend(Vue) {
    let cid = 0;
    Vue.extend = function (extendOptions) {
        const Super = this;
        const Sub = function VueComponent(options) {
            this._init(options)
        }
        Sub.cid = cid++;
        Sub.prototype = Object.create(Super.prototype);
        Sub.prototype.constructor = Sub;
        Sub.options = mergeOptions(
            Super.options,
            extendOptions
        );
        return Sub
    }
}

extend方法就是创建出一个子类,继承于Vue,并返回这个类

3.属性合并

function mergeAssets(parentVal,childVal){
    const res = Object.create(parentVal);
    if(childVal){
        for(let key in childVal){
            res[key] = childVal[key];
        }
    }
    return res;
}
strats.components = mergeAssets;

4.初始化合并

vm.$options = mergeOptions(vm.constructor.options,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'
)

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

1.创建组件虚拟节点

export function createElement(vm,tag, data = {}, ...children) {
    let key = data.key;
    if (key) {
        delete data.key;
    }
    if (typeof tag === 'string') {
        if (isReservedTag(tag)) {
            return vnode(tag, data, key, children, undefined);
        } else {
            // 如果是组件需要拿到组件的定义,通过组件的定义创造虚拟节点
            let Ctor = vm.$options.components[tag];
            return createComponent(vm,tag,data,key,children,Ctor)
        }
    }
}
function createComponent(vm,tag,data,key,children,Ctor){
    // 获取父类构造函数t 
    const baseCtor = vm.$options._base; 
    if(isObject(Ctor)){
        Ctor = baseCtor.extend(Ctor);
    }
    data.hook = { // 组件的生命周期钩子
        init(){}
    }
    return vnode(`vue-component-${Ctor.cid}-${tag}`,data,key,undefined,{Ctor,children});
}
function vnode(tag, data, key, children, text, componentOptions) {
    return {tag, data, key, children, text, componentOptions}
}

2.创建组件的真实节点

export function patch(oldVnode,vnode){
    // 1.判断是更新还是要渲染
    if(!oldVnode){
        return createElm(vnode);
    }else{
        // ...
    }
}
function createElm(vnode){ // 根据虚拟节点创建真实的节点
    let {tag,children,key,data,text} = vnode;
    // 是标签就创建标签
    if(typeof tag === 'string'){
        // createElm需要返回真实节点
        if(createComponent(vnode)){
            return vnode.componentInstance.$el;
        }
        vnode.el = document.createElement(tag);
        updateProperties(vnode);
        children.forEach(child=>{ // 递归创建儿子节点,将儿子节点扔到父节点中
            return vnode.el.appendChild(createElm(child))
        })
    }else{
        // 虚拟dom上映射着真实dom  方便后续更新操作
        vnode.el = document.createTextNode(text)
    }
    // 如果不是标签就是文本
    return vnode.el;
}
function createComponent(vnode) {
    let i = vnode.data;
    if((i = i.hook) && (i = i.init)){
        i(vnode);
    }
    if(vnode.componentInstance){
        return true;
    }
}

调用init方法,进行组件的初始化

data.hook = {
    init(vnode){
        let child = vnode.componentInstance = new Ctor({});
        child.$mount(); // 组件的挂载
    }
}

Diff算法解析

import {compileToFunction} from './compiler/index.js';
import { patch,createElm } from './vdom/patch';
// 1.创建第一个虚拟节点
let vm1 = new Vue({data:{name:'zf'}});
let render1 = compileToFunction('<div>{{name}}</div>')
let oldVnode = render1.call(vm1)
// 2.创建第二个虚拟节点
let vm2 = new Vue({data:{name:'jw'}});
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);
// 3.通过第一个虚拟节点做首次渲染
let el = createElm(oldVnode)
document.body.appendChild(el);
// 4.调用patch方法进行对比操作
patch(oldVnode,newVnode);

我们想掌握vue中的diff算法就先构建出两个虚拟dom 之后做patch

一.基本Diff算法

1.比对标签

 // 如果标签不一致说明是两个不同元素
 if(oldVnode.tag !== vnode.tag){
    oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
 }

在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签

// 如果标签一致但是不存在则是文本节点
if(!oldVnode.tag){
    if(oldVnode.text !== vnode.text){
    	oldVnode.el.textContent = vnode.text;
    }
}

如果标签一致,有可能都是文本节点,那就比较文本的内容即可

2.比对属性

// 复用标签,并且更新属性
let el = vnode.el = oldVnode.el;
updateProperties(vnode,oldVnode.data);
function updateProperties(vnode,oldProps={}) {
    let newProps = vnode.data || {};
    let el = vnode.el;
    // 比对样式
    let newStyle = newProps.style || {};
    let oldStyle = oldProps.style || {};
    for(let key in oldStyle){
        if(!newStyle[key]){
            el.style[key] = ''
        }
    }
    // 删除多余属性
    for(let key in oldProps){
        if(!newProps[key]){
            el.removeAttribute(key);
        }
    }
    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]);
        }
    }
}

当标签相同时,我们可以复用老的标签元素,并且进行属性的比对。

3.比对子元素

// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
	
// 老的有儿子新的没有清空即可
}else if(oldChildren.length > 0 ){
	el.innerHTML = '';
// 新的有儿子
}else if(newChildren.length > 0){
	for(let i = 0 ; i < newChildren.length ;i++){
		let child = newChildren[i];
		el.appendChild(createElm(child));
	}
}

这里要判断新老节点儿子的状况

if (oldChildren.length > 0 && newChildren.length > 0) {
	updateChildren(el, oldChildren, newChildren)
	// 老的有儿子新的没有清空即可
}

二.Diff中的优化策略

1.在开头和结尾新增元素

function isSameVnode(oldVnode,newVnode){
    // 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
    return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
function updateChildren(parent, oldChildren, newChildren) {
    let oldStartIndex = 0;
    let oldStartVnode = oldChildren[0];
    let oldEndIndex = oldChildren.length - 1;
    let oldEndVnode = oldChildren[oldEndIndex];

    let newStartIndex = 0;
    let newStartVnode = newChildren[0];
    let newEndIndex = newChildren.length - 1;
    let newEndVnode = newChildren[newEndIndex];

    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        // 优化向后追加逻辑
        if(isSameVnode(oldStartVnode,newStartVnode)){
            patch(oldStartVnode,newStartVnode);
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
        // 优化向前追加逻辑
        }else if(isSameVnode(oldEndVnode,newEndVnode)){ 
            patch(oldEndVnode,newEndVnode); // 比较孩子 
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
        }
    }
    if(newStartIndex <= newEndIndex){
        for(let i = newStartIndex ; i<=newEndIndex ;i++){
            let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
            parent.insertBefore(createElm(newChildren[i]),ele);
        }
    }
}

2.头移尾、尾移头

// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
    patch(oldStartVnode,newEndVnode);
    parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
    oldStartVnode = oldChildren[++oldStartIndex];
    newEndVnode = newChildren[--newEndIndex]
// 尾部移动到头部
}else if(isSameVnode(oldEndVnode,newStartVnode)){
    patch(oldEndVnode,newStartVnode);
    parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
    oldEndVnode = oldChildren[--oldEndIndex];
    newStartVnode = newChildren[++newStartIndex]
}

以上四个条件对常见的dom操作进行了优化。

3.暴力比对

function makeIndexByKey(children) {
    let map = {};
    children.forEach((item, index) => {
    	map[item.key] = index
    });
    return map; 
}
let map = makeIndexByKey(oldChildren);

对所有的孩子元素进行编号

let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
    parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
    let moveVnode = oldChildren[moveIndex]; 
    oldChildren[moveIndex] = undefined;
    parent.insertBefore(moveVnode.el, oldStartVnode.el);
    patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]

用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入

if(oldStartIndex <= oldEndIndex){
    for(let i = oldStartIndex; i<=oldEndIndex;i++){
        let child = oldChildren[i];
        if(child != undefined){
            parent.removeChild(child.el)
        }
    }
}

如果有剩余则直接删除

if(!oldStartVnode){
    oldStartVnode = oldChildren[++oldStartIndex];
}else if(!oldEndVnode){
    oldEndVnode = oldChildren[--oldEndIndex]
}

在比对过程中,可能出现空值情况则直接跳过

三.更新操作

Vue.prototype._update = function (vnode) {
    const vm  = this;
    const prevVnode = vm._vnode; // 保留上一次的vnode
    vm._vnode = vnode;
    if(!prevVnode){
        vm.$el = patch(vm.$el,vnode); // 需要用虚拟节点创建出真实节点 替换掉 真实的$el
        // 我要通过虚拟节点 渲染出真实的dom     
    }else{
        vm.$el = patch(prevVnode,vnode); // 更新时做diff操作
    }
}