Vue核心原理

一、使用Rollup搭建开发环境

1、官网

rollup中文网open in new window

2、什么是Rollup?

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js更专注于Javascript类库打包 (开发应用时使用Webpack,开发库时使用Rollup)

3、环境搭建

(1) 安装命令

npm install rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-serve -D

(2) rollup.config.js文件编写

import babel from 'rollup-plugin-babel'
import serve from 'rollup-plugin-serve'

export default {
    input: './src/index.js', // 打包入口
    output: {
        format: 'umd', //模块化类型
        name: 'Vue', // 全局变量的名字
        file: 'dist/umd/vue.js', // 打包输出文件名称
        sourcemap: true // 打包前后源码映射,方便调试
    },
    plugins: [
        babel({
            exclude: 'node_modules/**' // 排除目录
        }),
        serve({
            // open: true, // 打开默认浏览器
            port: 3000, // 端口
            contentBase: '', // 以当前目录为根目录标准
            openPage: '/index.html' // 默认打开入口文件
        })
    ]
}

(3) 配置.babelrc文件

{
	"presets": [
		// es6转es5
		"@babel/preset-env"
	]
}

(4) 新建一个html入口文件

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
	<!-- 引入vue文件 -->
    <script src="dist/umd/vue.js"></script>
    <script>
        console.log(Vue);
    </script>
</body>

</html>

(5) 配置package.json

"scripts": {
	"dev": "rollup -c -w"
},

(6) 执行命令

npm run dev

二、响应式原理

1、定义并导出构造函数

// src\index.js

import { initMixin } from "./init";

// Vue构造函数
function Vue(options) {
    // 入口方法,做初始化操作
    this._init(options)
}

// 插件思想:对原型进行扩展
initMixin(Vue)

export default Vue

2、定义初始化混合方法

// src\init.js

import { initState } from "./state"

// 定义init方法,进行扩展Vue的原型
export function initMixin(Vue) {
    // 初始化方法 
    Vue.prototype._init = function(options) {
        // 拿到当前实例
        const vm = this
		// 拿到配置参数挂载到$options
        vm.$options = options

        // 初始化状态:将数据做一个初始化的劫持,数据改变就去更新视图
        initState(vm)

        // 其他初始化方法
        // initEvents
    }
}

3、初始化状态

// src\state.js

/**
 * 初始化状态
 * 顺序:props > methods > data > computed > watch
 */
export function initState(vm) {
    const opts = vm.$options
	// 按照顺序依次拆分初始化
    if (opts.props) {
        initProps(vm)
    }
    if (opts.methods) {
        initMethods(vm)
    }
    if (opts.data) {
        initData(vm)
    }
    if (opts.computed) {
        initComputed(vm)
    }
    if (opts.watch) {
        initWatch(vm)
    }
}

function initProps(vm) {}

function initMethods(vm) {}

function initData(vm) {}

function initComputed(vm) {}

function initWatch(vm) {}

4、初始化数据

// src\state.js

import { observe } from "./observer/index"

// 初始化数据方法
function initData(vm) {
    let data = vm.$options.data;
	// 拿到data属性,如果是函数直接执行,其余放行
    vm._data = data = typeof data === 'function' ? data.call(vm) : data;

    // 数据劫持方案
    // 对象Object.defineProperty
    // 数组 单独处理:拦截可以改变数组的方法进行操作
	// 观测数据
    observe(data);
}

5、对象劫持-递归属性

  • data本身为对象,需要观测
  • data对象里面还有对象,需要递归观测
  • 给data设值的新值也是对象,需要进行递归观测
// src\observer\index.js

/**
 * 数据观测类
 * 使用defineProperty 重新定义属性
 */
class Observer {
    constructor(value) {
        // 判断一个对象是否被观测过,看他有没有__ob__这个属性
        Object.defineProperty(value, '__ob__', {
            enumerable: false, // 不能被枚举,不能被循环出来
            configurable: false,
            value: this // 注入当前的实例对象
        })
		// 对象处理
		this.walk(value); 
    }

    walk(data) {
        // 遍历对象,进行循环观测
        let keys = Object.keys(data);
        keys.forEach(key => {
            defineReactive(data, key, data[key]); // 源码对应 > Vue.util.defineReactive
        })
    }
}

// ES5的双向数据绑定类
// vue2慢的核心原因就是这个方法
// vue2 应用了defineProperty需要一加载的时候 就进行递归操作,所以好性能,如果层次过深也会浪费性能
// 1.性能优化的原则:
// 1) 不要把所有的数据都放在data中,因为所有的数据都会增加get和set
// 2) 不要写数据的时候 层次过深, 尽量扁平化数据 
// 3) 不要频繁获取数据
// 4) 如果数据不需要响应式 可以使用Object.freeze 冻结属性 
function defineReactive(data, key, value) {
    // 如果值是对象进行递归观测
    observe(value);

	// 定义双向绑定,进行get和set的观测 
    Object.defineProperty(data, key, {
        get() {
            console.log('取值');
            return value
        },
        set(newValue) {
            console.log('设值');
			// 值没变化就跳过
            if (newValue == value) return;
            // 如果用户设值的值是对象,需要再次进行递归观测
            observe(newValue);
			// 更新值
            value = newValue;
        }
    })
}

/**
 * 数据观测
 */
export function observe(data) {
    // 对象数据校验:不是对象 或 null 就返回
    if (typeof data !== 'object' || data === null) {
        return data
    }
    // 如果数据被观测过,直接返回,防止重复观测
    if (data.__ob__) {
        return data
    }

    // 数据观测
    new Observer(data);
}

6、数组劫持-重写原型

  • 如果data是数组,需要重写能改变数组的七个方法
  • 如果data数组里面每个item都是对象,那么需要循环遍历,观测每一项item数据
  • 如果给data数组新增的数据也是对象,那么新增的对象也需要进行观测
// src\observer\index.js

import { arrayMethods } from "./array";

// 1、改写观测类
class Observer {
    constructor(value) {
       if (Array.isArray(value)) {
            // 数组处理:函数劫持、切片编程思想
			// 重写push shift pop unshift splice sort reverse
            value.__proto__ = arrayMethods;
            // 观测数组中的对象类型
            this.observeArray(value);
        } else {
            // 对象处理
            this.walk(value);
        }
    }

	// 数组观测方法
    observeArray(value) {
        // 遍历数组的每一项进行观测
        value.forEach(item => {
            observe(item);
        })
    }
}
// src\observer\array.js

// 拿到数组原型上的方法
let oldArrayProtoMethods = Array.prototype

// 原型继承 arrayMethods.__proto__ = oldArrayProtoMethods
export let arrayMethods = Object.create(oldArrayProtoMethods)

let methods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'reverse',
    'sort',
    'splice'
]

// 数组方法重写
methods.forEach(method => {
    arrayMethods[method] = function(...args) {
        console.log('数组调用');
        // this为observer里面的value
        const result = oldArrayProtoMethods[method].apply(this, args);

        let inserted;
        let ob = this.__ob__;

        switch (method) {
            case 'push':
            case 'unshift':
                // 如果追加的内容也是对象,需要再次进行对象劫持
                inserted = args;
                break;
            case 'splice':
                // 截取参数下标为2到末尾:arr.splice(0, 1, {a: 1})
                inserted = args.slice(2);
            default:
                break;
        }

        // 如果给数组新增的值是对象要继续进行观测
        if (inserted) ob.observeArray(inserted)

        return result
    }
})

