Vue2源码解读

模版引擎

数据变为视图的办法

  • 纯DOM
  • 数组jion
  • ES6的反引号
  • 模版引擎

mustache基本使用

mustache官方git: https://github.com/janl/mustache.js

https://www.bootcdn.cn/中查找mustache库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<body>

<script type="text/template" id="template">
<ul>
{{#arr}}
<li>
<div>{{name}}的基本信息</div>
<div>
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
</div>
</li>
{{/arr}}
</ul>
</script>

<script src="jslib/mustache.js"></script>
<script type="text/javascript">
let data ={
arr: [
{name: '张三', age: 18},
{name: '李四', age: 16},
]
}
let templateStr = document.getElementById("template").innerHTML
let domStr = Mustache.render(templateStr, data)
document.querySelector("body").innerHTML = domStr
</script>
</body>
  • 不循环
1
2
3
4
5
6
7
8
9
10
11
<div id="box"></div>
<ul id="ul"></ul>
<script type="text/javascript" src="jslib/mustache.js"></script>
<script type="text/javascript">
let template = `大家好,我是{{name}},今年{{age}}岁`
let info = {
name: '张三',
age: 18
}
document.getElementById('box'). innerHTML = Mustache.render(template, info)
</script>
  • 数组
1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
let data ={
arr: ['a', 'b', 'c', {name: '张三', age: 14}, [1,2,3]]
}
let template2 = `
{{#arr}}
<li>{{.}}</li>
{{/arr}}
`
document.getElementById('ul').innerHTML = Mustache.render(template2, data)
</script>

image-20230625215917492

  • 数组嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script type="text/javascript">
let data ={
arr: [
{name: '张三', age: 18, arr: [1,2,3,4]},
{name: '李四', age: 16, arr: [4,5,6,7]},
]
}
let template2 = `
{{#arr}}
<li>
<div>{{name}}的基本信息</div>
<div>
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p> 嵌套数组内容:
{{#arr}}
{{.}}
{{/arr}}
</p>
</div>
</li>
{{/arr}}
`
document.getElementById('ul').innerHTML = Mustache.render(template2, data)
</script>

image-20230625220729380

  • 布尔值。当为false时不会渲染
1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
let template3 = `
{{#m}}
<h1>hello</h1>
{{/m}}
`
let obj = {
m: false
}
document.getElementById('container').innerHTML = Mustache.render(template3, obj)
</script>

mustache底层核心原理

虽然利用正则表达式可以实现模版中的变量替换,但是无法实现循环遍历,所以并不能用正则表达式来实现模版替换

1
2
3
4
5
6
7
8
9
10
11
12
// 利用正则表达式替换,无法实现循环替换 
let templateStr = `hello,大家好,我是{{name}},我今年{{age}}岁了`
let data = {
name: 'lisi',
age: 13
}
function render(templateStr, data) {
return templateStr.replace(/\{\{(\w+)\}\}/g,function(str, $1) {
return data[$1]
})
}
console.log(render(templateStr, data))
  • mustache原理

image-20230628213328572

tokens是一个js的嵌套数组

image-20230628231101207

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let obj = {
arr:[
{name: 'lisi', age: 13, hobby: ['a', 'b', 'c']},
{name: 'zhangsan', age: 25, hobby: [1, 2, 3]}
]
}
let template = `
<ul>
{{#arr}}
<li>{{name}},{{age}}</li>
<li>
{{#hobby}}
<span>{{.}}</span>
{{/hobby}}
</li>
{{/arr}}
</ul>
`
// null代表
tokens = [
['text','\n <ul>\n', null, null],
['#', 'arr',null,null, [
['text', '<li>',null,null],
['name', 'name',null,null],
['text', ',',null,null],
['name', 'age',null,null],
['text', '</li><li>',null,null],
['#', 'hobby', null. null, [
['text', '<span>',null,null],
['name', '.',null,null],
['text', '</span>',null,null]
]
],
['text', '</li>',null,null]
]
],
['text', '</ul>',null,null]
]
console.log(Mustache.render(template, data))

手写mustache库

模块化打包工具有webpack(webpack-dev-server)rollupParcel

• mustache官方库使用rollup进行模块化打包,而我们今天使用webpack(webpack-dev-server)进行模块化打包,这是因为webpack(webpackdev-server)能让我们更方便地在浏览器中(而不是nodejs环境中)实时调试程序,相比nodejs控制台,浏览器控制台更好用,比如能够点击展开数组的每项。

• 生成库是UMD的,这意味着它可以同时在nodejs环境中使用,也可以在浏览器环境中使用。实现UMD不难,只需要一个“通用头”即可

UMD 是 JavaScript 模块的通用模块定义模式。这些模块能够在任何地方工作,无论是在客户端、服务器还是其他地方。

UMD 模式通常试图提供与当今最流行的脚本加载器(例如 RequireJS 等)的兼容性。 在许多情况下,它使用 AMD 作为基础,并添加了特殊的外壳来处理 CommonJS 兼容性。

webpack最新版是5,webpack-dev-server最新版是4,但是目前它们的最新版兼容程度不好,建议大家使用这样的版本

image-20230628232932001

image-20230628232945582

初始化

1
2
npm init
npm i -D webpack@4 webpack-dev-server@3 webpack-cli@3

image-20230628233529352

新建webpack.config.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const path = require('path')

module.exports = {
// 模式,开发
mode: 'development',
// 入口
entry: './src/index.js',
// 打包到什么文件
output: {
filename: 'bundle.js'
},
// 配置一下webpack-dev-server
devServer: {
// 静态文件根目录
contentBase: path.join(__dirname, "www"),
// 不压缩
compress: false,
// 端口号
port: 8080,
// 虚拟打包的路径,bundle.js文件没有真正的生成
publicPath: "/xuni/"
}
}

新建src/index.js文件

1
alert('哈哈哈')

新建www目录,然后新建index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>index.html</h1>
<script src="/xuni/bundle.js"></script>
</body>
</html>

修改package.json中的script

image-20230628234615787

运行

1
npm run dev

然后访问 http://localhost:8080/

image-20230628234840556

扫描类

新建src/Scanner.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 扫描器类
*/

export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr
// 指针
this.pos = 0
// 尾巴,一开始就是模版字符串原文(当前指针位置到末尾)
this.tail = templateStr
}

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

// 遇见指定内容停止,并且返回已走过的字符串(比如遇到{{停止,返回这之前的文件)
scanUtil(stopTag) {
// 记录执行时候pos的位置
const start = this.pos

while(!this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++
this.tail = this.templateStr.substring(this.pos)
}
return this.templateStr.substring(start, this.pos)
}

// 判断指针是否到头 (end of string)
eos() {
return this.pos >= this.templateStr.length
}
}

src/index.js 进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Scanner from './Scanner.js'
window.Mustache = {
render(templateStr, data) {
const scanner = new Scanner(templateStr)
while(!scanner.eos()) {
let word = scanner.scanUtil("{{")
console.log(word)
scanner.scan("{{")
word = scanner.scanUtil("}}")
console.log(word)
scanner.scan("}}")
}
}
}

要在www/index.html中调用

1
2
3
4
5
6
 let templateStr = `hello,大家好,我是{{name}},我今年{{age}}岁了`
let data = {
name: 'lisi',
age: 13
}
Mustache.render(templateStr, data)

image-20230629003232539

生成tokens数组

