源码结构
数据与结构分离
分离的意义
程序员的需求
- 程序员写了一个html5、css3购物网站首页
- 页面中有经常更新的数据,比如商品名,商品价格,最新推出的活动名称。
程序员的选择
①手动更新:添加、删除、修改商品时,直接修改html代码
②数据与结构相分离
- 商品数据保存在对象data中
- html页面中的商品数据使用data对象中的key来占位,写法是{{key}}
- 编写js代码,将data中的value装入替换掉html中的{{key}}
纯DOM法
- 通过dom元素id获取对应dom元素
- 把data对象中的value替换对应的{{key}}。
字符串拼接变量数据法
①ES6推出之前
- 单引号或双引号包裹的字符串无法换行,采用数组join法来满足程序员的视觉需求
- 把html代码以字符串片段的形式存放在strHTML数组中
<body>
<div id="box"></div>
<script>
let box = document.querySelector('#box')
let data = { name: '小明', age: 18, sex: '男' }
let strHTML = [‘<p>姓名:' +data.name+ '</p>',
’<p>年龄: ' +data.age+ ' </p>‘,
’<p>性别: ' +data.sex+ ' </p>’]
box.innerHTML = strHTML
</script>
</body>
②ES6推出后:反引号包裹的字符串可以换行
<body>
<div id="box"></div>
<script>
let box = document.querySelector('#box')
let data = { name: '小明', age: 18, sex: '男' }
let strHTML = `<p>姓名:'${data.name}</p>
<p>年龄:${data.age}</p>,
<p>性别:${data.sex}</p>`
box.innerHTML = strHTML
</script>
</body>
模版引擎法
思路
- 把 html模版字符串 解析成 tokens数组(tokens数组可以理解为AST对象)
- 将 tokens数组和data数据结合,生成dataTokens数组
- dataToken数组再转换成dataStr字符串
- 将 dataStr字符串 赋值给对应 dom元素的 innerHTML
render函数&&strToAST函数
<body>
<script>
let ASTNode=function(tag,selection,text,children){
return {
tag,
selection,
children,
text,
}
}
let h=ASTNode
let AST=h(1,'div',undefined,[
h(3,undefined,'我是父元素的文本节点',undefined),
h(1,'h3',undefined,[h(3,undefined,'我是一个三级标题',undefined)]),
h(1,'div',undefined,[h(3,undefined,'我是一个div',undefined)]),
h(1,'span',undefined,[h(3,undefined,'我是一个span',undefined)])
])
console.log("render: ",AST)
// --------------------------------------------------------------
let strToAST=function(templateStrHTML){
let tagRE=/\<(.+?)\>/
let toArrRE=/\s+/
let matchArr
let nodeStack=[]
let selectionStack=[]
let flag=0
while(templateStrHTML.length!==0){
matchArr=templateStrHTML.match(tagRE)
// 生成文本节点
if(matchArr[1][0]==='/'){
let text=templateStrHTML.substring(0,matchArr['index'])
text=text.trim()
if(text.length!==0){
let ASTText=h(3,undefined,text,undefined)
nodeStack.push(ASTText)
}
let elementIndex=selectionStack.pop()
while(nodeStack.length>(elementIndex+1)){
let childh=nodeStack.pop()
nodeStack[elementIndex].children.unshift(childh)
}
}
// 判断是否需要生成元素节点
if(matchArr[1][0]!=='/'){
if(flag===1){
let text=templateStrHTML.substring(0,matchArr['index'])
text=text.trim()
if(text.length!==0){
let ASTText=h(3,undefined,text,undefined)
nodeStack.push(ASTText)
}
}
flag=1
let splitArr=matchArr[1].split(toArrRE)
let attribute={}
for(let i=splitArr.length-1;i>0;i--){
let keyAndValue=splitArr[i].split('=')
attribute[keyAndValue[0]]=keyAndValue[1]
}
let hChild=h(1,splitArr[0],undefined,[])
let nodeStackLength=nodeStack.push(hChild)
selectionStack.push(nodeStackLength-1)
}
templateStrHTML=templateStrHTML.substring(matchArr['index']+matchArr[0].length)
}
return nodeStack[0]
}
let str=`
<div>
我是父元素的文本节点
<h3>我是一个三级标题</h3>
<div>我是一个div</div>
<span>我是一个span</span>
</div>`
console.log("strToAST:",strToAST(str))
</script>
</body>
依赖收集与派发更新
概述
- 代理的精髓:读取变量、设置变量、调用方法之前,执行一段代码
- 依赖收集的时机:读取变量get时
- 派发更新的时机:设置变量set时
depMap存储数据依赖:depMap是一个Map集合,set1、set2是Set集合
get依赖收集
- 全局变量watchEffect指向函数test
- 读取studentMessage.name
- 调用studentMessage.name的get方法
- 调用collect方法
- 收集函数test到集合depMap中
function test(){
console.log("test方法")
let name=studentMessage.name //调用studentMessage的get方法
}
function effect(callback){ //watchEffect是全局变量
watchEffect=callback
callback()
watchEffect=null
}
get(target,key){ //studentMessage.name的get方法
collection()
return target[key]
}
function collection(){
if(!depMap.get(studentMessage.name)){
depMap.set(studentMessage.name,new Set())
}
let set1=depMap.get(studentMessage.name)
set1.add(watchEffect)
}
effect(test)
set派发更新
- 修改studentMessage.name=studentMessage.name+'123'
- 调用studentMessage.name的set方法
- 调用distribute方法
- 调用test方法
- 真的改变studentMessage.name
set(target,key,newValue){ //studentMessage.name的set方法
distribute()
target[key]=newValue
}
function distribute(){
let set1=depMap.get(studentMessage.name)
set1.forEach((fn)=>{
fn()
})
}
studentMessage.name=studentMessage.name+'123'
effect函数+代理+test
- test替换成render函数:实现页面响应式
- test替换成watch函数:实现watch监听
- test替换成compute函数: 实现compute计算属性
直接替换算法
解决方式:新vdom生成新rdom,新rdom替换旧rdom
不足之处
- 我们写一个页面,页面中有许多商品
- 当删除商品A的信息,下架商品A时,要对页面实时更新
- 如果不进行对比,就需要替换整个页面的DOM元素。
对比替换算法
替换条件
- 标签:标签名不同,会替换;标签名相同但是key值不同,会替换
- 文本:文本内容不相同,会替换
前提条件
- 父元素div内有多个相同标签名的子元素div
- 子元素div内只有文本内容,不再嵌套子标签
- 子元素的文本内容与key属性值相同
- 新旧vdom按序排列,使用key属性的值标识
↓b | ||||||||
数组下表 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚拟dom | A | B | C | D | E | F | ||
新虚拟dom | F | C | D | E | B | A | H | K |
↑a |
- ↑a所指的新虚拟dom[0]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 新虚拟dom[0]==旧虚拟dom[5],比对成功
- 把旧虚拟dom[5]unshift到旧虚拟dom[0]的前面
- 旧虚拟dom[5]设置为null,不再参与后续遍历
↓b | |||||||||
数组下表 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚拟dom | F | A | B | C | D | E | null | ||
新虚拟dom | F | C | D | E | B | A | H | K | |
↑a |
- ↑a所指的新虚拟dom[1]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 新虚拟dom[1]==旧虚拟dom[2],比对成功
- 把旧虚拟dom[2]unshift到旧虚拟dom[0]的前面
- 旧虚拟dom[2]设置为null,不再参与后续遍历
↓b | ||||||||||
数组下表 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | A | B | null | D | E | null | ||
新虚dom | F | C | D | E | B | A | H | K | ||
↑a |
- ↑a所指的新虚拟dom[2]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 新虚拟dom[2]==旧虚拟dom[3],比对成功
- 把旧虚拟dom[3]unshift到旧虚拟dom[0]的前面
- 旧虚拟dom[3]设置为 null,不再参与后续遍历
↓b | |||||||||||
数组下表 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | D | A | B | null | null | E | null | ||
新虚dom | F | C | D | E | B | A | H | K | |||
↑a |
- ↑a所指的新虚拟dom[3]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 新虚拟dom[3]==旧虚拟dom[4],比对成功,
- 把旧虚拟dom[4]unshift到旧虚拟dom[0]的前面
- 旧虚拟dom[4]设置为 null,不再参与后续遍历
↓b | ||||||||||||
数组下表 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | D | E | A | B | null | null | null | null | ||
新虚dom | F | C | D | E | B | A | H | K | ||||
↑a |
- ↑a所指的新虚拟dom[4]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 新虚拟dom[4]==旧虚拟dom[1],比对成功
- 把旧虚拟dom[4]unshift到旧虚拟dom[0]的前面
- 旧虚dom[4]设置为 null,不再参与后续遍历
↓b | |||||||||||||
数组下表 | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | D | E | B | A | null | null | null | null | null | ||
新虚dom | F | C | D | E | B | A | H | K | |||||
↑a |
- ↑a所指的新虚莫dom[5]与 ↓b所指的旧虚dom及其之后的旧虚拟dom逐一比对
- 新虚拟dom[5]==旧虚拟dom[0],比对成功,
- 把旧虚拟dom[0]unshift到旧虚拟dom[0]的前面
- 旧虚拟dom[0]设置为 null,不再参与后续遍历
↓b | ||||||||||||||
数组下表 | -6 | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | D | E | B | A | null | null | null | null | null | null | ||
新虚dom | F | C | D | E | B | A | H | K | ||||||
↑a |
- ↑a所指的新虚拟dom[6]与↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 比对不成功
- 使用createELement创建key值为H的div元素
- key值为H的div元素,unshift到到旧虚拟dom[0]的前面。
↓b | |||||||||||||||
数组下表 | -7 | -6 | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | D | E | B | A | H | null | null | null | null | null | null | ||
新虚dom | F | C | D | E | B | A | H | K | |||||||
↑a |
- ↑a所指的新虚拟dom[7]与↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
- 比对不成功
- 使用createELement创建key值为K的div元素
- key值为K的div元素,unshift到到旧虚拟dom[0]的前面
数组下表 | -8 | -7 | -6 | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
旧虚dom | F | C | D | E | B | A | H | K | null | null | null | null | null | null | ||
新虚dom | F | C | D | E | B | A | H | K |
这是你会发现旧虚拟dom元素的相对顺序与新虚拟dom元素的相对顺序一模一样
总结
循环比较
- ↑a 指向的新虚拟dom[i]遍历↓b指向旧虚拟dom及其后面的旧虚拟dom(i的初始值=0)
- 如果找到新虚拟dom[i]==旧虚拟dom[j],旧虚拟dom[j]unshift到旧虚拟dom[0]前面,并且旧虚拟dom[j]最初位置的值设置为null,下次不再遍历
- 如果没有找到对应的旧虚拟dom[j],那就创建一个旧虚拟dom[j](旧虚拟dom[j]==新虚拟dom[i]),旧虚拟dom[j] unshift 到 旧虚拟dom[0]前面
- ↑a 向前移动一位,指向新虚拟dom[i+1]
循环比较结束后
- 新虚dom的元素已经全部与旧虚dom进行比对
- 旧虚dom中如果还存在下标j大于0且值不为null的旧虚dom元素,一律删除
diff替换算法
一般对比算法只有一个箭头指针,而diff算法使用了四个箭头指针。
图示
比较顺序:
- ①箭头A和箭头C比较(如果比较成功,箭头A和箭头C同时下移)
- ②箭头B和箭头D比较(如果比较成功,箭头B和箭头D同时上移)
- ③箭头A和箭头D比较(如果比较成功,箭头A下移,箭头D上移,并且,把 箭头D所指的元素 unshift到 箭头C所指元素上方)
- ④箭头B和箭头C比较 (如果比较成功,箭头B上移,箭头C下移,并且,把 箭头C所指的元素 push到 箭头D所指元素的下方)
- ⑤如果都没有成功,采用一般比较算法
比较截止条件
下面两个条件满足其一即可
- 箭头B 移动到 箭头A 上面,
- 箭头D 移动到 箭头C 上面。
截至比较之后的操作
- 如果因为满足 条件一 而截止:箭头C与箭头D之间的元素(包含 箭头C 和 箭头D 所指的 元素)全部删除。
- 如果因为满足 条件二 而截止:箭头A与箭头B之间的元素(包含 箭头C 和 箭头D 所指的 元素)全部创建,并且整体 unshit 到箭头C所指元素的上面
消息订阅与发布
以mounted为例
- vue程序在时间点pointA,给事件mounted绑定了回调函数fun
- 组件挂载完成后,会触发事件mounted,并调用回调函数fun