7、数据代理

  • 将取值全部代理到vm上面 vm.message = vm._data.message
// src\state.js

// 代理方法
function proxy(vm, data, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[data][key]; // vm_data.a
        },
        set(newValue) { // vm.a = 100
            vm[data][key] = newValue; // vm._data.a = 100
        }
    })
}

// 初始化数据
function initData(vm) {
    let data = vm.$options.data;
    vm._data = data = typeof data === 'function' ? data.call(vm) : data;
    // 用代理,从vm取属性,代理到vm_data上
    for (let key in data) {
        proxy(vm, '_data', key)
    }
	// 观测数据
    observe(data);
}

三、模板编译

两种页面挂载方式

// 1. 参数中挂载
new Vue({ el: '#app'})

// 2. 手动挂载
vm.$mount("#app")

0、定义挂载函数-获取模板

  1. 默认先找render方法
  2. 没有render方法会查找template
  3. 没有template会找当前el指定的元素中的内容来进行渲染
// src\init.js

import { compileToFunction } from "./compiler/index"

// 初始化方法 
Vue.prototype._init = function(options) {
	// 拿到当前实例
	const vm = this
	vm.$options = options
	// 初始化状态
	initState(vm)
	// 如果当前有el属性,需要进行模板渲染
	if (vm.$options.el) {
		vm.$mount(vm.$options.el);
	}
}

// 挂载函数
Vue.prototype.$mount = function(el) {
	// 拿到当前实例
	const vm = this;
	const options = vm.$options;
	// 获取dom对象
	el = document.querySelector(el);

	// 1. 如果没有render方法,需要将template转化为render方法
	if (!options.render) {
		// 判断是否配置了模板
		let template = options.template;
		// 如果没有模板但是有el,就获取整个外部HTML
		if (!template && el) {
			template = el.outerHTML;
		}
		// 编译原理:将模板编译成render函数
		const render = compileToFunction(template);
		options.render = render
	}
	// 2. 有render方法
	// 渲染最终用的都是这个render方法

	// 需要挂载这个组件
	// mountComponent(vm, el);
}

1、解析模板-标签和内容

  • ast 抽象语法树,用对象来描述语言本身
  • 虚拟dom,用对象来描述节点
// src\compiler\index.js

import { parseHTML } from "./parse";

export function compileToFunction(template) {
	// 1、将模板转为ast
    let ast = parseHTML(template);
    console.log(ast);
}
// src\compiler\parse.js

// 思路:利用正则匹配字符串,匹配到了就截取字符串放到相应位置,一直截取完成就转化为了ast了

// 模板解析正则
// 匹配标签名,aaa-123aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 匹配命名空间标签 <my:xxx></my:xxx>,捕获的内容是标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 匹配标签开头
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 匹配标签结尾的 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性,三种写法:aaa="aaaa" | aaa = 'aaaa' | aaa = aaa
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配标签结束的 >
const startTagClose = /^\s*(\/?)>/; 
// 匹配双大括号,{{ xxx }}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

// 解析函数
export function parseHTML(html) {

    function start(tagName, attrs) {
        console.log(tagName, attrs, '------开始---');
    }

    function end(tagName) {
        console.log(tagName, '------结束---');
    }

    function chars(text) {
        console.log(text, '-------文本---');
    }

    // 循环解析:只要html不为空字符串就一直解析
    while (html) {
		// 匹配开始|结束标签,尖括号开头
        let textEnd = html.indexOf('<');
        if (textEnd == 0) {
            // 1、处理开始标签:开始标签匹配结果,获得标签名称和属性
            const startTagMatch = parseStartTag();
            if (startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue;
			}
			
            // 2、处理结束标签:匹配结束标签
            const endTagMatch = html.match(endTag);
            if (endTagMatch) {
                advance(endTagMatch[0].length);
                end(endTagMatch[1]);
                continue;
            }
        }

        // 3、处理文本
        let text;
        if (textEnd > 0) {
            // 截取文本
            text = html.substring(0, textEnd);
        }
        if (text) {
            // 处理文本
            advance(text.length);
            chars(text);
        }
    }

    // 字符串进行截取操作,再更新html内容
    function advance(n) {
        html = html.substring(n);
    }

	// 处理开始标签函数
    function parseStartTag() {
        const start = html.match(startTagOpen);
        if (start) {
            //  ["<div", "div", index: 0, input: "<div id="app">...</div>", groups: undefined]
            const match = {
				tagName: start[1], // 标签名
				attrs: [] // 属性
			};
			// 删除开始标签
            advance(start[0].length);

            let end;
            let attr;
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                // 不是结束标签,并且有属性,就进行属性取值
                // [" id="app"", "id", "=", "app", undefined, undefined, index: 0, ...]
                match.attrs.push({
                    name: attr[1], // 属性名称
                    value: attr[3] || attr[4] || attr[5] // 属性值
                });
                // 删除属性
                advance(attr[0].length);
            }
            // > 没有属性就表示为 结束的闭合标签
            if (end) {
                // 删除结束标签
                advance(end[0].length);
                return match;
            }
        }
    }

    return root;
}

2、生成ast语法树

// ast语法树模板

// <div>hello {{name}} <span>world</span></div>
{
	tag: 'div',
	parent: null,
	type: 1,
	attrs: [],
	children: [
		{
			tag: null,
			parent: '父div对象',
			attrs: [],
			text: hello {{name}} 
		}
	]
}
// src\compiler\parse.js

// 开始标签依次存入stack中,在结束标签的时候取出建立父子关系

export function parseHTML(html) {

	let root; // 根节点,也是树根
    let currentParent; // 当前父元素
	let stack = []; // 栈
	const ELEMENT_TYPE = 1; // 元素类型
	const TEXT_TYPE = 3; // 文本类型

	// 创建ast对象
	function createASTElement(tagName, attrs) {
		return {
			tag: tagName, // 标签名
			type: ELEMENT_TYPE, // 元素类型
			children: [], // 孩子列表
			attrs, // 属性集合
			parent: null // 父元素
		}
	}
	
    // 标签是否符合预期
    // <div><span></span></div>
	// 处理开始标签
    function start(tagName, attrs) {
        // 创建一个元素,作为根元素
        let element = createASTElement(tagName, attrs);
        if (!root) {
            root = element;
        }
        // 当前解析标签保存起来
        currentParent = element;
        // 将生产的ast元素放到栈中
        stack.push(element);
    }

    // 在结尾标签处,创建父子关系
    // <div><p><span></span></p></div>  [div, p, span]
    function end(tagName) {
        let element = stack.pop(); // 取出栈中的最后一个
        currentParent = stack[stack.length - 1]; // 倒数第二个是父亲
        if (currentParent) {
            // 闭合时可以知道这个标签的父亲是谁,儿子是谁
            element.parent = currentParent;
            currentParent.children.push(element);
        }
    }

	// 处理文本
    function chars(text) {
		// 去除空格
        text = text.replace(/\s/g, '');
        if (text) {
            currentParent.children.push({
                type: TEXT_TYPE,
                text
            });
        }
	}
	
	return root;
}

3、生成代码

template模板转化为render函数示例

// src\compiler\generate.js