新建src/parseTemplateToTokens.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Scanner from './Scanner.js'
/**
* 将模板字符串变为tokens数组
* @param {[type]} templateStr [description]
* @return {[type]} [description]
*/
export default function parseTemplateToTokens(templateStr) {
let tokens = []
// 创建扫描器
const scanner = new Scanner(templateStr)
let words
while(!scanner.eos()) {
// 获取开始标记({{)之前的文字
words = scanner.scanUtil('{{')
if (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('}}')
}
return tokens
}

src/index.js测试使用

1
2
3
4
5
6
7
import parseTemplateToTokens from './parseTemplateToTokens.js'
window.Mustache = {
render(templateStr, data) {
const tokens = parseTemplateToTokens(templateStr)
console.log(tokens)
}
}

image-20230629005205611

image-20230629005343735

目前存在的问题,只是一维数组

将tokens数组改为多维数组

新建src/nestTokens.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 将一维token转变为多维
* @param {[type]} tokens [description]
* @return {[type]} [description]
*/
export default function nestTokens(tokens) {
// 结果数组
let nestedTokens = []
// 栈结构
let sections = []
// 当遇见#的时候,收集器会指向这个token的下标为2的新数组
let collector = nestedTokens

for(let i = 0; i < tokens.length; i++) {
let token = tokens[i]
switch(token[0]) {
case '#':
collector.push(token)
// 入栈
sections.push(token)
// 改变collector指向,创建一个新的数组
collector = token[2] = []
break;
case '/':
sections.pop()
// sections.length > 0 说明还有嵌套的数组
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
// 因为collector指向的是nestTokens,所以就相当于像结果数组里面加入
collector.push(token)
}
}
return nestedTokens;
}

对src/parseTemplateToTokens.js生成的tokens进行进一步解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Scanner from './Scanner.js'
`import nestTokens from './nestTokens.js'`
/**
* 将模板字符串变为tokens数组
* @param {[type]} templateStr [description]
* @return {[type]} [description]
*/
export default function parseTemplateToTokens(templateStr) {
let tokens = []
// 创建扫描器
const scanner = new Scanner(templateStr)
let words
while(!scanner.eos()) {
// 获取开始标记({{)之前的文字
words = scanner.scanUtil('{{')
if (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('}}')
}
`return nestTokens(tokens)`
}

image-20230701153730101

image-20230701153704279

tokens结合数据进行解析

先写一个对根据key值来取对象的值,因为data[‘a.b.c’], []无法获取含有点符号的key值

src/getValueByPointKey.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 功能是可以在dataObj对象中,寻找用连续点符号的keyName属性
比如,dataObj是
{
a: {
b: {
c: 100
}
}
}
getValueByPointKey(dataObj, 'a.b.c')结果就是100
* @return {[type]} [description]
*/
export default function getValueByPointKey(dataObj, keyName) {
// 如果keyName包含.并且不是.,
if (keyName.indexOf('.') != -1 && keyName != '.') {
let keys = keyName.split('.')
let temp = dataObj
for(let i = 0; i < keys.length; i++) {
temp = temp[keys[i]]
}
return temp
}
// 如果没有点符号
return dataObj[keyName]
}

// 解析tokens数组变为dom字符串

src/renderTemplate.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import getValueByPointKey from './getValueByPointKey.js'
import renderTemplateParseArray from './renderTemplateParseArray.js'
/**
* 让tokens数组变为dom字符串
* @param {[type]} tokens [description]
* @param {[type]} data [description]
* @return {[type]} [description]
*/
export default function renderTemplate(tokens, data) {
// 结果字符串
let resultStr = ''
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,就说说明是要进行变量替换的
// 防止有a.b.c这种形式我们引入一个函数getValueByPointKey来处里
resultStr += getValueByPointKey(data, token[1])
} else if (token[0] == '#') {
// 解析数组
resultStr += renderTemplateParseArray(token, data)
}
}
return resultStr
}

但是数组是无法解析,所以在写一个数组解析的文件

/src/renderTemplateParseArray.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import getValueByPointKey from './getValueByPointKey.js'
import renderTemplate from './renderTemplate.js'
/**
* 循环遍历解析数组变为dom字符串
* @param {[type]} token [description]
* @param {[type]} data [description]
* @return {[type]} [description]
* 处理数组,结合renderTemplate实现递归
注意,这个函数收的参数是token!而不是tokens!
token是什么,就是一个简单的['#', 'students', [

]]

这个函数要递归调用renderTemplate函数,调用多少次???
千万别蒙圈!调用的次数由data决定
比如data的形式是这样的:
{
students: [
{ 'name': '小明', 'hobbies': ['游泳', '健身'] },
{ 'name': '小红', 'hobbies': ['足球', '蓝球', '羽毛球'] },
{ 'name': '小强', 'hobbies': ['吃饭', '睡觉'] },
]
};
renderTemplateParseArray()函数就要递归调用renderTemplate函数3次,因为数组长度是3
*/
export default function renderTemplateParseArray(token, data) {
// 结果字符串
let resultStr = ''
// 得到对应的数组 ['#', 'students', []] // 也就是得到students
let arr = getValueByPointKey(data, token[1])

for(let i = 0; i < arr.length; i++) {
// 这里要补一个“.”属性
resultStr += renderTemplate(token[2], {
...arr[i],
'.': arr[i]
})
}
return resultStr
}

在index.js中调用

1
2
3
4
5
6
7
8
9
import parseTemplateToTokens from './parseTemplateToTokens.js'
import renderTemplate from './renderTemplate.js'
window.Mustache = {
render(templateStr, data) {
const tokens = parseTemplateToTokens(templateStr)
const domStr = renderTemplate(tokens, data)
return domStr
}
}

效果如下:

image-20230701162218105

去掉多多余空格

