Vue 源码学习

Vue 源码学习

Vue 源码 – mustache 模版引擎

1. 什么是模版引擎

  • 数据 --> 视图
    • 纯 DOM 法
    • 数组 join 法
    • ES6 反引号
    • 模版引擎
  • 模版引擎是将数据变为视图最优雅的解决方案
// 数据
[
	{"name":"张三", "age":18},
	{"name":"李四", "age":20},
	{"name":"王五", "age":22}
]

// 视图
<ul>
	<li>
		<div class="name">姓名:张三</div>
		<div class="age">年龄:18</div>
	</li>
	<li>
		<div class="name">姓名:李四</div>
		<div class="age">年龄:20</div>
	</li>
	<li>
		<div class="name">姓名:王五</div>
		<div class="age">年龄:22</div>
	</li>
</ul>

// v-for

2. mustache 基本使用

2.1 mustache 库简介

  • 官方 git: https://github.com/janl/mustache.js
  • mustache 是最早的模板引擎库,它的底层实现原理在当时是非常有创造性的、轰动性的,为后续模板引擎的发展提供了崭新的思路

2.2 mustache 库基本使用

2.2.1 引入 mustache 库
2.2.2 循环对象数组
  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<ul>
			// 模版语法
		    {{#arr}} // 从#开始循环arr这个数组,name、sex、age都是arr数组里的值
		        <li>   
		            <div class="name">姓名:{{name}}</div>
					<div class="age">年龄:{{age}}</div>
		        </li>
		    {{/arr}}
		</ul>
	`

	var data = {
		arr: [
			{"name":"张三", "age":18},
			{"name":"李四", "age":20},
			{"name":"王五", "age":22}
		]
	}
	
	/*
	  Mustance 全局变量
	  render 方法(层递)
	  templateStr 模版字符串
	  data 数据
	*/
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>
2.2.3 不循环
  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<h1>我是{{name}},今年{{age}}岁</h1>
	`

	var data = {
		name: '张三',
        age: 18
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>
2.2.4 循环简单数组
  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<ul>
            {{#arr}}
                <li>{{.}}</li>    
            {{/arr}}
        </ul>
	`

	var data = {
		arr: ['A', 'B', 'C']
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>
2.2.5 数组嵌套
  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<ul>
            {{#arr}}
                <li>
                    {{name}}的爱好是:
                    <ol>
                        {{#hobbies}} 
                            <li>{{.}}</li>
                        {{/hobbies}}
                    </ol>
                </li>    
            {{/arr}}
        </ul>
	`

	var data = {
		arr: [
        	{'name': '小明', 'age': 12, 'hobbies': ['游泳', '羽毛球']},
            {'name': '小红', 'age': 11, 'hobbies': ['编程', '写作文', '看报纸']},
            {'name': '小强', 'age': 13, 'hobbies': ['打台球']},
        ]
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>
2.2.6 布尔值
  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		{{#m}}
            <h1>你好</h1>
        {{/m}}
	`

	var data = {
		m: false
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>

3. mustache 底层核心原理

3.1 正则表达式思路

  • 在简单情况下,可以用正则表达式实现
  • 但当情况复杂时,正则表达式的思路就不行了
var templateStr = `
	<h1>我是{{name}},今年{{age}}岁</h1>
`

var data = {
	name: '张三',
    age: 18
}

// 最简单的模板引擎的实现机理,利用的是正则表达式中的replace()方法。
// replace()的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,就是$1
// 结合data对象,即可进行智能的替换
function render(templateStr, data) {
    return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, $1) {
        return data[$1];
    });
}

var result = render(templateStr, data);
console.log(result);

3.2 mustache 库的原理

请添加图片描述

  • Mustache 库底层重点做了两件事
    • 将模版字符串编译为 tokens 形式
    • 将 tokens 结合数据,解析为 DOM 字符串
3.2.1 什么是 tokens
  • tokens 是一个 JS 的嵌套数组
// 模版字符串
<h1>我是{{name}},今年{{age}}岁</h1>

// tokens
[
	["text", "<h1>我是"],
	["name", "name"],
	["text", ",今年"],
	["name", "age"],
	["text", "岁</h1>"]
]
3.2.2 循环情况下的 tokens
  • 当模版字符串中有循环存在时,它将被编译为嵌套更深的 tokens
// 模版字符串
<div>
	<ul>
		{{#arr}}
		<li>{{.}}</li>
		{{/arr}}
	</ul>
</div>

// tokens
[
	["text", "<div><ul>"],
	["#", "arr", [
		["text", "<li>"],
		["name", "."],
		["text", "</li>"]
	]],
	["text", "<ul></div>"]
]
3.2.3 双重循环情况下的 tokens
  • 循环是双重的,那么 tokens 会更深一层
// 模版字符串
<div>
	<ol>
	{{#students}}
	<li>
		学生
		<ol>
			{{#hobbies}}
			<li>{{.}}</li>
			{{/hobbies}}
		</ol>
	</li>
	{{/students}}
	</ol>
</div>

// tokens
[ 
	["text", "<div><ol>"],
	["#", "students", [ 
		["text", "<li>学生"],
		["name", "name"],
		["text", "的爱好是<ol>"],
		["#", "hobbies", [ 
			["text", "<li>"],
			["name", "."],
			["text", "</li>"],
		]],
		["text", "</ol></li>"],
	]],
	["text", "</ol></div>"] 
]

4. 手写 mustache 库

4.1 构建项目
4.1.1 使用 webpack 和 webpack-dev-server 构建
  • Mustache 官方库使用 rollup 进行模块化打包
  • 但是我这里使用 webpack ( webpack-dev-server ) 进行模块化打包
    • webpack 能更方便的在浏览器中实时调用程序 ( 相比于 nodejs 控制台,浏览器控制台更好用 )
  • 生成库是 UMD 的,这意味着它可以同时在 nodejs 环境中使用,也可以在浏览器环境中使用
4.1.2 webpack.config.js 文件
const path = require('path');

module.exports = {
    // 开发模式
	mode: 'development',
	// 入口文件
	entry: './index.js',
	// 打包文件 
	output: {
		filename: 'bundle.js'
	},
	// 配置 webpack-dev-server
	devServer: {
		// 静态文件根目录 
		contentBase: path.join( dirname, "www"), 
		compress: false, // 不压缩
		port: 8080, // 端口号
		// 虚拟打包的路径,bundle.js文件没有真正的生成
		publicPath: "/xuni/"
	}
};
4.2 整体结构
  • index.html
<body>
    <div id="container"></div>

    <script src="/xuni/bundle.js"></script>

    <script>
        // 模板字符串
        var templateStr = `
            <div>
                <ul>
                    {{#students}}
                    <li class="myli">
                        学生{{name}}的爱好是
                        <ol>
                            {{#hobbies}}
                            <li>{{.}}</li>
                            {{/hobbies}}
                        </ol>
                    </li>
                    {{/students}}
                </ul>
            </div>
        `;

        // 数据
        var data = {
            students: [
                { 'name': '小明', 'hobbies': ['编程', '游泳'] },
                { 'name': '小红', 'hobbies': ['看书', '弹琴', '画画'] },
                { 'name': '小强', 'hobbies': ['锻炼'] }
            ]
        };

        // 调用render
        var domStr = test_TemplateEngine.render(templateStr, data);
        console.log(domStr);

        // 渲染上树
        var container = document.getElementById('container');
        container.innerHTML = domStr;
    </script>
</body>
  • index.js
// 全局提供 test_TemplateEngine 对象
window.test_TemplateEngine = {
    // 渲染方法
    render(templateStr, data) {
        // 调用 parseTemplateToTokens 函数,让模板字符串能够变为 tokens 数组
        var tokens = parseTemplateToTokens(templateStr);
        // 调用 renderTemplate 函数,让 tokens 数组变为 dom 字符串
        var domStr = renderTemplate(tokens, data);
        
        return domStr;
    }
};
4.3 具体方法
4.3.1 如何将模版字符串变为 tokens
  • Scanner 扫描类
    • 扫描模版字符串,判断类型
/* 
    扫描器类
*/
export default class Scanner {
    constructor(templateStr) {
        // 将模板字符串写到实例身上
        this.templateStr = templateStr;
        // 指针
        this.pos = 0;
        // 尾巴,一开始就是模板字符串原文
        this.tail = templateStr;
    }

    // 功能弱,就是走过指定内容,没有返回值
    scan(tag) {
        if (this.tail.indexOf(tag) == 0) {
            // tag有多长,比如{{长度是2,就让指针后移多少位
            this.pos += tag.length;
            // 尾巴也要变,改变尾巴为从当前指针这个字符开始,到最后的全部字符
            this.tail = this.templateStr.substring(this.pos);
        }
    }

    // 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
    scanUtil(stopTag) {
        // 记录一下执行本方法的时候pos的值
        const pos_backup = this.pos;
        // 当尾巴的开头不是stopTag的时候,就说明还没有扫描到stopTag
        // 写&&很有必要,因为防止找不到,那么寻找到最后也要停止下来
        while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
            this.pos++;
            // 改变尾巴为从当前指针这个字符开始,到最后的全部字符
            this.tail = this.templateStr.substring(this.pos);
        }

        return this.templateStr.substring(pos_backup, this.pos);
    }

    // 指针是否已经到头,返回布尔值。end of string
    eos() {
        return this.pos >= this.templateStr.length;
    }
};
  • tokens 嵌套
    • 利用数据结构–栈
    • 扫描到 # 号进栈,扫描到 / 号出栈
  • nestTokens.js
/* 
    函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
*/
export default function nestTokens(tokens) {
    // 结果数组
    var nestedTokens = [];
    // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
    var sections = [];
    // 收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组
    // 收集器的指向会变化,当遇见#的时候,收集器会指向这个token的下标为2的新数组
    var collector = nestedTokens;

    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];

        switch (token[0]) {
            case '#':
                // 收集器中放入这个token
                collector.push(token);
                // 入栈
                sections.push(token);
                // 收集器要换人。给token添加下标为2的项,并且让收集器指向它
                collector = token[2] = [];
                break;
            case '/':
                // 出栈。pop()会返回刚刚弹出的项
                sections.pop();
                // 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
                collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
                break;
            default:
                // 甭管当前的collector是谁,可能是结果nestedTokens,也可能是某个token的下标为2的数组,甭管是谁,推入collctor即可
                collector.push(token);
        }
    }

    return nestedTokens;
};
  • parseTemplateToTokens.js
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';

/* 
    将模板字符串变为tokens数组
*/
export default function parseTemplateToTokens(templateStr) {
    var tokens = [];
    // 创建扫描器
    var scanner = new Scanner(templateStr);
    var words;
    // 让扫描器工作
    while (!scanner.eos()) {
        // 收集开始标记出现之前的文字
        words = scanner.scanUtil('{{');
        if (words != '') {
            // 去掉空格,智能判断是普通文字的空格,还是标签中的空格
            // 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格
            let isInJJH = false;
            // 空白字符串
            var _words = '';
            for (let i = 0; i < words.length; i++) {
                // 判断是否在标签里
                if (words[i] == '<') {
                    isInJJH = true;
                } else if (words[i] == '>') {
                    isInJJH = false;
                }
                // 如果这项不是空格,拼接上
                if (!/\s/.test(words[i])) {
                    _words += words[i];
                } else {
                    // 如果这项是空格,只有当它在标签内的时候,才拼接上
                    if (isInJJH) {
                        _words += ' ';
                    }
                }
            }
            // 存起来,去掉空格
            tokens.push(['text', _words]);
        }
        // 过双大括号
        scanner.scan('{{');
        // 收集开始标记出现之前的文字
        words = scanner.scanUtil('}}');
        if (words != '') {
            // 这个words就是{{}}中间的东西。判断一下首字符
            if (words[0] == '#') {
                // 存起来,从下标为1的项开始存,因为下标为0的项是#
                tokens.push(['#', words.substring(1)]);
            } else if (words[0] == '/') {
                // 存起来,从下标为1的项开始存,因为下标为0的项是/
                tokens.push(['/', words.substring(1)]);
            } else {
                // 存起来
                tokens.push(['name', words]);
            }
        }
        // 过双大括号
        scanner.scan('}}');
    }

    // 返回折叠收集的tokens
    return nestTokens(tokens);
}
4.3.2 如何将 tokens 数组变为 DOM 字符串
  • # 标记的 tokens,需要递归处理下标为2的小数组
  • 注意
    • JS 不认识点符号
  • lookup.js
/* 
    功能是可以在dataObj对象中,寻找用连续点符号的keyName属性
    比如,dataObj是
    {
        a: {
            b: {
                c: 100
            }
        }
    }
    那么lookup(dataObj, 'a.b.c')结果就是100
*/
export default function lookup(dataObj, keyName) {
    // 看看keyName中有没有点符号,但是不能是.本身
    if (keyName.indexOf('.') != -1 && keyName != '.') {
        // 如果有点符号,那么拆开
        var keys = keyName.split('.');
        // 设置一个临时变量,这个临时变量用于周转,一层一层找下去。
        var temp = dataObj;
        // 每找一层,就把它设置为新的临时变量
        for (let i = 0; i < keys.length; i++) {
            temp = temp[keys[i]];
        }
        return temp;
    }
    // 如果这里面没有点符号
    return dataObj[keyName];
};
  • 递归
  • parseArray.js
import lookup from './lookup.js';
import renderTemplate from './renderTemplate.js';

/* 
    处理数组,结合renderTemplate实现递归
    注意,这个函数收的参数是一个token,而不是多个token组成的tokens
    
    这个函数要递归调用renderTemplate函数,调用的次数由data决定
    比如data的形式是这样的:
    {
        students: [
            { 'name': '小明', 'hobbies': ['游泳', '健身'] },
            { 'name': '小红', 'hobbies': ['足球', '蓝球', '羽毛球'] },
            { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] },
        ]
    };
    那么parseArray()函数就要递归调用renderTemplate函数3次,因为数组长度是3
*/

export default function parseArray(token, data) {
    // 得到整体数据data中这个数组要使用的部分
    var v = lookup(data, token[1]);
    // 结果字符串
    var resultStr = '';
    // 遍历v数组,v一定是数组
    // 注意,下面这个循环可能是整个包中最难思考的一个循环
    // 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。
    for(let i = 0 ; i < v.length; i++) {
        // 这里要补一个“.”属性
        // 拼接
        resultStr += renderTemplate(token[2], {
            ...v[i],
            '.': v[i]
        });
    }
    return resultStr;
};
  • renderTemplate.js
import lookup from './lookup.js';
import parseArray from './parseArray.js';
/* 
    函数的功能是让tokens数组变为dom字符串
*/
export default function renderTemplate(tokens, data) {
    // 结果字符串
    var resultStr = '';
    // 遍历tokens
    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];
        // 看类型
        if (token[0] == 'text') {
            // 拼起来
            resultStr += token[1];
        } else if (token[0] == 'name') {
            // 如果是name类型,那么就直接使用它的值,当然要用lookup
            // 因为防止这里是“a.b.c”有逗号的形式
            resultStr += lookup(data, token[1]);
        } else if (token[0] == '#') {
            resultStr += parseArray(token, data);
        }
    }

    return resultStr;
}

Vue 源码 – 虚拟 DOM 和 diff 算法

1. 虚拟 DOM 和 diff 算法简单介绍

1.1 虚拟 DOM
  • 真实 DOM
<div class="box">
	<h3>我是一个标题</h3>
	<ul>
		<li>牛奶</li>
		<li>咖啡</li>
		<li>可乐</li>
	</ul>
</div>
  • 虚拟 DOM
    • 用 JavaScript 对象描述 DOM 的层次结构,DOM 中的一切属性都在虚拟 DOM 中有对应的属性
{
	"sel": "div", // select选择器
	"data": {
		"class": { "box": true }
	},
	"children": [ // 子元素
		{
			"sel": "h3",
			"data": {},
			"text": "我是一个标题"
		},
		{
			"sel": "ul",
			"data": {},
			"children": [ 
				{ "sel": "li", "data": {}, "text": "牛奶" },
				{ "sel": "li", "data": {}, "text": "咖啡" },
				{ "sel": "li", "data": {}, "text": "可乐" }
			]
		}
	]
}
1.2 diff 算法
  • diff 算法可以进行精细化对比,实现最小量更新
  • 新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真正的DOM 上
<div class="box">
	<h3>我是一个标题</h3>
	<ul>
		<li>牛奶</li>
		<li>咖啡</li>
		<li>可乐</li>
	</ul>
</div>

变为

<div class="box">
	<h3>我是一个标题</h3>
	<span>我是一个新的span</span>
	<ul>
		<li>牛奶</li>
		<li>咖啡</li>
		<li>可乐</li>
		<li>雪碧</li>
	</ul>
</div>
{
	"sel": "div",
	"data": {
		"class": { "box": true }
	},
	"children": [ 
		{
			"sel": "h3",
			"text": "我是一个标题"
		},
		{
			"sel": "ul",
			"data": {},
			"children": [
				{ "sel": "li", "text": "牛奶" },
				{ "sel": "li", "text": "咖啡" },
				{ "sel": "li", "text": "可乐" }
			]
		}
	]
}

{
	"sel": "div",
	"data": {
		"class": { "box": true }
	},
	"children": [ 
		{
			"sel": "h3",
			"text": "我是一个标题"
		},
		{
			"sel": "span",
			"text": "我是一个新的span"
		},
		{
			"sel": "ul",
			"data": {},
			"children": [
				{ "sel": "li", "text": "牛奶" },
				{ "sel": "li", "text": "咖啡" },
				{ "sel": "li", "text": "可乐" },
				{ "sel": "li", "text": "雪碧" }
			]
		}
	]
}

2. snabbdom 简介和测试环境搭建

  • snabbdom 是瑞典语单词,原意是“速度
  • snabbdom 是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码借鉴了 snabbdom
  • 官方 git: https://github.com/snabbdom/snabbdom
2.1 安装 snabbdom
  • 在 git上 的 snabbdom 源码是用 TypeScript 写的,git 上并不提供编译好的 JavaScript 版本
  • 如果要直接使用 build 出来的 JavaScript 版的 snabbdom 库,可以从 npm 上下载
    • npm i -S snabbdom
2.2 snabbdom 测试环境搭建
  • snabbdom 库是 DOM 库不能在 nodejs 环境运行,所以需要搭建 webpack 和 webpack-dev-server 开发环境,不需要安装任何 loader

  • 注意

    • 必须安装最新版 webpack@5,不能安装 webpack@4
    • 因为 webpack4 没有读取身份证中 exports 的能力
  • webpack.config.js

const path = require('path');

module.exports = {
    // 入口
    entry: './src/index.js',
    // 出口
    output: {
        // 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成
        publicPath: 'xuni',
        // 打包出来的文件名,不会真正的物理生成
        filename: 'bundle.js'
    },
    devServer: {
        // 端口号
        port: 8080,
        // 静态资源文件夹
        contentBase: 'www'
    }
};

3. snabbdom 的 h 函数是如何工作

3.1 h 函数用来产生虚拟节点
  • h 函数用来产生虚拟节点(vnode)
    • 比如这样调用 h 函数
    • h('a', {props: {href: 'http://www.baidu.com'}}, '张三'
    • 将得到这样的虚拟节点
    • {"sel": "a", "data": {props: {href: 'http://www.baidu.com'}}, "text": "张三"}
    • 它表示的真正的 DOM 节点
    • <a href="http://www.baidu.com">张三</a>"
import { init } from 'snabbdom/init';
import { classModule } from 'snabbdom/modules/class';
import { propsModule } from 'snabbdom/modules/props';
import { styleModule } from 'snabbdom/modules/style';
import { eventListenersModule } from 'snabbdom/modules/eventlisteners';
import { h } from 'snabbdom/h';

// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);

// 创建虚拟节点
const myVnode1 = h('a', {
    props: {
        href: 'http://www.baidu.com',
        target: '_blank'
    }
}, '百度');

// 让虚拟节点上树
const container = document.getElementById('container');
patch(container, myVnode1);
3.2 虚拟节点的属性
{
	children: undefined // 子元素
	data: {}			// 数据(属性、样式等)
	elm: undefined		// 此元素对应的真正的 dom 节点
	key: undefined		// 此节点唯一标识
	sel: "div"			// 选择器
	text: "我是一个盒子"  // 文字
}
3.3 h 函数的嵌套使用
const myVnode3 = h('ul', [
    h('li', {}, '苹果'),
    h('li', '西瓜'),
    h('li', [
        h('div', [
            h('p', '哈哈'),
            h('p', '嘻嘻')
        ])
    ]),
    h('li', h('p', '火龙果'))
]);
3.4 手写 h 函数
3.4.1 相关源码
  • h.js
import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'

export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement<T> = T | T[]
export type VNodeChildren = ArrayOrElement<VNodeChildElement>

function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg'
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      const childData = children[i].data
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
      }
    }
  }
}

export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {}
  var children: any
  var text: any
  var i: number
  if (c !== undefined) {
    if (b !== null) {
      data = b
    }
    if (is.array(c)) {
      children = c
    } else if (is.primitive(c)) {
      text = c
    } else if (c && c.sel) {
      children = [c]
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b
    } else if (is.primitive(b)) {
      text = b
    } else if (b && b.sel) {
      children = [b]
    } else { data = b }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
    }
  }
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    addNS(data, children, sel)
  }
  return vnode(sel, data, children, text, undefined)
};
  • vnode.ts
import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'

export type Key = string | number

export interface VNode {
  sel: string | undefined
  data: VNodeData | undefined
  children: Array<VNode | string> | undefined
  elm: Node | undefined
  text: string | undefined
  key: Key | undefined
}

export interface VNodeData {
  props?: Props
  attrs?: Attrs
  class?: Classes
  style?: VNodeStyle
  dataset?: Dataset
  on?: On
  hero?: Hero
  attachData?: AttachData
  hook?: Hooks
  key?: Key
  ns?: string // for SVGs
  fn?: () => VNode // for thunks
  args?: any[] // for thunks
  [key: string]: any // for any other 3rd party module
}

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
3.4.2 手写源码
  • vnode.js
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function(sel, data, children, text, elm) {
    const key = data.key;
    return {
        sel, data, children, text, elm, key
    };
}
  • h.js
import vnode from './vnode.js';
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
export default function (sel, data, c) {
    // 检查参数的个数
    if (arguments.length != 3)
        throw new Error('对不起,h函数必须传入3个参数');
    // 检查参数c的类型
    if (typeof c == 'string' || typeof c == 'number') {
        // 说明现在调用h函数是形态①
        return vnode(sel, data, undefined, c, undefined);
    } else if (Array.isArray(c)) {
        // 说明现在调用h函数是形态②
        let children = [];
        // 遍历c,收集children
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个对象,如果不满足
            if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
                throw new Error('传入的数组参数中有项不是h函数');
            // 这里不用执行c[i],因为测试语句中已经有了执行
            // 此时只需要收集好就可以了
            children.push(c[i]); 
        }
        // 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它有children属性的
        return vnode(sel, data, children, undefined, undefined);
    } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
        // 说明现在调用h函数是形态③
        // 即,传入的c是唯一的children。不用执行c,因为测试语句中已经执行了c。
        let children = [c];
        return vnode(sel, data, children, undefined, undefined);
    } else {
        throw new Error('传入的第三个参数类型不对');
    }
};

4. diff 算法原理

4.1 diff 体验
  • 最小量更新
  • key 很重要
    • key 是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点
import { init } from 'snabbdom/init';
import { classModule } from 'snabbdom/modules/class';
import { propsModule } from 'snabbdom/modules/props';
import { styleModule } from 'snabbdom/modules/style';
import { eventListenersModule } from 'snabbdom/modules/eventlisteners';
import { h } from 'snabbdom/h';

// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');

// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);

const vnode1 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D')
]);

patch(container, vnode1);

const vnode2 = h('ul', {}, [
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'B' }, 'B')
]);

// 点击按钮时,将vnode1变为vnode2
btn.onclick = function () {
    patch(vnode1, vnode2);
};
  • 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的
    • 延伸问题:如何定义是同一个虚拟节点?
    • 答:选择器相同且 key 相同
const vnode1 = h('ul', {}, [ // 直接拆掉 ul
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D')
]);

patch(container, vnode1);

const vnode2 = h('ol', {}, [ // 直接插入新的 ol
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'B' }, 'B')
]);

// 点击按钮时,将vnode1变为vnode2
btn.onclick = function () {
    patch(vnode1, vnode2);
};
  • 只进行同层比较,不会进行跨层比较
  • 即使是同一片虚拟节点,但是跨层了,不会进行 diff 算法,而是暴力删除旧的、然后插入新的
const vnode1 = h('div', {}, [ 
    h('p', { key: 'A' }, 'A'),
    h('p', { key: 'B' }, 'B'),
    h('p', { key: 'C' }, 'C'),
    h('p', { key: 'D' }, 'D')
]);

patch(container, vnode1);

const vnode2 = h('div', {}, h('section', {}, [ // 多加了一层 section
    h('p', { key: 'D' }, 'D'),
    h('p', { key: 'A' }, 'A'),
    h('p', { key: 'C' }, 'C'),
    h('p', { key: 'B' }, 'B')
]));

// 点击按钮时,将vnode1变为vnode2
btn.onclick = function () {
    patch(vnode1, vnode2);
};
4.2 diff 处理新旧节点不是同一个节点时
4.2.1 相关源码
  • init.ts
function emptyNodeAt (elm: Element) { // 把节点的标签名和id以及类名(h('div#box.box1'))传入到虚拟节点中
  const id = elm.id ? '#' + elm.id : ''
  const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
  return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}

return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
  let i: number, elm: Node, parent: Node
  const insertedVnodeQueue: VNodeQueue = []
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

  if (!isVnode(oldVnode)) { // 如果老的虚拟节点不是节点,比如第一次渲染dom树
    oldVnode = emptyNodeAt(oldVnode)
  }

  if (sameVnode(oldVnode, vnode)) { // 判断两个是不是同一个节点
    patchVnode(oldVnode, vnode, insertedVnodeQueue) // 如果是同一个节点,进行 diff
  } else {
  	// 不是同一个节点,进行暴力删除、添加 
    elm = oldVnode.elm!
    parent = api.parentNode(elm) as Node

    createElm(vnode, insertedVnodeQueue) // 创建新节点

    if (parent !== null) {
      api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
      removeVnodes(parent, [oldVnode], 0, 0) // 移除节点
    }
  }

  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
  }
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
  return vnode
}
4.2.2 流程图

请添加图片描述

4.2.3 如何定义同一个节点
// 新老节点的key和选择器相同
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
4.2.4 递归创建子节点
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  let i: any
  let data = vnode.data
  if (data !== undefined) {
    const init = data.hook?.init
    if (isDef(init)) {
      init(vnode)
      data = vnode.data
    }
  }
  const children = vnode.children
  const sel = vnode.sel
  if (sel === '!') {
    if (isUndef(vnode.text)) {
      vnode.text = ''
    }
    vnode.elm = api.createComment(vnode.text!)
  } else if (sel !== undefined) {
    // Parse selector
    const hashIdx = sel.indexOf('#')
    const dotIdx = sel.indexOf('.', hashIdx)
    const hash = hashIdx > 0 ? hashIdx : sel.length
    const dot = dotIdx > 0 ? dotIdx : sel.length
    const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
    const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
      ? api.createElementNS(i, tag)
      : api.createElement(tag)
    if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
    if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
    for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
    if (is.array(children)) {
      for (i = 0; i < children.length; ++i) {
        const ch = children[i]
        if (ch != null) {
          api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
        }
      }
    } else if (is.primitive(vnode.text)) {
      api.appendChild(elm, api.createTextNode(vnode.text))
    }
    const hook = vnode.data!.hook
    if (isDef(hook)) {
      hook.create?.(emptyNode, vnode)
      if (hook.insert) {
        insertedVnodeQueue.push(vnode)
      }
    }
  } else {
    vnode.elm = api.createTextNode(vnode.text!)
  }
  return vnode.elm
}

5. 手写 diff 算法

5.1 手写第一次渲染 DOM 树
  • index.js
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';

const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D')
]);

// 得到盒子和按钮
const container = document.getElementById('container');

// 第一次上树
patch(container, myVnode1);
  • patch.js
import vnode from './vnode.js';
import createElement from './createElement.js';

export default function patch(oldVnode, newVnode) {
	// 判断传入的第一个参数,是 DOM 节点还是虚拟节点
	if(oldVnode.sel = '' || oldVnode.sel = undefined) {
		// 传入的第一个参数是 DOM 节点,此时要包装为虚拟节点
		oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
	}
	
	// 判断 oldVnode 和 newVnode 是不是同一个节点
	if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode == sel) {
		console.log('是同一个节点')
	} else {
		console.log('不是同一个节点,暴力插入新的,删除旧的')
		createElement(newVnode, oldVnode.elm)
	}
  • createElement.js
// 真正创建节点,将 vnode 创建为 DOM,插入到 pivot 这个元素之前
export default function(vnode, pivot) {
	console.log('目的是把虚拟节点', vnode, '插入到标杆', pivot, '前');\
	// 创建一个 DOM 节点,这个节点现在还是孤儿节点
	let domNode = document.createElement(vnode.sel);
	// 有子节点还是有文本
	if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
		// 它内部是文字
		domNode.innerText = vnode.text;
		// 将孤儿节点上树,让标杆节点的父元素调用 insertBefore 方法,将新的孤儿节点插入到标签节点之前
		pivot.parentNode.insertBefore(domNode, pivot)
	} else if(Array.isArray(vnode.children) && vnode.children.length > 0) {
	
	}
}
5.2 手写递归创建子节点
  • createElement.js
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
    // console.log('目的是把虚拟节点', vnode, '真正变为DOM');
    // 创建一个DOM节点,这个节点现在还是孤儿节点
    let domNode = document.createElement(vnode.sel);
    // 有子节点还是有文本??
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 它内部是文字
        domNode.innerText = vnode.text;
        // 补充 elm 属性
        vnode.elm = domNode;
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 它内部是子节点,就要递归创建节点
        for (let i = 0; i < vnode.children.length; i++) {
            // 得到当前这个children
            let ch = vnode.children[i];
            // 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
            let chDOM = createElement(ch);
            // 上树
            domNode.appendChild(chDOM);
        }
    }
    // 补充elm属性
    vnode.elm = domNode;
   
    // 返回elm,elm属性是一个纯DOM对象
    return vnode.elm;
};
  • patch.js
import vnode from './vnode.js';
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点?
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }

    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        patchVnode(oldVnode, newVnode);
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        let newVnodeElm = createElement(newVnode);
        
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
};
  • index.js
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';

const myVnode1 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'E' }, 'E')
]);

// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');

// 第一次上树
patch(container, myVnode1);

// 新节点
const myVnode2 = h('ul', {}, [
    h('li', { key: 'Q' }, 'Q'),
    h('li', { key: 'T' }, 'T'),
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'Z' }, 'Z'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'E' }, 'E')
]);

btn.onclick = function () {
    patch(myVnode1, myVnode2);
}
5.3 diff 处理新旧节点是同一个节点时
  • 流程图
    请添加图片描述

  • path.js

import vnode from './vnode.js';
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点?
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }

    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        patchVnode(oldVnode, newVnode);
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        let newVnodeElm = createElement(newVnode);
        
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
};
  • patchVnode.js
import createElement from "./createElement";
import updateChildren from './updateChildren.js';

// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
    // 判断新旧vnode是否是同一个对象
    if (oldVnode === newVnode) return;
    // 判断新vnode有没有text属性
    if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
        // 新vnode有text属性
        console.log('新vnode有text属性');
        if (newVnode.text != oldVnode.text) {
            // 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。
            oldVnode.elm.innerText = newVnode.text;
        }
    } else {
        // 新vnode没有text属性,有children
        console.log('新vnode没有text属性');
        // 判断老的有没有children
        if (oldVnode.children != undefined && oldVnode.children.length > 0) {
            // 老的有children,新的也有children,此时就是最复杂的情况。
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
        } else {
            // 老的没有children,新的有children
            // 清空老的节点的内容
            oldVnode.elm.innerHTML = '';
            // 遍历新的vnode的子节点,创建DOM,上树
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}
5.4 手写 diff 更新子节点
5.4.1 新增的情况
  • 新创建的节点(newVnode.children[i].elm)插入到所有未处理的节点(oldVnode.children[um].elm)之前,而不是所有已处理节点之后
// 旧子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'C' }, 'C')

// 新子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'M' }, 'M')
h('li', { key: 'N' }, 'N')
h('li', { key: 'C' }, 'C')
5.4.2 删除的情况
// 旧子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'C' }, 'C')

// 新子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'C' }, 'C')
5.4.3 更新的情况
// 旧子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'C' }, 'C')

// 新子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'C' },[
	h()
])
5.5 diff 算法的子节点更新策略
5.5.1 经典的 diff 算法优化策略
  • 四种命中查找
    • 新前与旧前
    • 新后与旧后
    • 新后与旧前
      • 此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后
    • 新前与旧后
      • 此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前
  • 命中一种
    • 不再进行命中判断
  • 没有命中
    • 需要用循环来寻找,移动到 oldStartIdx 之前
5.5.2 新增的情况
  • 如果是旧节点先循环完毕,说明新节点中有要插入的节点
// 旧子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'C' }, 'C') // 旧后
h('li', { key: 'E' }, 'E') // 旧前

// 新子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'D' }, 'D') 
h('li', { key: 'E' }, 'E') // 新前
h('li', { key: 'C' }, 'C') // 新后

while(新前<=新后&&旧前<=旧后){}
5.5.3 删除的情况
  • 如果是新节点先循环完毕,如果老节点中还有剩余节点,说明他们是要被删除的节点
// 旧子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
h('li', { key: 'C' }, 'C') // 旧后 旧前
h('li', { key: 'D' }, 'D')

// 新子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B') // 新后
h('li', { key: 'D' }, 'D') // 新前

while(新前<=新后&&旧前<=旧后){}
5.5.4 多删除的情况
  • 如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和新后指针中间的节点),说明他们是要被删除的节点
// 旧子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B')
// D
h('li', { key: 'C' }, 'C') // 旧前
// undefined
h('li', { key: 'E' }, 'E') // 旧后

// 新子节点
h('li', { key: 'A' }, 'A')
h('li', { key: 'B' }, 'B') 
h('li', { key: 'D' }, 'D') // 新后
// 新前

while(新前<=新后&&旧前<=旧后){}
5.5.4 复杂的情况
// 当新前与旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
// 旧子节点
E
C
M
h('li', { key: 'A' }, 'A') // 旧前
h('li', { key: 'B' }, 'B')
undefined
h('li', { key: 'D' }, 'D') 
undefined // 旧后

// 新子节点
h('li', { key: 'E' }, 'E')
h('li', { key: 'C' }, 'C') 
h('li', { key: 'M' }, 'M') // 新后
// 新前
// 当新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
// 旧子节点
h(undefined)
h(undefined)
h(undefined) 
h(undefined)
h(undefined) // 旧前 新前
E
D
C
B
A

// 新子节点
h('li', { key: 'E' }, 'E') // 新前
h('li', { key: 'D' }, 'D') // 新后
h('li', { key: 'C' }, 'C')
h('li', { key: 'B' }, 'B')
h('li', { key: 'A' }, 'A')

while(新前<=新后&&旧前<=旧后){}

Vue 源码 – 数据响应式原理

1. 相关图解

请添加图片描述

2. MVVM 模式

2.1 模版
<p>我{{age}}岁了</p>
2.2 数据变化
this.age++
2.3 数据变化,视图会自动变化

请添加图片描述

3. 侵入式和非侵入式

3.1 非侵入式
// Vue 数据变化
this.a ++
3.2 侵入式
// React 数据变化
this.setState({
	a: this.state.a + 1
})

// 小程序数据变化
this.setData({
	a: this.data.a + 1
})

4. Object.defineProperty() 方法

  • 数据劫持 / 数据代理
  • 利用 JavaScript 引擎赋予的功能,检测对象属性变化
  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
var obj = {};

Object.defineProperty(obj, 'a', { 
	value: 3
});

Object.defineProperty(obj, 'b', { 
	value: 5
});

console.log(obj); 
console.log(obj.a, obj.b);
  • Object.defineProperty() 方法可以设置一些额外隐藏的属性
Object.defineProperty(obj, 'a', { 
	value: 3,
	// 是否可写
	writable: false
});

Object.defineProperty(obj, 'b', {
	value: 5,
	// 是否可以被枚举
	enumerable: false
});

5. getter / setter

5.1 get
  • get 属性的 getter 函数,如果没有 getter,则为 undefined,当访问该属性时,会调用此函数
  • 执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
5.2 set
  • set 属性的 setter 函数,如果没有 setter,则为 undefined
  • 当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
  • 注意
    • 用闭包存储 get 和 set 的值
Object.defineProperty(obj, 'a', {
	// getter
	get() {
		console.log('你试图访问obj的a属性');
	},
	// setter 
	set() {
		console.log('你试图改变obj的a属性');
	}
});
console.log(obj.a); 
obj.a = 10;

6. defineReactive 函数

  • getter / setter 需要变量周转才能工作
var temp;
Object.defineProperty(obj, 'a', {
	// getter 
	get() {
		console.log('你试图访问obj的a属性'); 
		return temp;
	},
	// setter 
	set(newValue) {
		console.log('你试图改变obj的a属性', newValue); 
		temp = newValue; 
	}
});
  • 使用 defineReactive 函数不需要设置临时变量,而是用闭包
function defineReactive(data, key, val) {
	Object.defineProperty(data, key, {
		// 可枚举
		enumerable: true,
		// 可以被配置,比如可以被delete
		configurable: true,
		// getter 
		get() {
			console.log('你试图访问obj的' + key + '属性'); 
			return val;
		},
		// setter
		set(newValue) {
			console.log('你试图改变obj的' 
			if (val === newValue) {
				return; 
			}
			val = newValue; 
		}
	});
}

7. 递归侦测对象全部属性

7.1 相关图解

请添加图片描述

7.2 Observer
  • 将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的)的 object
var obj = { 
	a: {
		m: {
			n: 5 
		}
	},
	b: 4
};

8. 数组的响应式处理

  • 改写七个方法
    • push
    • pop
    • shift
    • unshift
    • splice
    • sort
    • reverse

请添加图片描述

9. 依赖收集

9.1 什么是依赖
  • 需要用到数据的地方,称为依赖
  • Vue1.x,细粒度依赖,用到数据的 DOM 都是依赖
  • Vue2.x,中等粒度依赖,用到数据的组件是依赖
  • 在 getter 中收集依赖,在 setter 中触发依赖
9.2 Dep 类和 Watcher 类
  • 把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,成员中都有一个Dep的实例
  • Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件

请添加图片描述

  • 依赖就是Watcher
    • 只有Watcher触发的getter才会收集依赖\
    • 哪个Watcher触发了getter,就把哪个Watcher收集到Dep中
  • Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍
  • 代码实现的巧妙之处:
    • Watcher把自己设置到全局的一个指定位置,然后读取数据
    • 因为读取了数据,所以会触发这个数据的getter
    • 在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher 收集到Dep中

请添加图片描述

Vue 源码 – AST 抽象语法树

1. 抽象语法树是什么

请添加图片描述

<div class="box">
	<h3 class="title">我是一个标题</h3>
	<ul>
		<li v-for="item in arr" :key="index">
			{{item}}
		</li>
	</ul>
</div>

<div class="box">
	<h3 class="title">我是一个标题</h3>
	<ul>
		<li>牛奶</li>
		<li>咖啡</li>
		<li>可乐</li>
	</ul>
</div>

请添加图片描述

2. 抽象语法树本质上就是一个 JS 对象

<div class="box">
	<h3 class="title">我是一个标题</h3>
	<ul>
		<li v-for="item in arr" :key="index">
			{{item}}
		</li>
	</ul>
</div>
  • 以字符串的视角解析为 AST
{
	tag: "div",
	attrs: [{ name: "class", value: "box" }],
	type: 1, 
	children: [ 
		{
			tag: "h3",
			attrs: [{ name: "class", value: "title" }], 
			type: 1,
			children: [{ text: "我是一个标题", type: 3 }]
		},
		{
			tag: "ul",
			attrs: [], 
			type: 1, 
			children: [ 
				{
					tag: "li",
					for: "arr",
					key: "index",
					alias: "item", 
					type: 1, 
					children: []
				}
			]
		}
	]
}

3. 抽象语法树和虚拟节点的关系

  • 相关图解
    请添加图片描述

4. 算法储备

4.1 指针思想
  • 指针题目
    • 试寻找字符串中,连续重复次数最多的字符

请添加图片描述

  • 指针就是下标,不是C语言中的指针,C语言中的指针可以操作内存。JS中的指针就是一个下标位置
    • i: 0 j: 1
  • 如果i和j指向的字一样,那么i不动,j后移
  • 如果i和j指向的字不一样,此时说明它们之间的字都是连续相同的,让i追上j, j后移
4.2 递归深入
  • 递归题目1
    • 试输出斐波那契数列的前10项,即1、1、2、3、5、8、13、21、34、55。然后请思
      考,代码是否有大量重复的计算?应该如何解决重复计算的问题?
    • cache 思想
{
	'0': 1,
	'1': 1,
	'2': 2,
	'3': 3,
	'4': 5,
}

请添加图片描述

  • 递归题目2
    • 形式转换:试将高维数组[1, 2, [3, [4, 5], 6], 7, [8], 9]变为图中所示的对象
    • 只要出现了“规则复现”就要想到用递归
{
	children: [
		{ value: 1 },
		{ value: 2 },
		{ children: [
			{ value: 3 },
			{ children: [
				{ value: 4 },
				{ value: 5 }
			]},
			{ value: 6 }
		]},
		{ value: 7 },
		{ childrenL [
			{ value: 8 },
			{ value: 9 }
		]}
	]
}
4.3 栈
    • 栈(stack)又名堆栈,它是一种运算受限的线性表,仅在表尾能进行插入和删除操作。这一端被称为栈顶,相对地,把另一端称为栈底
    • 向一个栈插入新元素又称作进栈、入栈或压栈;从一个栈删除元素又称作出栈或退栈
    • 后进先出(LIFO)特点:栈中的元素,最先进栈的必定是最后出栈,后进栈的一定会先出栈
    • JavaScript中,栈可以用数组模拟。需要限制只能使用push()和pop(),不能使用unshift()和shift()。即,数组尾是栈顶
    • 可以用面向对象等手段,将栈封装的更好
      请添加图片描述
  • 利用“栈”的题目

    • 试编写“智能重复”smartRepeat函数,实现:
      • 将3[abc]变为abcabcabc
      • 将3[2[a]2[b]]变为aabbaabbaabb
      • 将2[1[a]3[b]2[3[c]4[d]]]变为abbbcccddddcccddddabbbcccddddcccdddd
    • 不用考虑输入字符串是非法的情况,比如:
      • 2[a3[b]]是错误的,应该补一个1,即2[1[a]3[b]]
      • [abc]是错误的,应该补一个1,即1[abc]
  • 利用“栈”的题目
    请添加图片描述

    • 遍历每一个字符
      • 如果这个字符是数字,那么就把数字压栈,把空字符串压栈
      • 如果这个字符是字母,那么此时就把栈顶这项改为这个字母
      • 如果这个字符是],那么就将数字弹栈,就把字符串栈的栈顶的元素重复刚刚的这个次数,弹栈,拼接到新栈顶上
        请添加图片描述
    • 遍历每一个字符
      • 如果这个字符是数字,那么就把数字压栈,把空字符串压栈
      • 如果这个字符是字母,那么此时就把栈顶这项改为这个字母
      • 如果这个字符是],那么就将数字弹栈,就把字符串栈的栈顶的元素重复刚刚的这个次数,弹栈,拼接到新栈顶上

5. 手写实现AST抽象语法树

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值