// 编写:
<div id="app" style="color:red">hello {{name}} <span>hello</span></div>

// 结果:
render() {
	return _c('div', {id: 'app', style: {color: 'red'}}, _v('hello'+_s(name)),_c('span',null,_v('hello')))
}

代码生成


const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ xxx }}

// 生成单个儿子节点
function gen(node) {
    // 判断元素还是标签
    if (node.type === 1) {
		// 递归进行元素节点字符串的生成
        return generate(node);
    } else {
        let text = node.text; // 获取文本
        // 如果是普通文本,不带{{}}
        if (!defaultTagRE.test(text)) {
			//_v('hello {{name}} world {{msg}}') => _v('hello' + _s(name))
            return `_v(${JSON.stringify(text)})`;
        }
        // 存放每一段代码
        let tokens = [];
        // 如果正则是全局模式,需要每次使用前置为0
        let lastIndex = defaultTagRE.lastIndex = 0;
        // 每次匹配到的结果
        let match, index;

        while (match = defaultTagRE.exec(text)) {
            index = match.index; // 保存匹配到的索引
            if (index > lastIndex) {
                tokens.push(JSON.stringify(text.slice(lastIndex, index)));
            }
            tokens.push(`_s(${match[1].trim()})`);
            lastIndex = index + match[0].length;
        }
        // 双大括号后面还有字符串
        if (lastIndex < text.length) {
            tokens.push(JSON.stringify(text.slice(lastIndex)))
        }
        return `_v(${tokens.join('+')})`;
    }
}

// 生成儿子节点
function genChildren(el) {
    const children = el.children;
    if (children) {
        // 将所有转化后的儿子用都好拼接起来
        return children.map(child => gen(child)).join(',');
	}
	return false;
}

// 生成属性
function genProps(attrs) {
    let str = '';
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i];
        if (attr.name === 'style') {
			// 如果是样式需要特殊处理下
            let obj = {};
            attr.value.split(';').forEach(item => {
                let [key, value] = item.split(':');
                obj[key] = value;
            })
            attr.value = obj;
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;
    }
    return `{${str.slice(0, -1)}}`;
}

// 语法层面的转义:ast树转化为code字符串代码
export function generate(el) {
    // 儿子的生成
    let children = genChildren(el);

	// 拼接代码:元素和儿子
    let code = `_c('${el.tag}',${
		el.attrs.length ? `${genProps(el.attrs)}` : undefined
	}${
		children ? `,${children}` : ''
	})`;

    return code;
}

let code = generate(ast);

4、生成render函数

// src\compiler\index.js

import { parseHTML } from "./parse";
import { generate } from "./generate";

export function compileToFunctions(template) {
	// 1、模板转为ast
	let ast = parseHTML(template);
	// 2、ast转为code代码字符串
	let code = generate(ast);
	// 3、通过new Function + with的方式:将字符串变成函数
	// 原理:通过with来限制取值范围,后续调用render函数改变this就可以取到结果了
    let render = `with(this){return ${code}}`;
	let renderFn = new Function(render);
	
    return renderFn
}

四、初渲染原理

  1. 先初始化数据,进行观测
  2. 将模板进行编译为render函数
  3. 利用render.call(vm)进行改变this指向,最终产生虚拟DOM
  4. 调用_update()方法,执行patch函数将虚拟DOM转化为真实DOM
  5. 将真实DOM放到页面替换#app

1、在Vue的原型上混入_render和_update方法

// src\index.js

import { lifecycleMixin } from "./lifecycle";
import { renderMixin } from "./vdom/index";

// Vue构造函数
function Vue(options) {
    // console.log(options);
    // 入口方法,做初始化操作
    this._init(options)
}

// 混合生命周期和渲染函数
lifecycleMixin(Vue);
// 混入_render方法
renderMixin(Vue);

2、生成虚拟DOM

  1. 在Vue的原型上定义_render方法
  2. 在Vue的原型上定义很多编译方法_c、_s、_v等
  3. 执行render.call(vm)的时候,传入vue实例,改变this指向
  4. 利用with的原理,最后将包括字符串的函数转化为虚拟dom
// src\vdom\index.js

export function renderMixin(Vue) {
    // 创建虚拟dom-标签元素
    Vue.prototype._c = function() {
        return createElement(...arguments);
    }

    // 处理虚拟dom中双大括号{{}}。如果结果一个对象时,stringify会对这个对象取值
    Vue.prototype._s = function(val) {
        return val == null ? '' : (typeof val == 'object') ? JSON.stringify(val) : val;
    }

    // 创建虚拟dom-文本元素
    Vue.prototype._v = function(text) {
        return createTextVnode(text);
    }

    // 扩展_render方法
    Vue.prototype._render = function() { //_render = render
        const vm = this;
        const render = vm.$options.render;
        // 执行render方法,改变里面this的指向为vm,最后生成虚拟dom
        let vnode = render.call(vm);
        // 返回虚拟dom
        return vnode;
    }
}

// 生成元素节点的虚拟dom对象
function createElement(tag, data = {}, ...children) {
    return vnode(tag, data, data.key, children);
}

// 生成文本节点的虚拟dom对象
function createTextVnode(text) {
    return vnode(undefined, undefined, undefined, undefined, text);
}

// 用来产生虚拟dom的,可以自定义一些属性
function vnode(tag, data, key, children, text) {
    return {
        tag,
        data,
        key,
        children,
        text
    }
}

3、调用渲染函数

// src\init.js

// 挂载函数
Vue.prototype.$mount = function(el) {
	const vm = this;
	const options = vm.$options;
	el = document.querySelector(el);
	
	......

	// 调用渲染函数
	mountComponent(vm, el);
}
// src\lifecycle.js

import { patch } from "./vdom/patch";

// 定义_update方法
export function lifecycleMixin(Vue) {
    Vue.prototype._update = function(vnode) {
        const vm = this;
        // 将虚拟节点渲染成真实节点:用新创建的元素,替换老的vm.$el
        vm.$el = patch(vm.$el, vnode);
    }
}

// 定义渲染函数
export function mountComponent(vm, el) {
    vm.$el = el;
    // 先调用render方法创建虚拟节点,再将虚拟节点渲染到页面上
    vm._update(vm._render());
}

4、生成真实DOM

// src\vdom\patch.js

// 将虚拟节点转化为真实节点
export function patch(oldVnode, vnode) {
    // 产生真实的dom
    let el = createElm(vnode);
    // 获取老的app的父亲-body
    let parentElm = oldVnode.parentNode;
    // 当前真实元素插入到app的后面
    parentElm.insertBefore(el, oldVnode.nextSibling);
    // 删除老的节点
    parentElm.removeChild(oldVnode);
	// 返回新节点进行实时替换
    return el;
}

