学习Vue源码

源码结构

353c7ebd244e4d07863ee19e1cbbd709.jpeg

数据与结构分离

分离的意义

程序员的需求

  1. 程序员写了一个html5、css3购物网站首页
  2. 页面中有经常更新的数据,比如商品名,商品价格,最新推出的活动名称。

程序员的选择  

①手动更新:添加、删除、修改商品时,直接修改html代码

②数据与结构相分离

  1. 商品数据保存在对象data中
  2. html页面中的商品数据使用data对象中的key来占位,写法是{{key}}
  3. 编写js代码,将data中的value装入替换掉html中的{{key}}

纯DOM法

  1. 通过dom元素id获取对应dom元素
  2. 把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>

模版引擎法

思路

  1. 把 html模版字符串 解析成 tokens数组(tokens数组可以理解为AST对象)
  2. 将 tokens数组和data数据结合,生成dataTokens数组
  3. dataToken数组再转换成dataStr字符串
  4. 将 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>

631717761f8e49a2b53419493023fc4d.jpeg

依赖收集与派发更新

概述

  1. 代理的精髓:读取变量、设置变量、调用方法之前,执行一段代码
  2. 依赖收集的时机:读取变量get时
  3. 派发更新的时机:设置变量set时

depMap存储数据依赖:depMap是一个Map集合,set1、set2是Set集合

ba40b438ac304682952933bb8b78dac2.jpeg

get依赖收集

  1. 全局变量watchEffect指向函数test
  2. 读取studentMessage.name
  3. 调用studentMessage.name的get方法
  4. 调用collect方法
  5. 收集函数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派发更新

  1. 修改studentMessage.name=studentMessage.name+'123'
  2. 调用studentMessage.name的set方法
  3. 调用distribute方法
  4. 调用test方法
  5. 真的改变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

不足之处

  1. 我们写一个页面,页面中有许多商品
  2. 当删除商品A的信息,下架商品A时,要对页面实时更新
  3. 如果不进行对比,就需要替换整个页面的DOM元素。

对比替换算法

替换条件

  • 标签:标签名不同,会替换;标签名相同但是key值不同,会替换
  • 文本:文本内容不相同,会替换

前提条件

  • 父元素div内有多个相同标签名的子元素div
  • 子元素div内只有文本内容,不再嵌套子标签
  • 子元素的文本内容与key属性值相同
  • 新旧vdom按序排列,使用key属性的值标识
   ↓b
数组下表01234567
旧虚拟domABCDEF
新虚拟domFCDEBAHK
 ↑a
  1. ↑a所指的新虚拟dom[0]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 新虚拟dom[0]==旧虚拟dom[5],比对成功
  3. 把旧虚拟dom[5]unshift到旧虚拟dom[0]的前面
  4. 旧虚拟dom[5]设置为null,不再参与后续遍历
 ↓b
数组下表-101234567
旧虚拟domFABCDEnull
新虚拟domFCDEBAHK
 ↑a
  1. ↑a所指的新虚拟dom[1]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 新虚拟dom[1]==旧虚拟dom[2],比对成功
  3. 把旧虚拟dom[2]unshift到旧虚拟dom[0]的前面
  4. 旧虚拟dom[2]设置为null,不再参与后续遍历
    ↓b
数组下表-2-101234567
旧虚domFCABnullDEnull
新虚domFCDEBAHK
  ↑a
  1. ↑a所指的新虚拟dom[2]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 新虚拟dom[2]==旧虚拟dom[3],比对成功
  3. 把旧虚拟dom[3]unshift到旧虚拟dom[0]的前面
  4. 旧虚拟dom[3]设置为 null,不再参与后续遍历
   ↓b
数组下表-3-2-101234567
旧虚domFCDABnullnullEnull
新虚domFCDEBAHK
   ↑a
  1. ↑a所指的新虚拟dom[3]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 新虚拟dom[3]==旧虚拟dom[4],比对成功,
  3. 把旧虚拟dom[4]unshift到旧虚拟dom[0]的前面
  4. 旧虚拟dom[4]设置为 null,不再参与后续遍历
  ↓b
数组下表-4-3-2-101234567
旧虚domFCDEABnullnullnullnull
新虚domFCDEBAH