新增处理多余的空格逻辑处理,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import Scanner from './Scanner.js'
import nestTokens from './nestTokens.js'
/**
* 将模板字符串变为tokens数组
* @param {[type]} templateStr [description]
* @return {[type]} [description]
*/
export default function parseTemplateToTokens(templateStr) {
let tokens = []
// 创建扫描器
const scanner = new Scanner(templateStr)
let words
while(!scanner.eos()) {
// 获取开始标记({{)之前的文字
words = scanner.scanUtil('{{')
if (words != '') {
` // 新添加处理空白字符开始
// 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格
let isInTag = false // 标记是否在标签里
let _words = ''
for (let i = 0; i < words.length; i++) {
// 判断是否在标签里
if(words[i] == '<') {
isInTag = true
} else if (words[i] == '>') {
isInTag = false
}
// 如果这项不是空格, 拼接上
if(!/\s/.test(words[i])) {
_words += words[i]
} else {
// 如果这项是空格,只有当它在标签内的时候,才拼接上
if (isInTag) {
_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('}}')
}
return nestTokens(tokens)
}

image-20230701163956179

虚拟DOM和diff算法

image-20230702152802030

diff是发生在虚拟DOM上的

image-20230702152852888

snabbdom

snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;

官方git:https://github.com/snabbdom/snabbdom

新建snabbdom-diff文件夹

1
2
npm init
npm i snabbdom

snabbdom库是DOM库,当然不能在nodejs环境运行,所以我们需要搭建webpack和webpack-dev-server开发环境,好消息是不需要安装任何loader

• 这里需要注意,必须安装最新版webpack@5,不能安webpack@4,这是因为webpack4没有读取身份证中exports的能力,建议大家使用这样的版本:

1
npm i -D webpack@5 webpack-cli@4 webpack-dev-server@4

修改package.json中的scripts

1
"dev": "webpack-dev-server"

image-20230702170436451

新建webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path')
module.exports = {
mode: 'development',
//入口
entry:'./src/index.js',
//打包到什么文件
output:{
filename:'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
//配置webpack-dev-server
devServer:{
//静态文件根目录
static: {
directory: path.resolve(__dirname, 'dist'),
},
//不压缩
compress:false,
//端口号
port:8080
}
}

新建src/index.js

1
alert('hello')

新建dist/index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>index.html</h1>
<div id='container'></div>
<script src="./bundle.js"></script>
</body>
</html>

测试配置是否成功

1
npm run dev

image-20230702162026040

在src/index.js中引入官方示例

image-20230702170648418

注意要现在dist/index.heml中创建一个id为container的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";

const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);

const container = document.getElementById("container");

const vnode = h("div#container.two.classes", { on: { click: () => console.log("div clicked") } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);

const newVnode = h(
"div#container.two.classes",
{ on: { click: () => console.log("updated div clicked") } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

http://localhost:8080/

image-20230702170715255

虚拟DOM和h函数

虚拟DOM:用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。

image-20230702170951725

h函数用来产生虚拟节点(vnode)

1
2
const newVnode = h('a',{props: {href: 'http://www.baidu.com'}}, '百度')
console.log(newVnode)
1
2
3
4
5
6
7
8
9
10
{
"sel": "a",
"data": {
"props": {
"href": "http://www.baidu.com"
}
},
"text": "百度",
"elm": {}
}

创建一个虚拟结点,并且渲染到页面上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";

const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);

const newVnode = h('a',{props: {href: 'http://www.baidu.com', target: '_blank'}}, '百度')
console.log(newVnode)

const container = document.getElementById("container");

patch(container, newVnode);

h函数嵌套

1
2
3
4
5
6
7
8
9
// 中间的{}可以省略不写
const newVnode2 = h('ul',{},[
h('li','我是第一个小li'),
h('li', [
h('span', '我是第二个li,我嵌套了一个span')
]),
h('li','我是第三个小li')
])
console.log(newVnode2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"sel": "ul",
"data": {},
"children": [
{
"sel": "li",
"data": {},
"text": "我是第一个小li",
"elm": {}
},
{
"sel": "li",
"data": {},
"children": [
{
"sel": "span",
"data": {},
"text": "我是第二个li,我嵌套了一个span",
"elm": {}
}
],
"elm": {}
},
{
"sel": "li",
"data": {},
"text": "我是第三个小li",
"elm": {}
}
],
"elm": {}
}

手写h函数

src/mysnabbdom/vnode.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 生成虚拟结点
* @param {[type]} sel [description]
* @param {[type]} data [description]
* @param {[type]} children [description]
* @param {[type]} text [description]
* @param {[type]} elm [description]
* @return {[type]} [description]
*/
export default function vnode(sel, data, children, text, elm) {
const key = data === undefined ? undefined : data.key
return {
sel,
data,
children,
text,
elm,
key
}
}

src/mysnabbdom/h.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import vnode from './vnode.js'
/**
* [h description]
* @param {[type]} sel [description]
* @param {[type]} data [description]
* @param {[type]} c [description]
* @return {[type]} [description]
* 也就是说,调用的时候形态必须是下面的三种之一:
* 形态① h('div', {}, '文字')
* 形态② h('div', {}, [])
* 形态③ h('div', {}, h())
*/
export default function h(sel, data, c) {
// 检查参数的个数
if(arguments.length != 3) {
throw new Error('必须传递三个参数')
}
// 检查参数c的类型
if (typeof c == 'string' || typeof c == 'number') {
// 形态① h('div', {}, '文字')
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
// 形态② h('div', {}, [])
let children = []
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('传入的参数中某一项不是h函数')
}
children.push(c[i])
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// 形态③ h('div', {}, h())
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('传入的参数有误')
}
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import h from './mysnabbdom/h.js'

const newVnode = h('a',{props: {href: 'http://www.baidu.com', target: '_blank'}}, '百度')

console.log(newVnode)

// 中间的{}可以省略不写
const newVnode2 = h('ul',{},[
h('li',{},'我是第一个小li'),
h('li',{}, [
h('span',{}, '我是第二个li,我嵌套了一个span')
]),
h('li',{},'我是第三个小li')
])
console.log(newVnode2)

diff算法

  • 虚拟Dom中的key是这个节点的唯一标识,告诉dif算法,在更改前后它们是同一个DOM节点。

  • 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。

    • 选择器相同并且key值相同就是同一个虚拟结点。(创建元素时支持 div#id.calssName)
    • image-20230703110354789
  • 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你,而是暴力删除旧的、然后插入新的。

    • image-20230703110156766

diff算法体验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom"

const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
])

const container = document.getElementById("container")
const btn = document.getElementById("btn")

const ul1 = h('ul', [
h('li', '1'),
h('li', '2'),
h('li', '3'),
h('li', '4')
])

patch(container, ul1)

const ul2 = h('ul', [
h('li', '1'),
h('li', '2'),
h('li', '3'),
h('li', '4'),
h('li', '5')
])

btn.onclick = function() {
patch(ul1, ul2)
}

image-20230703111539567

手动将第一个li的内容改了以后,然后更换Dom发现只是增加了最后一个<li>5</li>,并没有全部替换

image-20230703111728330

1
2
3
4
5
6
7
const ul2 = h('ul', [
h('li', '5'),
h('li', '1'),
h('li', '2'),
h('li', '3'),
h('li', '4')
])

当我们把5添加到第一个,并且手动修改第一个和第二个li的内容,点击更换DOM之后发现全部被替换了

image-20230703112128541

当我们给虚拟结点加上key之后,就不会全部删除然后替换了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const container = document.getElementById("container")
const btn = document.getElementById("btn")

const ul1 = h('ul', [
h('li', {key: 1}, '1'),
h('li', {key: 2}, '2'),
h('li', {key: 3}, '3'),
h('li', {key: 4}, '4')
])

patch(container, ul1)

const ul2 = h('ul', [
h('li', {key: 5}, '5'),
h('li', {key: 1}, '1'),
h('li', {key: 2}, '2'),
h('li', {key: 3}, '3'),
h('li', {key: 4}, '4')
])

btn.onclick = function() {
patch(ul1, ul2)
}

image-20230703112602924

diff算法处理新旧结点流程

image-20230703110249655

手写diff算法和path函数

前面h函数已经生成了虚拟结点,现在需要把虚拟结点转变为真正的DOM结点

src/mysnabbdom/createElement.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 创建DOM结点,将vnode转为真正的DOM
* vnode: {el,data,children,text,elm,key}
* @param {[type]} vnode [description]
* @return {[type]} [description]
*/
export default function createElement(vnode) {
let DomNode = document.createElement(vnode.sel)
// 添加属性
if (vnode.data && vnode.data.props) {
const keys = Object.keys(vnode.data.props)
for(let i = 0; i < keys.length; i++) {
DomNode.setAttribute(keys[i], vnode.data.props[keys[i]])
}
}
// 只有文字,没有children
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)){
DomNode.innerText = vnode.text
} else if(Array.isArray(vnode.children) && vnode.children.length > 0){
for (let i = 0; i < vnode.children.length; i++) {
let vnode1 = vnode.children[i]
// 真实的DOM
let node = createElement(vnode1)
DomNode.appendChild(node)
}
}
// 补充elm属性
vnode.elm = DomNode

return vnode.elm
}

在src/index.js添加代码测试是否可用(只是测试,后面会在index.js中删除)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import createElement from './mysnabbdom/createElement.js'
const newVnode = h('a',{props: {href: 'http://www.baidu.com', target: '_blank'}}, '百度')

console.log(createElement(newVnode))

// 中间的{}可以省略不写
const newVnode2 = h('ul',{},[
h('li',{},'我是第一个小li'),
h('li',{}, [
h('span',{}, '我是第二个li,我嵌套了一个span')
]),
h('li',{},'我是第三个小li')
])
console.log(createElement(newVnode2))

image-20230703154257214

将DOM结点渲染到页面

src/mysnabbdom/patch.js (不过如果是同一结点,还没有解决)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import vnode from './vnode.js'
import createElement from './createElement.js'
import patchVnode from './patchVnode.js'
/**
* [patch description]
* @return {[type]} [description]
* vnode(sel, data, children, text, elm)
*/
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数是DOM结点还是虚拟结点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 传入的第一个参数真实DOM结点,要转为虚拟DOM结点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
if(sameVnode(oldVnode, newVnode)) {
// 如果是同一结点
console.log('是同一结点')
// TODO
`patchVnode(oldVnode, newVnode)`
} else {
console.log('不是同一结点')
// 创建真正的dom结点
let newVnodeDom = createElement(newVnode)
// 插入到老结点之前
if(oldVnode.elm.parentNode && newVnodeDom) {
oldVnode.elm.parentNode.insertBefore(newVnodeDom, oldVnode.elm)
}
// 删除老结点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}

/**
* 判断是否是相同的虚拟结点
* @param {[type]} oldVnode [description]
* @param {[type]} newVnode [description]
* @return {[type]} [description]
*/
function sameVnode(oldVnode, newVnode) {
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
return true
} else {
return false
}
}

同一结点处理流程

image-20230703163204910

对比同一结点

src/mysnabbdom/patchVnode.js (目前还没有做的是两个都有children,就要做到精细化对比)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import createElement from './createElement.js'
`import updateChild from './updateChild.js'`
/**
* 同一结点处理
* @param {[type]} oldVnode [description]
* @param {[type]} newVnode [description]
* @return {[type]} [description]
* vnode{sel, data, children, text, elm, key}
*/
export default function patchVnode(oldVnode, newVnode) {
// 判断新旧node是否同一个对象
if(oldVnode === newVnode) {
console.log('是同一个对象, 不做处理')
return
} else if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
console.log('只替换了text')
// 新node有text
if (newVnode.text !== oldVnode.text) {
// 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。
oldVnode.elm.innerText = newVnode.text
}
} else {
// 新node没有text, 有children
// 判断老的有没有children
if(oldVnode.children != undefined && oldVnode.children.length > 0) {
// 新老结点都有children,这是复杂的情况
console.log('新老结点都有children')
// TODO
`updateChild(oldVnode.elm, oldVnode.children, newVnode.children)`
}else {
// 老的没有,新的有
// 清空老的结点里面的内容
oldVnode.elm.innerHTML = ''
// 遍历新的vnode的子节点,创建DOM,上树
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}

diff算法

传统更新结点,可能是采用两层for循环遍历来对比有哪些变化,(这种比较麻烦)

image-20230704110110037

diff算法更新策略

四种命中查找:

① 新与旧

② 新与旧

③ 新与旧(此种发生了,涉及移动节点,那么新后指向的节点,移动的旧后之后

④ 新与旧(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧前之前

命中一种就不再进行命中判断了

如果都没有命中,就需要用循环来寻找了。移动到oldStartIdx之前。

新增的情况

image-20230704111930598

先执行① 新前与旧前 对比, A和A对比, B和B对比, 到C和D的时候,没命中,就要换②

② 新后与旧后 C和E对比,命不中,换③

③新后与旧前 C和C对比,命中

image-20230704112015772

删除的情况

image-20230704112538972

多删除的情况。

先执行① 新前与旧前 对比, A和A对比, B和B对比, 到C和D的时候,没命中,就要换②

② 新后与旧后 D和E对比,命不中,换③

③新后与旧前 D和C对比,命不中,换④

④新前与旧后 D和E对比,还是命不中

最后只能通过循环遍历,在旧结点中找到D,先将虚拟节点置为undefined,并将结点放在旧前的前面。然后删除旧前和旧后之间的节点

image-20230704113031575

多删除复杂情况

先执行① 新前与旧前 ,A和E对比,命不中,换②

② 新后与旧后 M和E对比,命不中,换③

③新后与旧前 M和A对比,命不中,换④

④新前与旧后 E和E对比,命中,此时要把旧子节点的E的虚拟结点置为undefined,然后把这个结点移到旧前的前面

–>新前后移指向C

然后 ① 新前与旧前 ,C和A对比,命不中,换②

② 新后和旧后,M和D对比,命不中,换③

③新后和旧前,M和A对比,命不中,换④

④新前和旧后,C和D对比,命不中,这个时候就要循环遍历

找到C后置为undefined,然后把这个结点移到旧前的前面

–>新前后移指向M

然后 ① 新前与旧前 ,M和A对比,命不中,换②

② 新后和旧后,M和D对比,命不中,换③

③新后和旧前,M和A对比,命不中,换④

④新前和旧后,M和D对比,命不中,这个时候就要循环遍历

循环遍历也找不到M,就把M结点移到旧前的前面

最后新前>新后,要删除旧前到旧后之间的数据A,B,D

image-20230704145707227

① 新前与旧前 ,E和A对比,命不中,换②

② 新后和旧后,A和E对比,命不中,换③

③新后和旧前,A和A对比,命中

把旧前的A置为undefined,并且把新后指向的结点移到旧后后面

—>新后向上移,旧前向下移

③新后和旧前,B和B对比,命中

把旧前的B置为undefined,并且把新后指向的结点移到旧后后面

….

….

image-20230704151745071

src/mysnabbdom/patchVnode.js (diff算法核心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import patchVnode from './patchVnode.js'
import createElement from './createElement.js'
/**
* [updateChild description]
* @param {[type]} parentELm [description]
* @param {[type]} oldCh [description]
* @param {[type]} newCh [description]
* @return {[type]} [description]
*/
export default function updateChild(parentELm, oldCh, newCh) {

// 新前
let newStartIdx = 0
// 旧前
let oldStartIdx = 0
// 新后
let newEndIdx = newCh.length - 1
// 旧后
let oldEndIdx = oldCh.length - 1
// 新前结点
let newStartVnode = newCh[0]
// 旧前结点
let oldStartVnode = oldCh[0]
// 新后结点
let newEndVnode = newCh[newEndIdx]
// 旧后结点
let oldEndVnode = oldCh[oldEndIdx]

let keyMap = null

while(newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
console.log('☆')
if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx]
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx]
} else if(checkSameVnode(newStartVnode, oldStartVnode)) {
// 新前与旧前
console.log("命中①:新前与旧前")
// 会递归进行比较
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if(checkSameVnode(newEndVnode, oldEndVnode)) {
// 新后与旧后
console.log("命中②:新后与旧后")
patchVnode(oldEndVnode, newEndVnode)
newEndVnode = newCh[--newEndIdx]
oldEndVnode = oldCh[--oldEndIdx]
}else if(checkSameVnode(newEndVnode, oldStartVnode)) {
// 新后与旧前
console.log("命中③:新后与旧前")
patchVnode(oldStartVnode, newEndVnode)
// 当③新后与旧前命中的时候,此时要移动节点。移动新后值向的这个节点到老节点的旧后的后面
// 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
// 新后指向的节点,也就是旧前指向的节点,这里要用旧前,因为新后的elm为undefined,也就是说当同一结点的时候,并不会为让新的虚拟结点变为真实的dom结点
parentELm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
newEndVnode = newCh[--newEndIdx]
oldStartVnode = oldCh[++oldStartIdx]
} else if (checkSameVnode(newStartVnode, oldEndVnode)) {
// 新前与旧后
console.log("命中④:新前与旧后")
patchVnode(oldEndVnode, newStartVnode)
// 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
parentELm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
oldEndVnode = oldCh[--oldEndIdx]
}else {
// 四种命中都没有命中
console.log("四种都没有命中")
// 制作keyMap一个映射对象,这样就不用每次都遍历老对象了。
if (!keyMap) {
keyMap = {}
// 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
for(let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key
if (key != undefined) {
keyMap[key] = i
}
}
}
console.log(keyMap)
// 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
const idxInOld = keyMap[newStartVnode.key]
if (idxInOld == undefined) {
// 判断,如果idxInOld是undefined表示它是全新的项
// 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
parentELm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
// 如果不是undefined,不是全新的项,而是要移动
const oldMoveDom = oldCh[idxInOld]
patchVnode(oldMoveDom, newStartVnode)
// 把这项设置为undefined,表示我已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动,调用insertBefore也可以实现移动。
parentELm.insertBefore(oldMoveDom, oldStartVnode.elm)
}
// 指针下移,只移动新的头
newStartVnode = newCh[++newStartIdx];
}
}
// 看看有没有剩余的
if (newStartIdx <= newEndIdx) {
// 有需要新增的结点
console.log("新结点还有剩余结点,要把所有剩余的节点,都要插入到oldStartIdx之前")
// 插入的标杆
const before = oldCh[oldStartIdx] == null ? null : oldCh[oldStartIdx].elm
// 遍历新的newCh,添加到老的没有处理的之前
for(let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
// newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
parentELm.insertBefore(createElement(newCh[i]), before)
}
}else if (oldStartIdx <= oldEndIdx) {
console.log('old还有剩余节点没有处理,要删除项');
// 批量删除oldStart和oldEnd指针之间的项
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if(oldCh[i]){
parentELm.removeChild(oldCh[i].elm)
}
}
}
}

/**
* 判断是不是同一结点
* @param {[type]} a [description]
* @param {[type]} b [description]
* @return {[type]} [description]
*/
function checkSameVnode(a, b) {
if (a.sel === b.sel && a.key === b.key) {
return true
}
return false
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import h from './mysnabbdom/h.js'
import patch from './mysnabbdom/patch.js'

const container = document.getElementById("container")
const btn = document.getElementById("btn")

const ul1 = h('ul', {}, [
h('li', {key: 1}, '1'),
h('li', {key: 2}, '2')
])

const ul2 = h('ul', {}, [
h('li', {key: 5}, '5'),
h('li', {key: 1}, '1'),
h('li', {key: 2}, '2')
])

patch(container, ul1)



btn.onclick = function() {
patch(ul1, ul2)
}

数据响应式

vue2数据更新原理图

image-20230706094755545

MVVM模式

MVVM简介

数据变化,视图会自动变化

image-20230706095315199

侵入式和非侵入式

侵入性就是让用户代码产生对框架的依赖,这些代码不能直接脱离框架使用,不利于代码的复用。

非侵入性就是引入了框架,对现有的类结构没有影响,不需要实现框架某些接口或者特定的类。

image-20230706100103273

Object.defineProperty

数据劫持/ 数据代理

初始化项目

Object.defineProperty方法介绍

Object.defineproperty(obj, prop, descriptor)

  • obj : 第一个参数就是要在哪个对象身上添加或者修改属性

  • prop : 第二个参数就是一个字符串或 Symbol,指定了要定义或修改的属性键

  • desc : 配置项,一般是一个对象

第三个参数里面的配置

1
2
3
4
5
6
writable:	是否可重写
value: 当前值
get: 读取时内部调用的函数
set: 写入时内部调用的函数
enumerable: 是否可以遍历
configurable: 是否可再次修改配置项
  • configurable

    当设置为 false 时,该属性的类型不能在数据属性和访问器属性之间更改,且该属性不可被删除,且其描述符的其他属性也不能被更改(但是,如果它是一个可写的数据描述符,则 value 可以被更改,writable 可以更改为 false)。默认值为 false

  • enumerable

    当且仅当该属性在对应对象的属性枚举中出现时,值为 true默认值为 false

数据描述符还具有以下可选键值:

  • value

    与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。默认值为 undefined

  • writable

    如果与属性相关联的值可以使用赋值运算符更改,则为 true默认值为 false

访问器描述符还具有以下可选键值:

  • get

    用作属性 getter 的函数,如果没有 getter 则为 undefined。当访问该属性时,将不带参地调用此函数,并将 this 设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined

  • set

    用作属性 setter 的函数,如果没有 setter 则为 undefined。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this 设置为通过该属性分配的对象。默认值为 undefined

defineReactive函数

getter/setter需要变量周转才能工作

使用defineReactive函数不需要设置临时变量了,而是用闭包

src/defineReactive.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
*
* @param {[type]} obj [description]
* @param {[type]} key [description]
* @param {[type]} val [description]
* @return {[type]} [description]
*/
export default function defineReactive(obj, key, val) {
if (arguments.length == 2) {
val = obj[key]
}
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
get() {
console.log(`正在访问${obj}${key}属性`)
return val
},
set(newValue) {
if (newValue != val) {
console.log(`正在修改${obj}${key}属性`)
val = newValue
}
}
})
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import defineReactive from './defineReactive.js'

const obj = {
a: {
b: {
c: {
d: 3
}
}
},
text: 123
}

defineReactive(obj, 'a')
defineReactive(obj, 'text')
console.log(obj.a)
console.log(obj.text)
obj.text = 123456
obj.a.b.c.d = 1
console.log(obj.a)
console.log(obj.text)

递归侦测对象全部属性

image-20230706105021749

Observer类

将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)

的object

src/observe.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Observer from './Observer.js'
/**
* 为对象创建__ob__属性
* @param {[type]} value [description]
* @return {[type]} [description]
*/
export default function(value) {
// 如果value不是对象,什么都不做
if (typeof value != 'object') {
return;
}
let ob
if(typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
}else {
ob = new Observer(value)
}
return ob
}

src/utils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 定义对象的配置信息,因为__ob__是要对用户不可遍历的,
* 所以enumerable要设为false
* @param {[type]} obj [description]
* @param {[type]} key [description]
* @param {[type]} value [description]
* @param {[type]} enumerable [description]
* @return {[type]} [description]
*/
export function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}

src/Observer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import defineReactive from './defineReactive.js'
import { def } from './utils.js'
/**
* 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)
*/
export default class Observer {
constructor(value) {
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
def(value, '__ob__', this, false)
// 0bserver类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
this.walk(value)

}
// 循环遍历,为对象的每个属性添加侦听
walk(obj) {
for(let key in obj) {
defineReactive(obj, key)
}
}
}

src/defineReactive.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import observe from './observe.js'
/**
* 侦听对象
* @param {[type]} obj [description]
* @param {[type]} key [description]
* @param {[type]} val [description]
* @return {[type]} [description]
*/
export default function defineReactive(obj, key, val) {
if (arguments.length == 2) {
val = obj[key]
}
// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childObj = observe(val)
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
get() {
// let newobj = Object.keys(obj).join(.)
console.log(`正在访问${obj}下的${key}属性`)
return val
},
set(newValue) {
if (newValue !== val) {
console.log(`正在修改${obj}下的${key}属性`)
val = newValue
// 当设置了新值,这个新值也要被observe
childObj = observe(newValue);
}
}
})
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import observe from './observe.js'

const obj = {
a: {
b: {
c: {
d: 3
}
}
},
text: 123
}
observe(obj)
console.log(obj.a.b)
console.log(obj.a.b.c.d)
obj.text = {
a:{
d: 1
}
}
console.log(obj.text.a.d)

数组响应式

vue改写了数组的7个方法,push, pop, shift, unshift, splice, sort, reverse

  • Object.setPrototypeOf(o, arrayMethods) 设置o的原型为arrayMethods

  • o.__proto__ = arrayMethods 设置o的原型为arrayMethods

image-20230707104201779

src/array.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { def } from './utils.js'
// 得到数组原型
const arrPrototype = Array.prototype

// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrPrototype)
// 需要改写的7个数组方法
const methods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

methods.forEach(methodsName => {
// 备份原来的7个方法,因为功能不变,只是做了增强
const originMethod = arrPrototype[methodsName]
// def(obj, key, value, enumerable)
// 定义新的方法
def(arrayMethods, methodsName, function(){
const result = originMethod.apply(this, arguments)
// 把类对象变为数组
const args = [...arguments]
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
const ob = this.__ob__
// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
let inserted = [];
switch(methodsName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice格式是splice(下标, 数量, 插入的新项)
inserted = args.splice(2);
break;
}
// 判断有没有要插入的新项,让新项也变为响应的
if(inserted) {
// 循环遍历
ob.observerArray(inserted)
}
console.log(`监听到${methodsName}方法正在改变数组:${this}`)
return result
}, false)
})

src/Observer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import defineReactive from './defineReactive.js'
import { def } from './utils.js'
`import { arrayMethods } from './array.js'`
import observe from './observe.js'
/**
* 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)
*/
export default class Observer {
constructor(value) {
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
def(value, '__ob__', this, false)
// 0bserver类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
// 检查它是数组还是对象
` if (Array.isArray(value)) {
// 如果是数组,将这个数组的原型,指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
this.observerArray(value)

} else {`
this.walk(value)
` }`
}
// 循环遍历,为对象的每个属性添加侦听
walk(obj) {
for(let key in obj) {
defineReactive(obj, key)
}
}
` // 数组循环遍历
observerArray(arr) {
// 这里用个len存储数组的长度,是为了防止数组的长度发生变化
for (let i = 0, len = arr.length; i < len; i++) {
observe(arr[i])
}
}`
}

依赖收集

需要用到数据的地方,称为依赖

• Vue1.x,细粒度依赖,用到数据的DOM都是依赖;

• Vue2.x,中等粒度依赖,用到数据的组件是依赖;

在getter中收集依赖,在setter中触发依赖

Dep类和Watcher类

  • 把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,

成员中都有一个Dep的实例;

  • Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件

image-20230707165304603

  • 依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。

  • Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

  • 代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher 收集到Dep中。

image-20230708221222953

完整代码

src/utils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 定义对象的配置信息,因为__ob__是要对用户不可遍历的,
* 所以enumerable要设为false
* @param {[type]} obj [description]
* @param {[type]} key [description]
* @param {[type]} value [description]
* @param {[type]} enumerable [description]
* @return {[type]} [description]
*/
export function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}

src/observe.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Observer from './Observer.js'
/**
* 为对象创建__ob__属性
* @param {[type]} value [description]
* @return {[type]} [description]
*/
export default function(value) {
// 如果value不是对象,什么都不做
if (typeof value != 'object') {
return;
}
let ob
if(typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
}else {
ob = new Observer(value)
}
return ob
}

src/Observer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import defineReactive from './defineReactive.js'
import { def } from './utils.js'
import { arrayMethods } from './array.js'
import observe from './observe.js'
import Dep from './Dep.js'
/**
* 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)
*/
export default class Observer {
constructor(value) {
// 每一个Observer的实例身上,都有一个dep
`this.dep = new Dep()`
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
def(value, '__ob__', this, false)
// 0bserver类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
// 检查它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,将这个数组的原型,指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
this.observerArray(value)

} else {
this.walk(value)
}
}
// 循环遍历,为对象的每个属性添加侦听
walk(obj) {
for(let key in obj) {
defineReactive(obj, key)
}
}
// 数组循环遍历
observerArray(arr) {
// 这里用个len存储数组的长度,是为了防止数组的长度发生变化
for (let i = 0, len = arr.length; i < len; i++) {
observe(arr[i])
}
}
}

src/defineReactive.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import observe from './observe.js'
`import Dep from './Dep.js'`
/**
* 侦听对象
* @param {[type]} obj [description]
* @param {[type]} key [description]
* @param {[type]} val [description]
* @return {[type]} [description]
*/
export default function defineReactive(obj, key, val) {

`const dep = new Dep()`

if (arguments.length == 2) {
val = obj[key]
}
// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childObj = observe(val)
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
get() {
// let newobj = Object.keys(obj).join(.)
console.log(`正在访问${obj}下的${key}属性`)
` // 如果现在处于依赖收集阶段
if (Dep.target) {
dep.depend()
if (childObj) {
childObj.dep.depend()
}
}`
return val
},
set(newValue) {
if (newValue !== val) {
console.log(`正在修改${obj}下的${key}属性`)
val = newValue
// 当设置了新值,这个新值也要被observe
childObj = observe(newValue);

// 发布订阅模式,通知dep
dep.notify()
}
}
})
}

src/array.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { def } from './utils.js'
// 得到数组原型
const arrPrototype = Array.prototype

// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrPrototype)
// 需要改写的7个数组方法
const methods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

methods.forEach(methodsName => {
// 备份原来的7个方法,因为功能不变,只是做了增强
const originMethod = arrPrototype[methodsName]
// def(obj, key, value, enumerable)
// 定义新的方法
def(arrayMethods, methodsName, function(){
const result = originMethod.apply(this, arguments)
// 把类对象变为数组
const args = [...arguments]
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
const ob = this.__ob__
// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
let inserted = [];
switch(methodsName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice格式是splice(下标, 数量, 插入的新项)
inserted = args.splice(2);
break;
}
// 判断有没有要插入的新项,让新项也变为响应的
if(inserted) {
// 循环遍历
ob.observerArray(inserted)
}
console.log(`监听到${methodsName}方法正在改变数组:${this}`)

ob.dep.notify();

return result
}, false)
})

src/Dep.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let uid = 0
export default class Dep {
constructor() {
console.log('Dep构造类')
this.id = uid++
// 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
// 这个数组里面放的是Watcher的实例
this.subs = []
}
// 添加订阅
addSub(sub) {
this.subs.push(sub)
}
// 添加依赖
depend() {
// Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
if (Dep.target) {
this.addSub(Dep.target)
}
}
// 通知更新
notify() {
console.log('通知方法')
// 浅克隆一份
// 浅克隆一份
const subs = this.subs.slice();
for(let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}

src/Watcher.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import Dep from './Dep.js'
let uid = 0
export default class Watcher {
constructor(target, expression, callback) {
console.log('Watcher构造方法')
this.id = uid++
this.target = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
update() {
this.run()
}
get() {
// 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this
const obj = this.target
let value
// 只要能找,就一直找
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const value = this.get()

if (value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
cb.call(this.target, value, oldValue)
}
}
}

/**
* 解析点对象语法, a.b.c.d
* @param {[type]} str [description]
* @return {[type]} [description]
*/
function parsePath(str) {
let segments = str.split('.')

return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import observe from './observe.js'
import Watcher from './Watcher.js'

const obj = {
a: {
b: {
c: {
d: 3
}
}
},
text: 123,
arr: [1, [2,3], {
name: 'zhangsan'
}]
}
observe(obj)
// console.log(obj.a.b)
// console.log(obj.a.b.c.d)
// obj.text = {
// a:{
// d: 1
// }
// }
// console.log(obj.text.a.d)
// obj.arr.push(4)
// obj.arr.pop()
new Watcher(obj, 'a.b.c', (val) => {
console.log('★我是watcher,我在监控a.m.n', val)
})
obj.a.b.c = 100
console.log(obj)

AST抽象语法树

直接将模版语法编译为HTML语法是非常困难的

image-20230709105703717

image-20230709105747466

抽象语法树本质是一个js对象

image-20230709105928035

抽象语法树和虚拟结点的关系

image-20230709110032204

相关算法储备

指针思想

试寻找字符串中,连续重复次数最多的字符。

‘aaaabbbbbcccccccccccccdddddd’

指针就是下标,不是C语言中的指针,C语言中的指针可以操作内存。JS中的指针

就是一个下标位置。

i: 0

j: 1

  • 如果i和j指向的字一样,那么i不动,j后移

  • 如果i和j指向的字不一样,此时说明它们之间的字都是连续相同的,让i追上j,j后移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const str = 'aaaabbbbbcccccccccccccdddddd'
function findMaxRepeat(str) {
let i = 0, j = 1
let max = 0
// j从1开始的,所以循环 str.length - 1 次
for (let k = 0; k < str.length - 1; k++) {
// 也可以使用str.charAt(i)
if (str[i] !== str[j]) {
if (j - i > max) {
max = j - i
console.log('max', max)
}
i = j
}
j++
}
return max
}
console.log(findMaxRepeat(str))

递归深入

试输出斐波那契数列的前10项,即1、1、2、3、5、8、13、21、34、55。然后请思考,代码是否有大量重复的计算?应该如何解决重复计算的问题?

image-20230709140045201

cache思想,利用缓存把已经求过的值存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let cache = {}
function fib(n) {
console.count('计数')
// 先去缓存里面,也可以cache.hasOwnProperty()
if (n in cache) {
console.log('命中缓存,从缓存中拿', n, cache[n])
return cache[n]
}
const v = n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2)
cache[n] = v
return v
}
for(let i = 0; i < 10; i++) {
console.log(fib(i))
}

形式转换:试将高维数组[1, 2, [3, [4, 5], 6], 7, [8], 9]变为图中所示的对象

image-20230709141629521

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 写法1        
const arr = [1, 2, [3, [4, 5], 6], 7, [8], 9]
function converT(arr) {
let result = []
arr.forEach(item => {
if (Array.isArray(item)) {
result.push({
children: converT(item)
})
} else {
result.push({
value: item
})
}
})
return result
}
console.log(converT(arr))


// 转换函数写法2,参数不是arr这个词语,而是item,意味着现在item可能是数组,也可能是数字
// 即,写法1的递归次数要大大小于写法2。因为写法2中,遇见什么东西都要递归一下。
function converT2(item) {
if (Array.isArray(item)) {
return {
children: item.map(_item => converT2(_item))
}
} else {
return {
value: item
}
}
}
console.log(converT2(arr))

  • 栈(stack)又名堆栈,它是一种运算受限的线性表,仅在表尾能、进行插入和删除操作。这一端被称为栈顶,相对地,把另一端称为栈底。

  • 向一个栈插入新元素又称作进栈、入栈或压栈;从一个栈删除元素 又称作出栈或退栈。

  • 后进先出(LIFO)特点:栈中的元素,最先进栈的必定是最后出栈,后进栈的一定会先出栈

JavaScript中,栈可以用数组模拟。需要限制只能使用push()和pop(),不能使用unshift()和shift()。即,数组尾是栈顶。

  • 当然,可以用面向对象等手段,将栈封装的更好。

image-20230709205721741

题目

试编写“智能重复”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]

image-20230709210042396

image-20230709210059477

正则相关方法

image-20230709210146906

image-20230709210640574

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function smartRepeat(template) {
// stack1用来存储数字
// stack2用来存储字母
const stack1 = []
const stack2 = []
let index = 0
// 当index = template.length - 1的时候,此时只剩下最后一个]
while(index < template.length - 1) {
// 剩余的字符串
let rest = template.substring(index)
// 查看剩余的是不是以数字开头
if (/^\d+\[/.test(rest)) {
// 取出数字,可能不是一位
console.log(stack1, stack2)
let num = Number(rest.match(/^(\d+)\[/)[1])
stack1.push(num)
// 遇到[,stack2要加入空字符串
stack2.push('')
// 同时跳过[括号
index += (num.toString().length + 1)
} else if (/^\w+\]/.test(rest)) {
// 找出这个字母或者单词
let word = rest.match(/^(\w+)\]/)[1]
stack2[stack2.length - 1] = word
index += word.length
} else if (rest[0] == ']') {
let num = stack1.pop()
let word = stack2.pop()
// repeat是ES6的方法,比如'a'.repeat(3)得到'aaa'
stack2[stack2.length - 1] += word.repeat(num)
index++
}
console.log(index, stack1, stack2)
}
// while结束之后,stack1和stack2中肯定还剩余1项。返回栈2中剩下的这一项,重复栈1中剩下的这1项次数,组成的这个字符串。如果剩的个数不对,那就是用户的问题,方括号没有闭合。
return stack2[0].repeat(stack1[0]);
}
// str = '2[1[a]3[b]2[3[c]4[d]]]'
str = '3[2[3[a]1[b]]4[d]]'
console.log(smartRepeat(str))

手写AST抽象语法树

image-20230710103018794

初始化项目

src/parse.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import parseAttrs from './parseAttrs'
/**
* 解析html字符串为抽象语法树
* @param {*} htmlStr
*/
export default function parse(htmlStr) {
// 指针
let index = 0
// 剩余部分
let rest= ''
// 标签开始正则
let tagStartRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/
// 标签结束正则
let tagEndRegExp = /^\<\/([a-z]+[1-6]?)\>/
// 开始标签和结束标签之间的文字
let wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/

// 栈1,存储标签
// 栈2,存储内容
const stack1 = []
// 这里初始有内容,是为了解决最后全部会弹出栈的问题
const stack2 = [{'children': []}]
while(index < htmlStr.length - 1) {
rest = htmlStr.substring(index)
if(tagStartRegExp.test(rest)) {
// 匹配到开始标签
const tag = rest.match(tagStartRegExp)[1]
let attrsString = rest.match(tagStartRegExp)[2]
// console.log('属性', attrsString)
// 开始标签入栈
stack1.push(tag)
stack2.push({'tag': tag, 'children': [], 'attrs': parseAttrs(attrsString)})
// console.log('匹配到开始标签', '<' + tag + '>')
const attrLength = attrsString ? attrsString.length : 0
// +2是要加上<>
index += tag.length + 2 + attrLength
} else if (tagEndRegExp.test(rest)) {
// 匹配到结束标签
const tag = rest.match(tagEndRegExp)[1]
let pop_tag = stack1.pop()
if(tag === pop_tag) {
// console.log('匹配到结束标签', '</' + tag + '>')
let pop_arr = stack2.pop()
if(stack2.length > 0) {
stack2[stack2.length - 1].children.push(pop_arr)
}
} else {
throw Error(stack1[stack1.length - 1] + '标签匹配错误')
}
index += tag.length + 3
} else if(wordRegExp.test(rest)) {
const word = rest.match(wordRegExp)[1]
// 判断word是不是全空
if(!/^\s+$/.test(word)) {
// console.log('匹配文字', word)
// 改变此时stack2栈顶元素中
stack2[stack2.length - 1].children.push({'text': word, 'type': 3})
}
index += word.length
} else {
index++
}
}
return stack2[0].children[0]
}

src/parseAttrs.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 解析属性字符串
*/
export default function parseAttrs(attrsString) {
if(attrsString === undefined) return []
// 判断当前是否在引号内
let isQuotation = false
// 断点
let point = 0
// 结果数组
let result = []

for(let i = 0; i < attrsString.length; i++) {
let ch = attrsString[i]
if(ch == '"' || ch == "'") {
isQuotation = !isQuotation
}else if (ch == ' ' && !isQuotation) {
// 是空格,且不在引号内(代表是分隔符)
if(!/^\s*$/.test(attrsString.substring(point, i))) {
result.push(attrsString.substring(point, i).trim())
point = i
}
}
}
// 循环结束之后,最后还剩一个属性k="v"
result.push(attrsString.substring(point).trim())

// 将["k=v","k=v","k=v"]变为[{name:k, value:v}, {name:k, value:v}, {name:k,value:v}];
result = result.map(item => {
// 根据“=”号拆分
const o = item.match(/^(.+)=["'](.+)["']/)
return {
name: o[1],
value: o[2]
}
})
return result
}

src/index.js

1
2
3
4
5
6
7
8
9
10
11
import parse from './parse'
const htmlStr = `<div class="aa bb cc" data-n="7" id="mybox">
<h3 name='abc'>你好</h3>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>`
const ast = parse(htmlStr)
console.log(ast)

指令和生命周期

初始化项目

createDocumentFragment()用法

参考文章:https://blog.csdn.net/weixin_43606809/article/details/97916174

  1. createDocumentFragment()方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
  2. DocumentFragment节点不属于文档树,继承的parentNode属性总是null。它有一个很实用的特点,当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点,即插入的是括号里的节点。这个特性使得DocumentFragment成了占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
  3. 如果使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。

另外,当需要添加多个dom元素时,如果先将这些元素添加到DocumentFragment中,再统一将DocumentFragment添加到页面,会减少页面渲染dom的次数,效率会明显提升。

因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

nodeType

nodeType 属性以数字形式返回指定节点的节点类型。

  • 如果节点是元素节点,则 nodeType 属性将返回 1
  • 如果节点是属性节点,则 nodeType 属性将返回 2
  • 如果节点是文本节点,则 nodeType 属性将返回 3
  • 如果节点是注释节点,则 nodeType 属性将返回 8

[].slice.call

参考文章:https://blog.csdn.net/qq_23878541/article/details/90413123

1
[].slice === Array.prototype.slice // true

[]和Array.prototype的区别

  1. 自身的属性不同(因为原型与[]的区别)

    1
    2
    3
    4
    5
    Object.getOwnPropertyNames(Array.prototype)
    (37) ["length", "constructor", "concat", "pop", "push", "shift", "unshift", "slice", "splice", "includes", "indexOf", "keys", "entries", "forEach", "filter", "map", "every", "some", "reduce", "reduceRight", "toString", "toLocaleString", "join", "reverse", "sort", "lastIndexOf", "copyWithin", "find", "findIndex", "fill", "remove", "removeFirstIf", "removeIf", "repeat", "last", "lastDef", "clone"]

    Object.getOwnPropertyNames([])
    ["length"]

    所以在本质上[]和Array.prototype没有本质区别,但是调用上是有区别的,但是根据专业检测,[]要更快一点

slice 这个方法在不接受任何参数的时候会返回 this 本身

手写Vue

src/Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import observe from './observe'
import Compile from './Compile'
import Watcher from './Watcher'
export default class Vue{
constructor(options) {
// 把参数options对象存为$options
this.$options = options || {}
// 数据
this._data = options.data || {}
observe(this._data)

// 默认数据要变为响应式的,这里就是生命周期
this._initData()
// 调用默认的watch
this._initWatch()

// 模板编译
new Compile(options.el, this)
}

_initData() {
let self = this
Object.keys(this._data).forEach(key => {
Object.defineProperty(self, key, {
get: () => {
return self._data[key]
},
set: (newVal) => {
self._data[key] = newVal
}
})
})
}

_initWatch() {
let self = this
const watch = this.$options.watch
Object.keys(watch).forEach(key => {
new Watcher(self, key, watch[key])
})
}
}

src/Compile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import Watcher from './Watcher'
export default class Compile {
constructor(el, vue) {
console.log('vue实例被创建了')
// vue实例
this.$vue = vue
// 挂载点
this.$el = document.querySelector(el)
// 如果用户传入了挂载点
if(this.$el) {
// 调用函数,让节点变为fragment,类似于mustache中的tokens。实际上用的是AST,这里就是轻量级的,fragment
let $fragment = this.node2Fragment(this.$el);
this.compile($fragment)
// 替换好的内容要上树
this.$el.appendChild($fragment);
}
console.log(this.$el, vue)
}
// 创建文档碎片(虚拟结点对象)
node2Fragment(el) {
let fragment = document.createDocumentFragment()
let child
// 让所有DOM节点,都进入fragment
while (child = el.firstChild) {
// 添加了之后,el.firstChild就会被删除了
fragment.appendChild(child)
}
return fragment
}
// 编译
compile(el) {
console.log('编译', el)
let childNodes = el.childNodes
let self = this

// 匹配{{}}中间的变量正则表达式
let reg = /\{\{(.*)\}\}/
childNodes.forEach(node => {
let text = node.textContent
console.log('text', text, node)
if(node.nodeType == 1) {
// nodeType为1为元素节点
self.compileElement(node)
} else if(node.nodeType == 3 && reg.test(text)) {
// nodeType为3
let name = text.match(reg)[1]
self.compileText(node, name)
}
// console.log('node结点', node.textContent)
})
}
// 编译元素结点
compileElement(node) {
console.log('元素结点', node)
// 获取元素上的所有属性
let nodeAttrs = node.attributes
let self = this
console.log('元素上的属性', nodeAttrs)
let reg = /\{\{(.*)\}\}/
let content = node.textContent
if(reg.test(content)) {
let name = content.match(reg)[1]
new Watcher(self.$vue, name, name => {
node.textContent = name
})
// attrValue 可能是a.b.c这种形式
let v = self.getVueVal(self.$vue, name)
node.textContent = v
}
// 类数组对象变为数组
const arr = []
arr.slice.call(nodeAttrs).forEach(attr => {
// 分析指令 (type="text", v-model="msg")
let attrName = attr.name // type v-model
let attrValue = attr.value // text msg
// 指令都是v-开头的
let dir = attrName.substring(2)
if(attrName.indexOf('v-') == 0) {
console.log(console.log('识别到的指令', dir))
// v-开头的就是指令
if(dir == 'model') {
new Watcher(self.$vue, attrValue, attrValue => {
node.value = attrValue
})
// attrValue 可能是a.b.c这种形式
let v = self.getVueVal(self.$vue, attrValue)
node.value = v

// 添加监听
node.addEventListener('input', e => {
const newVal = e.target.value
self.setVueVal(self.$vue, attrValue, newVal)
v = newVal
})
}else if (dir == 'if') {
console.log("if指令")
}
}
})
}
// 编译文本结点
compileText(node, name) {
node.textContent = this.getVueVal(this.$vue, name)
new Watcher(this.$vue, name, value => {
node.textContent = value
})
}

// 获取值 比如(a.b.c)
getVueVal(vue, exp) {
let val = vue
exp = exp.split('.')
exp.forEach(k=> {
val = val[k]
})
return val
}
// 设置值
setVueVal(vue, exp, value) {
let val = vue
exp = exp.split('.')
exp.forEach((k, i) => {
if (i < exp.length - 1) {
val = val[k]
} else {
val[k] = value
}
})
}
}

src/index.js

1
2
3
import Vue from './Vue'

window.Vue = Vue

www/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>index.html</h1>
<div id="app">
你好{{b.m.n}}
<br />
<li>{{a}}</li>
<input type="text" v-model="b.m.n">
</div>
<button onclick="add()">按我加1</button>
<script src="/xuni/bundle.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
a: 10,
b: {
m: {
n: 7
}
}
},
watch: {
a() {
console.log('a改变啦');
}
}
})
console.log(vm)
function add() {
vm.b.m.n++;
}
</script>
</body>
</html>

Vue原理

参考文章

template编译为抽象语法树 AST

  • 提取变量的逻辑

.match()方法,提取字符串中的关键词,例如标签名div,Mustache标签对应的变量msg

1
2
'<div>{{msg}}</div>'.match(/\{\{((?:.|\r?\n)+?)\}\}/)
// ["{{msg}}","msg"]

image-20230710162158255

(?:.|\r?\n):这是一个非捕获组,用于匹配除换行符以外的任意字符。

获得了"msg"标签对应变量的名称,我们就能在后续拼接出渲染DOM所需要的_vm.msg

即从我们声明的实例new Vue({data() {return {msg: 'abc'}}})中提取出msg: 'abc',渲染为 DOM 节点。

1
2
3
4
5
6
7
new Vue({
data() {
return {
msg: 'abc'
}
}
})
  • 遍历字符串的逻辑

<template>本质上是一段有大量HTML标签的字符串,我们需要遍历获取所有的标签、属性。

遍历方式的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import parseAttrs from './parseAttrs'
/**
* 解析html字符串为抽象语法树
* @param {*} htmlStr
*/
export default function parse(htmlStr) {
// 指针
let index = 0
// 标签开始正则
let tagStartRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/
// 标签结束正则
let tagEndRegExp = /^\<\/([a-z]+[1-6]?)\>/
// 开始标签和结束标签之间的文字
let wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/

vue源码是利用一个栈来存储的
// 栈1,存储标签
// 栈2,存储内容
const stack1 = []
// 这里初始有内容,是为了解决最后全部会弹出栈的问题
const stack2 = [{'children': []}]
while(index < htmlStr.length - 1) {
rest = htmlStr.substring(index)
if(tagStartRegExp.test(rest)) {
// 匹配到开始标签
const tag = rest.match(tagStartRegExp)[1]
let attrsString = rest.match(tagStartRegExp)[2]
// console.log('属性', attrsString)
// 开始标签入栈
stack1.push(tag)
stack2.push({'tag': tag, 'children': [], 'attrs': parseAttrs(attrsString)})
// console.log('匹配到开始标签', '<' + tag + '>')
const attrLength = attrsString ? attrsString.length : 0
// +2是要加上<>
`index += tag.length + 2 + attrLength`
} else if (tagEndRegExp.test(rest)) {
// 匹配到结束标签
const tag = rest.match(tagEndRegExp)[1]
let pop_tag = stack1.pop()
if(tag === pop_tag) {
// console.log('匹配到结束标签', '</' + tag + '>')
let pop_arr = stack2.pop()
if(stack2.length > 0) {
stack2[stack2.length - 1].children.push(pop_arr)
}
} else {
throw Error(stack1[stack1.length - 1] + '标签匹配错误')
}
index += tag.length + 3
} else if(wordRegExp.test(rest)) {
const word = rest.match(wordRegExp)[1]
// 判断word是不是全空
if(!/^\s+$/.test(word)) {
// console.log('匹配文字', word)
// 改变此时stack2栈顶元素中
stack2[stack2.length - 1].children.push({'text': word, 'type': 3})
}
index += word.length
} else {
index++
}
}
return stack2[0].children[0]
}

简单来说就是, while(html)循环,不断的html.match()提取模板中的信息。然后html = html.substring(n)删除掉n个已经遍历过的字符,直到html字符串为空,表示我们已经遍历、提取了全部的template

具体代码实现

1
2
3
4
5
6
7
8
9
10
11
12
// 将 Vue 实例的字符串模板 template 编译为 AST
class HTMLParser {}

// 基于 AST 生成渲染函数;
class VueCompiler {
HTMLParser = new HTMLParser()
}

// 基于渲染函数生成虚拟节点和真实 DOM
class Vue {
compiler = new VueCompiler()
}

正则表达式补充

参考文章:https://blog.csdn.net/weixin_39359534/article/details/118676496

元字符 描述
\ 将下一个字符标记符、或一个向后引用、或一个八进制转义符。例如,“\n”匹配\n。“\n”匹配换行符。序列“\”匹配“\”而“(”则匹配“(”。即相当于多种编程语言中都有的“转义字符”的概念。
^ 匹配输入字行首。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。
$ 匹配输入行尾。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。
* 匹配前面的子表达式任意次。例如,zo能匹配“z”,也能匹配“zo”以及“zoo”。等价于{0,}。
+ 匹配前面的子表达式一次或多次(大于等于1次)。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
? 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“do”或“does”。?等价于{0,1}。
{n} n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。
{n,} n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n,m} mn均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o为一组,后三个o为一组。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。
? 当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少地匹配所搜索的字符串,而默认的贪婪模式则尽可能多地匹配所搜索的字符串。例如,对于字符串“oooo”,“o+”将尽可能多地匹配“o”,得到结果[“oooo”],而“o+?”将尽可能少地匹配“o”,得到结果 [‘o’, ‘o’, ‘o’, ‘o’]
. 匹配除“\n”和”\r”之外的任何单个字符。要匹配包括“\n”和”\r”在内的任何字符,请使用像“[\s\S]”的模式。
(pattern) 匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“”或“”或“”。

Vue2源码解读
http://example.com/2023/06/25/01.前端/06.Vue/01.Vue2/03.Vue源码解读/
作者
Deng ErPu
发布于
2023年6月25日
许可协议