function createElm(vnode) {
    let { tag, children, key, data, text } = vnode;
    if (typeof tag == 'string') {
        // 创建元素,放到vnode.el上
        vnode.el = document.createElement(tag);

        // 只有元素才有属性
        updateProperties(vnode);

        // 遍历儿子,将儿子渲染后的结果放到父亲中
        children.forEach(child => {
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 创建文本,放到vnode.el上
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

// 处理属性
function updateProperties(vnode) {
    let el = vnode.el; // 当前的真实节点
    let newProps = vnode.data || {}; // 获取当前节点的属性 

    for (let key in newProps) {
        if (key == 'style') { // {color: red}
            // 样式需要遍历添加
            for (let styleName in newProps.style) {
                el.style[styleName] = newProps.style[styleName];
            }
        } else if (key == 'class') {
            // class直接添加
            el.className = el.class;
        } else {
            // 属性就需要利用方法添加,值就是对应的值
            el.setAttribute(key, newProps[key]);
        }
    }
}

五、生命周期的合并

0、生命周期方法的使用

// 1、全局mixin混入使用
Vue.mixin({
	created: function a() {
		console.log('created 1');
	}
})

// 2、在options中以属性方式使用
let vm = new Vue({
	el: '#app',
	created() {
		// 生命周期就是回调函数,先订阅号,后续触发
		console.log('created 3');
	}
})

1、Mixin原理

// src\global-api\index.js

import { mergeOptions } from "../../util";

// 定义全局API
export function initGlobalApi(Vue) {
    Vue.options = {};

	// 定义混入的静态方法
    Vue.mixin = function(mixin) {
        // 合并对象-生命周期
        this.options = mergeOptions(this.options, mixin);
    }
}

2、合并生命周期

// 01vue\util.js

// 定义完整的生命周期数组
export const LIFECYCLE_HOOKS = [
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeUpdate',
    'updated',
    'beforeDestroy',
    'destroyed'
];

// 使用策略模式进行不同方法的调用
const strats = {};
strats.data = function(parentVal, childVal) {
    return childVal;
}
strats.computed = function() {}
strats.watch = function() {}

// 生命周期的合并
function mergeHook(parentVal, childVal) {
    if (childVal) {
        // 如果儿子有值,然后根据父亲是否有值来处理
        if (parentVal) {
            // 爸爸和儿子进行拼接
            return parentVal.concat(childVal);
        } else {
            // 儿子需要转化为数组
            return [childVal];
        }
    } else {
        // 如果只有父亲有值,不合并,直接采用父亲的
        return parentVal;
    }
}

// 定义生命周期的策略方法
LIFECYCLE_HOOKS.forEach(hook => {
    strats[hook] = mergeHook;
})

// 对象合并方法
export function mergeOptions(parent, child) {
    const options = {};

    // 1、处理父亲有,儿子有或没有的情况
    for (let key in parent) {
        mergeField(key);
    }

    // 2、处理儿子有,父亲没有:把儿子多余的属性赋予到父亲上
    for (let key in child) {
        if (!parent.hasOwnProperty(key)) {
            mergeField(key);
        }
    }

    function mergeField(key) {
        // 根据key,采取不同的策略合并
        if (strats[key]) {
            options[key] = strats[key](parent[key], child[key]);
        } else {
            // todo 默认合并
            options[key] = child[key];
        }
    }

    return options;
}

3、定义生命周期的调用函数

// src\lifecycle.js

// 定义生命周期调用方法
export function callHook(vm, hook) {
    // 是数组
    const handlers = vm.$options[hook];
    if (handlers) {
        for (let i = 0; i < handlers.length; i++) {
            handlers[i].call(vm); // 更改生命周期中的this
        }
    }
}

4、在各个节点调用生命周期

// 调用beforeCreate和created
Vue.prototype._init = function(options) {
	// 拿到当前实例
	const vm = this

	// 需要将用户自定义的options和全局的options做合并
	vm.$options = mergeOptions(vm.constructor.options, options);

	callHook(vm, 'beforeCreate');
	initState(vm)
	callHook(vm, 'created');

	// 如果当前有el属性,需要进行模板渲染
	if (vm.$options.el) {
		vm.$mount(vm.$options.el)
	}
}

// 调用beforeMount和mounted
export function mountComponent(vm, el) {
    vm.$el = el;

    callHook(vm, 'beforeMount');
	 // 先调用render方法创建虚拟节点,再将虚拟节点渲染到页面上
    vm._update(vm._render());
    callHook(vm, 'mounted');
}

六、依赖收集

  • 每个属性都要有一个dep,用来收集watcher
  • 每个dep中存放着多个watcher
  • 同一个watcher会被多个dep所记录
  • dep与watcher是多对多的关系

1、定义Dep类

// src\observer\dep.js

// 唯一标识
let id = 0;

class Dep {
    constructor() {
        this.subs = []; // 用于存储watcher
        this.id = id++; // 标识dep的唯一性,用于防止重复取值添加dep
    }
    depend() {
        // watcher也可以存放dep,实现双向记忆,让watcher记住dep的同时,让dep也记住watcher
        Dep.target.addDep(this);
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}

// 静态属性
Dep.target = null;
export function pushTarget(watcher) {
    // 保留watcher
    Dep.target = watcher;
}

export function popTarget() {
    // 将变量删除掉
    Dep.target = null;
}

export default Dep;

2、定义Watcher类

// src\observer\watcher.js

import { pushTarget, popTarget } from "./dep";

let id = 0;
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        this.exprOrFn = exprOrFn; // 渲染函数
        this.cb = cb; // 回调函数
        this.options = options;
        this.id = id++; //watcher的唯一标识
        this.deps = []; // watcher记录有多少dep依赖
        this.depsId = new Set(); // 用于去重dep

        if (typeof exprOrFn == 'function') {
            this.getter = exprOrFn;
        }

        // 默认调用get方法
        this.get();
    }

    addDep(dep) {
        let id = dep.id;
        // 去重
        if (!this.depsId.has(id)) {
            this.deps.push(dep);
            this.depsId.add(id);
            dep.addSub(this);
        }
    }

	// 利用JS的单线程机制,先存储watcher,然后更新,最后删除watcher
    get() {
        // 存储当前watcher实例
        pushTarget(this);
        // 调用exprOrFn,渲染页面 > 取值(执行了get方法)
        this.getter();
        // 渲染完成后,将watcher删掉
        popTarget();
    }

    run() {
        // 渲染逻辑
        this.get();
    }
    update() {
        // 重新渲染
        this.get();
    }
}

export default Watcher;

3、对象依赖收集

// src\observer\index.js

// 每个属性都有一个dep
let dep = new Dep();
// 当页面取值时,说明这个值用来渲染了,将这个watcher和这个属性对应起来

Object.defineProperty(data, key, {
	get() {
		// 如果取值时有watcher
		if (Dep.target) {
			// 让watcher保存dep,并且让dep 保存watcher
			dep.depend();
		}
		return value
	},
	set(newValue) {
		if (newValue == value) return;
		observe(newValue);
		value = newValue;

		// 通知渲染watcher去依赖更新
		dep.notify();
	}
})

4、数组依赖收集

  1. 获取arr的值,会调用get方法,就让当前数组记住渲染watcher
  2. 给所有的对象类型都增加一个dep属性
  3. 当页面对arr取值时,让这个数组的dep记住这个watcher
  4. 当操作push, shift等更新数组的方法时,就找到数组对应的watcher来更新
// src\observer\index.js

// 1、构造函数中添加一个dep
class Observer {
    constructor(value) {
        this.dep = new Dep(); // value = {} / []
		
		......
    }
}

// 2、定义响应式中进行添加依赖
function defineReactive(data, key, value) {
    // 获取到数组对应的dep
    let childDep = observe(value);

    // 每个属性都有一个dep
    let dep = new Dep();
    // 当页面取值时,说明这个值用来渲染了,将这个watcher和这个属性对应起来

    Object.defineProperty(data, key, {
        get() {
           // 如果取值时有watcher
			if (Dep.target) {
				// 让watcher保存dep,并且让dep 保存watcher
				dep.depend();

                // 可能是数组,可能是对象
                if (childDep) {
                    // 默认给数组增加了一个dep属性,当对数组这个对象取值的时候
                    childDep.dep.depend(); // 数组存起来了渲染watcher
                }
            }
            return value
        },
        set(newValue) {
            if (newValue == value) return;
            observe(newValue);
            value = newValue;

            // 通知渲染watcher去依赖更新
            dep.notify();
        }
    })
}

// src\observer\array.js
// 3、重写方法中进行调用更新
arrayMethods[method] = function (...args) {
    	// ...
        ob.dep.notify()
        return result;
}

5、调用渲染函数进行依赖更新

渲染流程

  1. 先把这个渲染watcher放到Dep.target属性上
  2. 开始渲染,取值会调用get方法,需要让这个属性的dep 存储当前的watcher
  3. 页面上所属需要的属性都会将这个watcher存在自己的dep中
  4. 等会属性更新了,就重新调用渲染逻辑,通知自己存储的watcher来更新
// src\lifecycle.js

export function mountComponent(vm, el) {
    vm.$el = el;
    callHook(vm, 'beforeMount');

	// 定义更新函数
    let updateComponent = () => {
        vm._update(vm._render());
    };
    // 初始化就会创建watcher
    new Watcher(vm, updateComponent, () => {
        callHook(vm, 'updated');
    }, true);

    callHook(vm, 'mounted');
}

七、异步更新与nextTick

0、index.html中调用

let vm = new Vue();

 setTimeout(() => {
	vm.arr.push(123);
	vm.arr.push(123);
	vm.arr.push(123);

	console.log(vm.$el.innerHTML);
	vm.$nextTick(() => {
		console.log(vm.$el.innerHTML);
	});
}, 2000);

1、Vue原型找那个扩展nextTick方法

// src\state.js

import { nextTick } from "./util"

export function stateMixin(Vue) {
    Vue.prototype.$nextTick = function(cb) {
        nextTick(cb);
    }
}

2、实现队列机制

// src\observer\watcher.js

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
       ......
    }
	.....

    update() {
        // 这里不能每次都调用get方法,get方法会重新渲染页面,需要用队列实现多次调用只刷新一次
        queueWatcher(this);
    }
}

// 实现队列

// src\observer\watcher.js

import { nextTick } from "../util";

// 将需要批量更新的watcher存到一个队列中,稍后让watcher执行
let queue = [];
let has = {}; // 用于去重
let pending = false; // 防抖

function flushSchedulerQueue() {
    queue.forEach(watcher => {
        watcher.run();
        watcher.cb();
    });
    queue = []; // 清空watcher队列为了下次使用
    has = {}; // 清空标识的id
    pending = false;
}

function queueWatcher(watcher) {
    // 对watcher去重
    const id = watcher.id;
    if (has[id] == null) {
        // 将watcher存到队列中
        queue.push(watcher);
        has[id] = true;

        // 异步更新,等到所有同步代码执行完毕后再执行
        if (!pending) {
			// 内部调用
            nextTick(flushSchedulerQueue);
            pending = true;
        }

    }
    // console.log(watcher.id);
}

export default Watcher;

3、nextTick原理

Promise > MutationObserver > setImmediate > setTimeout

// src\util.js

const callbacks = [];
let pending = false;
let timerFunc;

function flushCallbacks() {
    // 让nextTick中传入的方法依次执行
    while (callbacks.length) {
        let cb = callbacks.pop();
        cb();
    }
    // 标识已经执行完毕
    pending = false;
}

if (Promise) {
    timerFunc = () => {
        // 异步处理更新
        Promise.resolve().then(flushCallbacks);
    }
} else if (MutationObserver) {
    // 可以监控dom变化,监控完毕后是异步更新
    let observe = new MutationObserver(flushCallbacks);

	/**
	* 思路:
	* 1、创建一个文本节点
	* 2、监控这个文本节点
	* 3、当文本节点里面的字符变化,就异步调用flushCallbacks进行更新操作
	*/

    // 先创建一个文本节点
    let textNode = document.createTextNode(1);
    // 观测文本节点中的内容
    observe.observe(textNode, { characterData: true });
    timerFunc = () => {
        // 文本内容更新为2,触发异步调用flushCallbacks
        textNode.textContent = 2;
    }
} else if (setImmediate) {
    // ie浏览器里面的api,性能比setTimeout要好些
    timerFunc = () => {
        setImmediate(flushCallbacks);
    }
} else {
    timerFunc = () => {
        setTimeout(flushCallbacks);
    }
}

// 内部会调用,用户也会调用,但是异步只需要一次
export function nextTick(cb) {
    callbacks.push(cb);
    // Vue3里面的nextTick原理就是Promise.resolve().then() 没有兼容性处理
    if (!pending) {
        timerFunc();
        pending = true;
    }
}

八、$watch的原理

0、使用方式

// 第一种
let vm = new Vue();
vm.$watch(function (newVal, oldVal) {
    console.log(newVal, oldVal);
});

vm.$watch('a.b.c', function (newVal, oldVal) {
  console.log(newVal, oldVal);
})

// 第二种
let vm = new Vue({
	el: '#app',
	data: {
		a: {a: {a: 1}},
		b: 2
	},
	methods: {
	    cc() {
	        console.log('method cc');
	    }
	},
	watch: {
		// 1. key value
		'a.a.a': {
			handler(newValue, oldValue) {
				// 对象没有老值,都是新
				console.log(newValue, oldValue);
			},
			immediate: true // 可选
		},
		// 2. 写成key和数组的方式
		'b': [
		    (newValue, oldValue) => {
		        console.log(newValue);
		    },
		    (newValue, oldValue) => {
		        console.log(newValue);
		    }
		],
		// 3、监控当前实例上的方法
		'c': 'cc',
		// 4、handler的写法
		'd': {
		    handler() {
		        console.log('ddd');
		    }
		}
	}
});

1、扩展$watch方法

// src\state.js

export function initState(vm) {
    const opts = vm.$options
	
	// ......

    if (opts.watch) {
        initWatch(vm)
    }
}

function initProps(vm) {}

function initMethods(vm) {}

// 初始化数据
function initData(vm) {}

function initComputed(vm) {}

// 初始化watch
function initWatch(vm) {
    let watch = vm.$options.watch;
    for (let key in watch) {
        // handler 可能是数组、字符串、对象、函数
        const handler = watch[key];
        if (Array.isArray(handler)) {
            // 数组
            handler.forEach(handle => {
                createWatcher(vm, key, handle);
            })
        } else {
            // 字符串、对象、函数
            createWatcher(vm, key, handler);
        }
    }
}

// 创建watcher:options 用来标识是用户watcher
function createWatcher(vm, exprOrFn, handler, options) {
    if (typeof handler == 'object') {
        options = handler;
        handler = handler.handler; // 是一个函数
    }
    if (typeof handler == 'string') {
        handler = vm[handler]; // 将实例的方法作为handler
    }
    // key  handler 用户传入的选项
    return vm.$watch(exprOrFn, handler, options);
}

export function stateMixin(Vue) {
    Vue.prototype.$nextTick = function(cb) {
        nextTick(cb);
    }

	// 在Vue的原型上面挂载$watch方法
    Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
        // 数据应该依赖这个watcher,数据变化后应该让watcher从新执行,user表示为自定义的watcher
        let watcher = new Watcher(this, exprOrFn, cb, {...options, user: true });
        if (options.immediate) {
            // 如果是immediate,立刻执行更新
            cb();
        }
    }
}