K

   ↑a
  1. ↑a所指的新虚拟dom[4]与 ↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 新虚拟dom[4]==旧虚拟dom[1],比对成功
  3. 把旧虚拟dom[4]unshift到旧虚拟dom[0]的前面
  4. 旧虚dom[4]设置为 null,不再参与后续遍历
 ↓b
数组下表-5-4-3-2-101234567
旧虚domFCDEBAnullnullnullnullnull
新虚domFCDEBAHK
    ↑a
  1. ↑a所指的新虚莫dom[5]与 ↓b所指的旧虚dom及其之后的旧虚拟dom逐一比对
  2. 新虚拟dom[5]==旧虚拟dom[0],比对成功,
  3. 把旧虚拟dom[0]unshift到旧虚拟dom[0]的前面
  4. 旧虚拟dom[0]设置为 null,不再参与后续遍历
   ↓b
数组下表-6-5-4-3-2-101234567
旧虚domFCDEBAnullnullnullnullnullnull
新虚domFCDEBAHK
 ↑a
  1. ↑a所指的新虚拟dom[6]与↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 比对不成功
  3. 使用createELement创建key值为H的div元素
  4. key值为H的div元素,unshift到到旧虚拟dom[0]的前面。
           ↓b
数组下表-7-6-5-4-3-2-101234567
旧虚domFCDEBAHnullnullnullnullnullnull
新虚domFCDEBAHK
   ↑a
  1. ↑a所指的新虚拟dom[7]与↓b所指的旧虚拟dom及其之后的旧虚拟dom逐一比对
  2. 比对不成功
  3. 使用createELement创建key值为K的div元素
  4. key值为K的div元素,unshift到到旧虚拟dom[0]的前面
数组下表-8-7-6-5-4-3-2-101234567
旧虚domFCDEBAHKnullnullnullnullnullnull
新虚domFCDEBAHK

这是你会发现旧虚拟dom元素的相对顺序与新虚拟dom元素的相对顺序一模一样

总结

循环比较

  1. ↑a 指向的新虚拟dom[i]遍历↓b指向旧虚拟dom及其后面的旧虚拟dom(i的初始值=0)
  2. 如果找到新虚拟dom[i]==旧虚拟dom[j],旧虚拟dom[j]unshift到旧虚拟dom[0]前面,并且旧虚拟dom[j]最初位置的值设置为null,下次不再遍历
  3. 如果没有找到对应的旧虚拟dom[j],那就创建一个旧虚拟dom[j](旧虚拟dom[j]==新虚拟dom[i]),旧虚拟dom[j] unshift 到 旧虚拟dom[0]前面
  4. ↑a 向前移动一位,指向新虚拟dom[i+1]

循环比较结束后

  1. 新虚dom的元素已经全部与旧虚dom进行比对
  2. 旧虚dom中如果还存在下标j大于0且值不为null的旧虚dom元素,一律删除

diff替换算法

一般对比算法只有一个箭头指针,而diff算法使用了四个箭头指针。

图示

e62b74e16e10448fac9314fb42a8d5b9.png

比较顺序:

  1. ①箭头A和箭头C比较(如果比较成功,箭头A和箭头C同时下移)
  2. ②箭头B和箭头D比较(如果比较成功,箭头B和箭头D同时上移)
  3. ③箭头A和箭头D比较(如果比较成功,箭头A下移,箭头D上移,并且,把 箭头D所指的元素 unshift到 箭头C所指元素上方)
  4. ④箭头B和箭头C比较 (如果比较成功,箭头B上移,箭头C下移,并且,把 箭头C所指的元素 push到 箭头D所指元素的下方)
  5. ⑤如果都没有成功,采用一般比较算法

比较截止条件

下面两个条件满足其一即可

  • 箭头B 移动到 箭头A 上面,
  • 箭头D 移动到 箭头C 上面。

截至比较之后的操作

  • 如果因为满足 条件一 而截止:箭头C与箭头D之间的元素(包含 箭头C 和 箭头D 所指的 元素)全部删除。
  • 如果因为满足 条件二 而截止:箭头A与箭头B之间的元素(包含 箭头C 和 箭头D 所指的 元素)全部创建,并且整体 unshit 到箭头C所指元素的上面 

消息订阅与发布

227e6cc1352c463abb25eaefc38a4b98.jpeg

 以mounted为例

  1. vue程序在时间点pointA,给事件mounted绑定了回调函数fun
  2. 组件挂载完成后,会触发事件mounted,并调用回调函数fun
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

stealPigs-youth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值