2、watcher类的getter方法改写

// src\observer\watcher.js

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
		// .....
		
        this.user = options.user; // 用户watcher
        this.isWatcher = typeof options === 'boolean'; // 标识是渲染watcher

        if (typeof exprOrFn == 'function') {
            this.getter = exprOrFn;
        } else {
            // exprOrFn 可能传递过来的是一个字符串a
            this.getter = function() {
                // 当去当前实例上取值时,才会触发依赖收集
                let path = exprOrFn.split('.'); // ['a', 'a', 'a']
                let obj = vm;
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]; // vm.a.a.a
                }
                return obj;
            }
        }

        // 默认会先调用一次get方法,进行取值,将结果保留下来
        this.value = this.get();
    }

    addDep(dep) {}

    get() {
        // 当前watcher实例
        pushTarget(this);
        // 调用exprOrFn,渲染页面 > 取值(执行了get方法)
        let result = this.getter();
        // 渲染完成后,将watcher删掉
        popTarget();
        return result;
    }

    run() {
        // 渲染逻辑
        let newValue = this.get();
        let oldValue = this.value;
        this.value = newValue; // 更新老值
        if (this.user) {
			// 调用cb方法
            this.cb.call(this.vm, newValue, oldValue);
        }
    }
    update() {}
}

export default Watcher;

九、computed原理

0、使用方式

 let vm = new Vue({
	el: '#app',
	data: {
		firstName: '张',
		lastName: '三'
	},
	computed: {
		// 内部使用了defineProperty,内部有一个变量dirty
		// 第一种: 函数方式-常用的方式
		fullName() {
			// this.firstName和this.lastName在求值时,会记住当前计算属性的watcher
			return this.firstName + this.lastName
		},
		// 第二种:对象方式-几乎不用
		fullName2: {
		    get() {},
		    set() {}
		}
	}
});

1、初始化方法

// src\state.js

// 实现三大步骤 
// 1. 需要watcher
// 2. 需要defineProperty
// 3. 需要dirty

// 初始化计算属性
function initComputed(vm) {
	// 获取计算属性对象
    let computed = vm.$options.computed;
    // 用来存放计算属性的watcher
    const watchers = vm._computedWatchers = {};

    for (let key in computed) {
        // 取出对应的值
        const userDef = computed[key];
        // 判断是函数还是对象,如果是对象就取get方法,最终得到一个getter函数
		const getter = typeof userDef == 'function' ? userDef : userDef.get;
		// 给每一个计算属性的对象key加一个watcher进行监听
        watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true });
        // 给每一个计算属性的对象key加一个数据观测
        defineComputed(vm, key, userDef);
    }
}

function defineComputed(target, key, userDef) {
    const sharePropertyDefinition = {};
    if (typeof userDef == 'function') {
        sharePropertyDefinition.get = createComputedGetter(key, userDef);
    } else {
        sharePropertyDefinition.get = createComputedGetter(key, userDef.get);
        sharePropertyDefinition.set = userDef.set;
    }

    Object.defineProperty(target, key, sharePropertyDefinition);
}

// 高阶函数做缓存
function createComputedGetter(key) {
    // 包装的计算属性的方法,每次取值会调用此方法
    return function() {
        // 拿到属性对应的watcher
        const watcher = this._computedWatchers[key];
        if (watcher) {
            if (watcher.dirty) {
                // 默认是脏的
                watcher.evaluate();
            }
            // 还有渲染watcher,也需要收集起来
            if (Dep.target) {
                watcher.depend();
            }

            // 默认返回watcher上的值
            return watcher.value;
        }
    }
}

2、watcher中添加方法

// src\observer\watcher.js

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        // ...
        this.lazy = options.lazy; // 如果watcher上有lazy属性,说明是一个计算属性
        this.dirty = this.lazy; // dirty表示取值时是否执行用户提供的方法

		// ...

        // 默认会先调用一次get方法,进行取值,将结果保留下来
        this.value = this.lazy ? void 0 : this.get();
    }

    addDep(dep) { }

    get() { }

	run() { }
	
    update() {
        if (this.lazy) {
            // 计算属性,页面重新渲染获取最新的值
            this.dirty = true;
        } else {
            // 这里不能每次都调用get方法,get方法会重新渲染页面
            queueWatcher(this);
        }
    }

    evaluate() {
        this.value = this.get();
        // 取过一次值之后,就标识为已经取过值了
        this.dirty = false;
    }

    depend() {
        // 计算属性watcher会存储dep
        // 通过watcher找到对应的所有dep,也让所有的dep记住这个watcher
        let i = this.deps.length;
        while (i--) {
            // 让dep去存储渲染watcher
            this.deps[i].depend();
        }
	}
}

3、dep中添加收集

// src\observer\dep.js

// 静态属性
Dep.target = null;
let stack = [];
export function pushTarget(watcher) {
    // 保留watcher
    Dep.target = watcher;
    stack.push(watcher); // 渲染watcher、其他watcher
}

export function popTarget() {
    // 将变量删除掉
    // Dep.target = null;
    stack.pop();
    Dep.target = stack[stack.length - 1];
}

十、Diff算法

0、构造数据

import { compileToFunction } from './compiler/index';
import { createElm, patch } from './vdom/patch';

// 1. 创建第一个虚拟节点
let vm1 = new Vue({ data: { name: 'test1' } });
let render1 = compileToFunction(
	`<div>
		<li style="background:red;" key="A">A</li>
		<li style="background:yellow;" key="B">B</li>
		<li style="background:pink;" key="C">C</li>
		<li style="background:green;" key="D">D</li>
		<li style="background:green;" key="F">F</li>
	</div>`
);
let oldVnode = render1.call(vm1);

// 2. 创建第二个虚拟节点
let vm2 = new Vue({ data: { name: 'test2' } });
let render2 = compileToFunction(
	`<div>
		<li style="background:green;" key="M">M</li>
		<li style="background:pink;" key="B">B</li>
		<li style="background:yellow;" key="A">A</li>
		<li style="background:purple;" key="Q">Q</li>
	</div>`
);
let newVnode = render2.call(vm2);

// 3. 通过第一个虚拟节点做首次渲染
let el = createElm(oldVnode)
document.body.appendChild(el);

// 4. 调用patch方法进行对比操作
// 传入新的、老的虚拟节点, 然后用新的虚拟节点对比老的虚拟节点,找到差异,去更新老的dom元素
setTimeout(() => {
    patch(oldVnode, newVnode);
}, 3000);

1、具体思路

  • 通过同层的树节点进⾏比较,⽽非对树进行逐层搜索遍历的⽅式,所以时间复杂度只有O(n),是⼀种相当高效的算法
  • 同层三件事:增删改,具体规则是:
    1. 新节点(new Vnode)不存在就删
    2. 老节点(old Vnode)不存在就增
    3. 都存在就比较类型,类型不同直接替换、类型相同执行更新

Vue

2、基本比对

(1) 比对标签

// 1. 比较两个元素的标签,标签不一样直接替换掉
if (oldVnode.tag !== vnode.tag) {
	// 新的标签替换标签
	return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}

// 2. 如果标签一样,而里面的文本内容不一致,例如:<div>1</div> <div>2</div>
if (!oldVnode.tag) {
	// 因为文本节点的虚拟节点tag 都是undefined,所以需要!oldVnode.tag来判断
	// 两个文本不一致,直接用新的替换老的
	if (oldVnode.text !== vnode.text) {
		return oldVnode.el.textContent = vnode.text;
	}
}

(2) 比对属性

// 3. 标签一样,并且需要开始比对标签的属性和儿子
// 第一步:直接复用节点
let el = vnode.el = oldVnode.el;
// 第二步:新老属性做对比,然后更新属性,用新的虚拟节点的属性和老的比较,去更新节点
updateProperties(vnode, oldVnode.data);

(3) 比对子元素

// 4. 比较儿子的几种情况
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];

if (oldChildren.length > 0 && newChildren.length > 0) {
	// (3) 老的有儿子,新的也有儿子,diff算法
	updateChildren(oldChildren, newChildren, el);
} else if (oldChildren.length > 0) {
	// (1) 老的有儿子,新的没有儿子
	el.innerHTML = '';
} else if (newChildren.length > 0) {
	// (2) 老的没有儿子。新的有儿子
	for (let i = 0; i < newChildren.length; i++) {
		let child = newChildren[i];
		el.appendChild(createElm(child));
	}
}

3、优化策略

(1) diff算法

  • 采用双指针操作:一个循环,同时循环老的和新的,哪个先结束,循环就停止,将多余的删除或者添加进去
  • 比对策略:
    1. 老的头部和新的头部比对
    2. 老的尾部和新的尾部比对
    3. 老的头部和新的尾部比对
    4. 老的尾部和新的头部比对
    5. 儿子直接没关系进行暴力比对

Vue

(2) 代码实现

// src\vdom\patch.js

// 判断是否为相同的虚拟节点
function isSameVnode(oldVnode, newVnode) {
    return oldVnode.tag == newVnode.tag && oldVnode.key == newVnode.key;
}

// 比对儿子
function updateChildren(oldChildren, newChildren, parent) {
    // vue2 采用双指针操作
    // 一个循环,同时循环老的和新的,那个新结束,循环就停止,将多余的删除或者添加进去

    // 老节点
    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]; // 结束节点

    function makeIndexByKey(children) {
        let map = {};
        children.forEach((item, index) => {
            if (item.key) {
                map[item.key] = index; // {A:0, B:1, C:2, D:3}
            }
        });
        return map;
    }
    let map = makeIndexByKey(oldChildren);

    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        if (!oldStartVnode) {
            // 如果指针指向null,需要跳过这些节点
            // 从左往右遍历的情况
            oldStartVnode = oldChildren[++oldStartIndex];
        } else if (!oldEndVnode) {
            // 如果指针指向null,需要跳过这些节点
            // 从右往左遍历的情况
            oldEndVnode = oldChildren[--oldEndIndex];
        } else if (isSameVnode(oldStartVnode, newStartVnode)) {
            // 1. 老的头部和新的头部比较
            // 从左往右开始比对,如果两个是同一个元素,比对儿子,更新属性和再去更新子节点
            patch(oldStartVnode, newStartVnode);
            // 向后移动指针
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
        } else if (isSameVnode(oldEndVnode, newEndVnode)) {
            // 2. 老的尾部和新的尾部比较
            // 从右往左开始比对,如果两个是同一个元素,比对儿子,更新属性和再去更新子节点
            patch(oldEndVnode, newEndVnode);
            // 向前移动指针
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
        } else if (isSameVnode(oldStartVnode, newEndVnode)) {
            // 3. 老的头部和新的尾部比较
            patch(oldStartVnode, newEndVnode);
            // 将老的当前元素插入到尾部的下一个元素的前面
            parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
            // 移动指针:老的开始指针从左往右移动(向后移动),新的尾部指针从右往左移动(往前移动)
            oldStartVnode = oldChildren[++oldStartIndex];
            newEndVnode = newChildren[--newEndIndex];
        } else if (isSameVnode(oldEndVnode, newStartVnode)) {
            // 4. 老的尾部和新的头部比较
            patch(oldEndVnode, newStartVnode);
            // 将当前元素插入到尾部的下一个元素的前面
            parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
            // 移动指针:老的尾部指针从右往左移动(往前移动),新的开始指针从左往右移动(向后移动)
            oldEndVnode = oldChildren[--oldEndIndex];
            newStartVnode = newChildren[++newStartIndex];
        } else {
            // 5. 儿子之前没关系:暴力比对
            // 拿到新的开头的虚拟节点的key,去老的中找
            let moveIndex = map[newStartVnode.key];
            if (moveIndex == undefined) {
                // 没有复用的key,不需要移动老节点,只需要将新的节点插入到老节点的开头就行了
                parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
            } else {
                // 有复用的key,需要先移动老节点,然后再将老节点原来的索引位置置为null,方便最后删除
                let moveVnode = oldChildren[moveIndex];
                oldChildren[moveIndex] = null;
                parent.insertBefore(moveVnode.el, oldStartVnode.el);
                // 可能老节点和新节点的属性和儿子不一样,需要比较属性和儿子
                patch(moveVnode, newStartVnode);
            }
            // 往后移动指针,用新的不停地去老的里面找 
            newStartVnode = newChildren[++newStartIndex];
        }

        // 反转节点,头部移动到尾部,尾部移动到头部
        // 为什么要有key,不能用index作为key?
        // key没变,元素复用,但是内容发生了变化
    }

    // 新节点中多余的元素添加到父亲中,从新节点的结束指针开始到末尾
    if (newStartIndex <= newEndIndex) {
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            // 将多余的元素插入进去,可能是向前添加,也可能向后添加
            // parent.appendChild(createElm(newChildren[i]));
            // 向后插入:ele = null
            // 向前插入:ele就是当前向谁前面插入
            let ele = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
            parent.insertBefore(createElm(newChildren[i]), ele);
        }
    }

    // 老的节点还没有处理的,说明这些老节点是不需要的节点
    // 如果这里面有null,说明这个节点已经被处理过了,就跳过
    if (oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            let child = oldChildren[i];
            if (child != undefined) {
                parent.removeChild(child.el);
            }
        }
    }
}
// src\vdom\patch.js

// 创建节点
export function createElm(vnode) {
    let { tag, children, key, data, text } = vnode;
    if (typeof tag == 'string') {
        // 创建元素,放到vnode.el上
        vnode.el = document.createElement(tag);

        // 只有元素才有属性
        updateProperties(vnode);

        // 遍历儿子,将儿子渲染后的结果放到父亲中
        children.forEach(child => {
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 创建文本,放到vnode.el上
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

// 更新属性的方法
function updateProperties(vnode, oldProps = {}) {
    // 当前的真实节点
    let el = vnode.el;
    // 获取当前节点的属性(新的属性)
    let newProps = vnode.data || {};

    // 老的有,新的没有,需要删除属性
    for (let key in oldProps) {
        if (!newProps[key]) {
            //  移除真实dom的属性
            el.removeAttribute(key);
        }
    }

    // 样式处理,老的 style={color:red} 新的 style={background:Red}
    let newStyle = newProps.style || {};
    let oldStyle = oldProps.style || {};
    // 老的样式中有,新的没有,删除老的样式
    for (let key in oldStyle) {
        if (!newStyle[key]) {
            el.style[key] = '';
        }
    }

    // 新的有,直接用新的去更新
    for (let key in newProps) {
        if (key == 'style') { // {color: red}
            // 样式需要遍历添加
            for (let styleName in newProps.style) {
                el.style[styleName] = newProps.style[styleName];
            }
        } else if (key == 'class') {
            // class直接添加
            el.className = newProps[key];
        } else {
            // 属性就需要利用方法添加,值就是对应的值
            el.setAttribute(key, newProps[key]);
        }
    }
}

4、更新操作

// src\lifecycle.js

export function lifecycleMixin(Vue) {
    Vue.prototype._update = function(vnode) {
        const vm = this;

        // 需要区分首次渲染还是更新
        const prevVnode = vm._vnode;
        if (!prevVnode) {
            // 用新创建的元素,替换老的vm.$el
            vm.$el = patch(vm.$el, vnode);
        } else {
            // 上一次的vnode和本次做对比
            vm.$el = patch(prevVnode, vnode);
        }
        // 保存上一次的vnode
        vm._vnode = vnode;
    }
}

十 组件原理

0、使用

  • 全局组件
Vue.component('my-button', {
	template:'<button>+</button>'
});
  • 局部组件
let vm = new Vue({
	el: '#app',
	components:{
		aa:{
			template:'<div>hello </div>'
		}
	},
	data: { },
});

1、组件的作用

  • 实现复用
  • 方便维护
  • 合理拆分组件可以提高性能:因为每个组件都有一个Watcher,当组件更新的时候,越小的组件,vdom越小,就能减少比对,提高性能

2、组件初始化

  • 通过Vue.component注册全局组件,之后可以在模板中进行使用
  • Vue.component内部会调用Vue.extend方法,将定义挂载到Vue.options.components上
  • Vue.extend方法就是创建出一个子类,继承于Vue,并返回这个类
// src\global-api\index.js

export function initGlobalApi(Vue) {
    Vue.options = {};
    Vue.mixin = function(mixin) {
        // 合并对象-生命周期
        this.options = mergeOptions(this.options, mixin);
	}

	// 核心就是创造一个子类继承我们的父类
	let cid = 0;
	Vue.extend = function(extendOptions) {
		const Super = this; // this > vue的构造函数Vue
		// 定义子类的构造函数
		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);
		Sub.components = Super.components;
		// 返回子类
		return Sub;
	}
	
	// 全局组件方法
	Vue.options._base = Vue; // 保留Vue的构造函数
	Vue.options.components = {};
	Vue.component = function(id, definition) {
		// 默认以name属性为准
		definition.name = definition.name || id;
		// 根据当前组件对象 生成了一个子类的构造函数,用于指向父类,用的时候得new definition().$mount()
		definition = this.options._base.extend(definition);
		// 缓存到options中
		Vue.options.components[id] = definition;
	}
}
  • 因为在初始化_init方法中会合并属性,需要加一个合并策略
// src\util.js
// 组件的合并策略-就近策略:当同时存在全局组件和局部组件的时候,以局部组件为主,没有再用全局组件
strats.components = function(parentVal, childVal) {
	// 将全局组件放到原型链上,沿着原型链进行查找
	const res = Object.create(parentVal);
	if (childVal) {
		for(let key in childVal){
            res[key] = childVal[key];
        }
	}
	return res;
}

2、组件转虚拟dom

  • vdom中的_c方法中调用createElement创建元素方法中进去区分组件还是原生标签
  • 给组件的vdom标记属性,存放构造函数和插槽
// src\vdom\index.js

// 生成元素节点的虚拟dom对象
function createElement(vm, tag, data = {}, ...children) {
	// 判断是否为原生标签
	if (isReservedTag(tag)) {
		// 原生标签直接创建虚拟节点
		return vnode(tag, data, data.key, children);
	} else {
		// 如果是组件,在产生虚拟节点时需要把组件的构造函数传入 new Ctor().$mount()
		let Ctor = vm.$options.components[tag];
		return createComponent(vm, tag, data, data.key, children, Ctor);
	}
}

// 生成组件
function createComponent(vm, tag, data, key, children, Ctor) {
	const baseCtor = vm.$options._base;
	// 如果组件是一个对象,需要通过Vue.extend来创建一个子组件构造函数
	if (typeof Ctor === 'object') {
		Ctor = baseCtor.extend(Ctor);
	}
	// 给子组件增加生命周期
	data.hook = {
		// 组件初始化会调用init方法,然后挂载
		init(vnode) {
			let child = vnode.componentInstance = new Ctor({});
			child.$mount();
		}
	}
	return vnode(`vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined, 
	{ Ctor, children });
}

// 用来产生虚拟dom的,可以自定义一些属性
function vnode(tag, data, key, children, text, componentOptions) {
    return {
        tag,
        data,
        key,
        children,
		text,
		// 组件的虚拟节点多一个属性,用来保存当前组件的构造函数和他的插槽
		componentOptions
    }
}

3、组件转真实dom

export function patch(oldVnode, vnode) {
	// 组件初始化的时候 oldvnode为undefined
	if(!oldVnode){ // 如果是组件这个oldVnode是个undefined
        return createElm(vnode); // vnode是组件中的内容
	}
	// ...
}

function createComponent(vnode) {
	// 调用hook中init方法 
	let i = vnode.data;
	// 拿到hook中的init方法,然后调用,内部会new 子组件,然后挂载到vnode上
	if ((i = i.hook) && (i = i.init)) {
		i(vnode);
	}
	if (vnode.componentInstance) {
		return true;
	}
}

export function createElm(vnode) {
    let { tag, children, key, data, text } = vnode;
    if (typeof tag == 'string') {

		// 如果是组件,组件渲染后的结果 放到当前组件的实例上 vm.$el
		if (createComponent(vnode)) {
			// 返回组件对应的dom元素
			return vnode.componentInstance.$el;
		}

        // 创建元素,放到vnode.el上
        vnode.el = document.createElement(tag);

        // 只有元素才有属性
        updateProperties(vnode);

        // 遍历儿子,将儿子渲染后的结果放到父亲中
        children.forEach(child => {
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 创建文本,放到vnode.el上
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

4、组件的渲染流程

  1. 调用Vue.component,注册全局组件
  2. 内部用的是Vue.extend 就是产生一个子类来继承父类
  3. 等会创建子类实例时会调用父类的_init方法,再$mount
  4. 组件的初始化就是 new 这个组件的构造函数并且调用$mount方法
  5. 创建虚拟节点 根据标签筛选出对应的组件,然后生成组件的虚拟节点 componentOptions里面包含Ctor,children
  6. 组件创建真实dom时 (先渲染的是父组件) 遇到是组件的虚拟节点时,去调用init方法,让组件初始化并挂载, 组件的$mount无参数会把渲染后的dom放到 vm.$el上 =》 vnode.componentInstance中,这样渲染时就 获取这个对象的$el属性来渲染