源码学习:手写Mini-Vue

源码学习:手写Mini-Vue

本文为笔者在学习vue源码过程中所作的笔记,剖析思路及代码实现来自于网课、网络文章和笔者自己的总结,笔者资历尚浅,写文章的目的在于加深理解,如何不妥之处,请在评论区友好地指出,讨论。

本文的内容仅可作为vue源码的入门,更详细深入的内容还得自己去看源码。本文要实现的内容可以参考官网来解释:

通过对 编译挂载更新的核心代码的实现(忽略一些边界判断),最终实现一个迷你版的Vue。

最终效果如下:

一、编译

具体的编译过程略写,这里只讲最简单的概念。

要理解这一部分的内容,首先得知道什么是虚拟DOM

1.1 虚拟DOM

虚拟DOM其实就是一个js对象,我们所写的html代码最终会被转化为一个对象。而在Vue中,template模板的内容最终会被编译成一个js对象。该对象简洁地描述了html代码在页面上的显示效果。

注意:VDom(虚拟DOM)描述了一整个Dom树的结构,VNode(虚拟节点)描述的是其中的一个节点

那么为什么要用虚拟DOM呢,有什么优势?

1、提升渲染性能,真实DOM是十分复杂的,DOM的属性和方法来自于DOM规范,并由浏览器的JavaScript引擎实现。利用console.dir('DOm节点'),我们可以方便的看到其上繁多的属性和方法。而虚拟DOM仅描述了你应用到的内容,其他内容暂时忽略,这样可以减少对真实DOM的频繁访问和操作,从而提升渲染性能

2、跨平台应用:虚拟DOM可以在不同平台和环境下进行渲染

3、方便patch新旧虚拟Dom,当我们将两个虚拟Dom进行对比时,省去了很多无关的内容,无疑提高了效率。

Vue提供了一个h函数,允许用它创建一个虚拟DOM对象

1.2H函数

我们所编写的template模板的内容,最终会被编译成一个render函数,render函数会返回h函数,而h函数创建了虚拟DOM。这里我们看一下h函数的实现

我们将h函数 进行简化,那么它的职责就是:返回一个VNode对象。下面看代码

虚拟节点:元素名(tag)、属性(props)、children(子节点)

javascript 复制代码
//renderer.js
const h = (tag, props, children) => {
    // 虚拟DOM就是一个js对象
    return {
        tag,
        props,
        children
    }
}

在index.html中引入

xml 复制代码
<body>
    <div id="app"></div>
    <script src="./renderer.js"></script>
    <script>
        // 1.通过h函数来创建一个vnode
        const vnode = h('div', { class: 'ming' }, [
            h("h2", null, "当前计数:100"),
            h("button", null, "+1")
        ])
        console.log(vnode);
    </script>
</body>

打开控制台可以看到

事实上,我们上边创建的vnode已经可以看成是一个虚拟DOM了

xml 复制代码
<div class="ming">
<h2>当前计数:100</h2>
<button>+1</button>
</div>

二、挂载

这一过程中,主要探索两个函数的实现,一个是mount 函数,一个是patch函数。

mount函数,用于将VNode转换为真实DOM,并挂载到页面上

patch函数 ,数据变化时生成新的VNode,用于对比两个VNode进行对比,并最终将变化的部分更新到页面上

2.1 mont函数

1、根据vnode创建出真实DOM,并处理相应的属性(如果有,则添加进新创建的节点中)

scss 复制代码
//renderer.js
const mount = (vnode, container) => {
    //将vnode -> 真实DOM
    // 1.创建出真实的原生节点,并且在vnode上保留el
    const el = vnode.el = document.createElement(vnode.tag)
​
    // 2.处理props
    //如果虚拟节点的props有值,则将值添加进新创建的节点上
    if(vnode.props) { 
        for(const key in vnode.props) {
            const value = vnode.props[key]
            el.setAttribute(key, value)
        }
    }
}

2、属性props的边界处理

dom元素的属性可能是这样子的,针对这种情况,我们需要对事件进行监听

ini 复制代码
<div onclick="function() {}"></div>
scss 复制代码
const mount = (vnode, container) => {
    //...
​
    // 2.处理props
    if (vnode.props) { 
        for (const key in vnode.props) {
            const value = vnode.props[key]
            if (key.startsWith("on")) { //对事件进行监听
                el.addEventListener(key.slice(2).toLowerCase(), value)
            } else {
                el.setAttribute(key, value)
            }
        }
    }
}

3、处理childern

如果vnode没有子节点,那么就直接跳过

如果vnoed有子节点,那么分为两种情况:

  • 1、子节点为文本字符串,直接将文本添加到新创建的真实的el元素上即可
  • 2、如果子节点是一个数组,说明vnode的子节点有其他节点需要处理,递归调用mount函数即可
ini 复制代码
// 3.处理childern
if (vnode.children) {
    if (typeof vnode.children === "string") { //如果是文本
        el.textContent = vnode.children
    } else { //其他情况视为数组
        vnode.children.forEach(item => {
            mount(item, el)
        })
    }
}

4、将el挂载到container上

scss 复制代码
//完整代码
const mount = (vnode, container) => {
    //将vnode -> 真实DOM
    // 1.创建出真实的原生,并且在vnode上保留el
    const el = vnode.el = document.createElement(vnode.tag)
​
    // 2.处理props
    if (vnode.props) { 
        for (const key in vnode.props) {
            const value = vnode.props[key]
            
            if (key.startsWith("on")) { //对事件进行监听
                el.addEventListener(key.slice(2).toLowerCase(), value)
            } else {
                el.setAttribute(key, value)
            }
        }
    }
​
    // 3.处理childern
    if (vnode.children) {
        if (typeof vnode.children === "string") { //如果是文本
            el.textContent = vnode.children
        } else { //其他情况视为数组
            vnode.children.forEach(item => {
                mount(item, el)
            })
        }
    }
​
    // 4.将el挂载到container上
    container.appendChild(el)
}
​

在index.html中使用

xml 复制代码
<html>
<head>
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./renderer.js"></script>
    <script>
        // 1.通过h函数来创建一个vnode
        const vnode = h('div', { class: 'ming' }, [
            h("h2", null, "当前计数:100"),
            h("button", null, "+1")
        ])
        
        // 2. 通过mount函数,将vnode挂载到div#app上
        mount(vnode, document.querySelector("#app"))
    </script>
</body>
</html>

这样一来,页面上就可以显示出vnode的内容了

2.2 patch函数

假设用户更新了修改了vnode,这个时候我们需要进行differ算法

kotlin 复制代码
const vnode = h('div', { class: 'ming' }, [
    h("h2", null, "当前计数:100"),
    h("button", null, "+1")
])
​
const vnode1 = h('div', null, '诶嘿')

例如:上边的代码中,如何通过differ算法,找到两者的不同之处,把新的修改内容替换掉原先vnode的内容,这一过程中尽可能复用旧vnode

1、判断两者的类型是否一致

scss 复制代码
//renderer.js
const patch = (n1, n2) => {
    if (n1.tag !== n2.tag) { //类型不同
        //拿到n1的父节点(这里是<div id="app">)
        const n1ElParent = n1.el.parentElement;
        //删除n1
        n1ElParent.removeChild(n1.el)
        //将n2挂载上去
        mount(n2, n1ElParent)
    } else {
        //...
    }
}

如果两个类型不一致,则简单粗暴地将整个的n2替换掉n1(将n1的DOM树直接移除),而不是去修改n1

注意:这里的n1中的el先暂时忽略其来源,最终实现的时候我们会将该vnode的真实DOM存一份到其中

2、两者类型相同,处理属性

对比新旧两个虚拟DOM的属性,并将新的n2中的属性添加进el中

javascript 复制代码
const patch = (n1, n2) => {
    if (n1.tag !== n2.tag) { 
        //...
    } else { //类型是相同的
        // 1.取出element对象,并且在n2中进行保存
        const el = n2.el = n1.el
​
        // 2.处理props
        const oldProps = n1.props || {}
        const newProps = n2.props || {}
        // 2.1 获取所有的newProps,并添加进el中
        for (const key in newProps) {
            const newValue = newProps[key]
            const oldValue = oldProps[key] //如果oldValue有值,说明新旧n1、n2存在相同的属性
            if (newValue !== oldValue) {
                //将不相同的属性进行添加操作
                if (key.startsWith("on")) {
                    el.addEventListener(key.slice(2).toLowerCase(), newValue)
                } else {
                    el.setAttribute(key, newValue)
                }
            }
        }
​
        // 3.处理children
    }
}

3、处理属性

剔除掉el的属性中,旧的虚拟DOM的属性

javascript 复制代码
const patch = (n1, n2) => {
    if (n1.tag !== n2.tag) {
        //...
    } else { //类型是相同的
        // 1.取出element对象,并且在n2中进行保存
        const el = n2.el = n1.el
​
        // 2.处理props
        const oldProps = n1.props || {}
        const newProps = n2.props || {}
        // 2.1 获取所有的newProps,并添加进el中
        //...
        
        // 2.2 删除旧的props
            for (const key in oldProps) {
              if (key.startsWith("on")) { // 对事件监听的判断
                const value = oldProps[key];
                el.removeEventListener(key.slice(2).toLowerCase(), value)
              } 
              if (!(key in newProps)) {
                el.removeAttribute(key);
              }
            }
        // 3.处理children
    }
}

4、两者类型相同,处理children

如果新的节点是文本,直接替换掉旧节点的整个children即可

ini 复制代码
        // 3.处理children
        const newChildren = n2.children || []
        const oldChildren = n1.children || []
​
        if (typeof newChildren === 'string') {
            // 如果新节点的children是文本,替换掉旧el的整个innerHTML
            el.innerHTML = newChildren 
        }

5、处理children

新节点是本身是一个数组的情况:假设旧节点是一个文本,那么我们应该先清空旧节点的文本,并将新节点的children挂载到对应的位置上

ini 复制代码
        // 3.处理children
        const newChildren = n2.children || []
        const oldChildren = n1.children || []
        // 3.1 新节点是一个文本
        if (typeof newChildren === 'string') {
            // 替换掉旧el的整个innerHTML
            el.innerHTML = newChildren
        } else {//3.2 新节点是一个数组
            //旧节点是一个文本
            if (typeof oldChildren === 'string') {
                el.innerHTML ="" //清空
                newChildren.forEach(item => {
                    mount(item, el) // 递归 :将每一个item挂载到el上
                })
            }

6、处理children

新节点是一个数组,当旧节点也是一个数组时,就需要分为多种情况进行考虑

取出新旧节点的children的长度最小值,以最小值的长度为遍历依据,使新旧节点两两进行对比(patch,回调)

(考虑到新旧节点都是可能有多层的树结构,子节点,子节点的子节点....)

如果新节点的children有多余的元素,或者旧节点的children有多余的元素,就要进行添加/删除的操作

ini 复制代码
        // 3.处理children
        const newChildren = n2.children || []
        const oldChildren = n1.children || []
        // 3.1 新节点是一个文本
        if (typeof newChildren === 'string') {
            // 替换掉旧el的整个innerHTML
            el.innerHTML = newChildren
        } else {//3.2 新节点是一个数组
            //3.2.1 旧节点是一个文本
            if (typeof oldChildren === 'string') {
                el.innerHTML ="" //清空
                newChildren.forEach(item => {
                    mount(item, el) // 递归 :将每一个item挂载到el上
                })
            } else {
                //3.2.2旧节点是一个数组
                //oldChildren: [v1, v2, v3]
                //newChildren: [v1, v5, v6]
                const commonLength = Math.min(oldChildren.length, newChildren.length)
                for (let i = 0; i < commonLength; i++) {
                    // 有相同节点的元素进行patch操作
                    patch(oldChildren[i], newChildren[i])
                }
            }
        }

7、处理children

当新节点的children长度大于旧节点的children,就需要对超出公共长度的节点进行添加操作

scss 复制代码
    else {
                //3.2.2旧节点是一个数组
                //oldChildren: [v1, v2, v3]
                //newChildren: [v1, v5, v6]
                const commonLength = Math.min(oldChildren.length, newChildren.length)
                for (let i = 0; i < commonLength; i++) {
                    // 有相同节点的元素进行patch操作
                    patch(oldChildren[i], newChildren[i])
                }
​
                //newChildren.length > oldChildren.length
                //oldChildren: [v1, v2, v3]
                //newChildren: [v1, v5, v6, v7, v8]
                if (newChildren.length > oldChildren.length) {
                    //newChildren: [v7, v8] 添加至el上
                    newChildren.slice(oldChildren.length).forEach(item => {
                        mount(item, el)
                    })
                }
            }

8、处理children

当新节点的children的长度小于旧节点的children的长度,我们需要将旧节点大于公共长度的的元素进行移除

scss 复制代码
    else {
                //3.2.2旧节点是一个数组
                //情况一
                //oldChildren: [v1, v2, v3]
                //newChildren: [v1, v5, v6]
                const commonLength = Math.min(oldChildren.length, newChildren.length)
                for (let i = 0; i < commonLength; i++) {
                    // 有相同节点的元素进行patch操作
                    patch(oldChildren[i], newChildren[i])
                }
​
                ////情况二:newChildren.length > oldChildren.length
                //oldChildren: [v1, v2, v3]
                //newChildren: [v1, v5, v6, v7, v8]
                if (newChildren.length > oldChildren.length) {
                    //newChildren: [v7, v8] 添加至el上
                    newChildren.slice(oldChildren.length).forEach(item => {
                        mount(item, el)
                    })
                }
​
                // 情况三:newChildren.length < oldChildren.length
                //oldChildren: [v1, v2, v3, v7, v8]
                //newChildren: [v1, v5, v6]
                if (newChildren.length < oldChildren.length) {
                    //oldChildren: [v7, v8]进行移除
                    oldChildren.slice(newChildren.length).forEach(item => {
                        el.removeChild(item.el)
                    })
                }
            }

patch算是写好了,但是暂时调不通。创建并传入的vnode,并没有n1这个属性(mount时注入)

三、更新

数据动态更新这一部分就涉及到vue的一个重要概念:响应式原理。这里将重点介绍vue2和vue3响应式实现的过程

响应式的思想

什么是响应式:1、数据发生了变化,依赖于该数据的函数再次发生调用

xml 复制代码
<script>
    const info = { counter: 100 }
​
    function dobuleCounter() {
        console.log(info.counter * 2) //依赖
    }
    dobuleCounter()
​
    //当页面某处执行了
    info.counter++
    //应该自动在这里再执行一次dobuleCounter()
</script>

dobuleCounter函数依赖于info.counter,当info.counter发生改变时,dobuleCounter函数应该使用新的数据执行一次

说到底,当某一个数据发生变化时,页面中的另外一个数据对这个数据有依赖,也应该进行相应的变化

3.1 依赖收集系统

创建一个收集依赖的类

Dep: depend(依赖) subscribers:订阅者

kotlin 复制代码
class Dep {
    constructor () {
        this.subscribers = [] //用于收集依赖
    }
}

当然,this.subscribers = []使用集合Set会更好,因为集合规定了元素不能出现重复。重复了只保留一个

javascript 复制代码
class Dep {
    constructor () {
        this.subscribers = new Set() //用于收集依赖
    }
    
    //收集依赖,修改了数据后会产生的影响
    addEffect(effect) {
        //数据变化后,将这个被影响的目标添加进subscribers中
        this.subscribers.add(effect)
    }
    
    //执行
    notify() {
        this.subscribers.forEach(effect => {
            //调用被影响的目标
            effect()
        })
    }
}
​
// 创建实例
const dep = new Dep()
​
const info = {
    counter: 100
}
​
//下面的两个函数依赖于info.counter
function dobuleCounter() {
    console.log(info.counter * 2)
}
function powerCounter() {
    console.log(info.counter * info.counter)
}
​
//收集依赖
dep.addEffect(dobuleCounter)
dep.addEffect(powerCounter)
​
​
//依赖发生改变
info.counter++  
dep.notify()

在index.html在引入上边的代码,可以看到代码确实info.counter改变后,对应的dobuleCounter、powerCounter也重新调用并更新了数据

问题:

上边的代码存在很大的问题,比如需要手动收集依赖、需要手动执行执行依赖的函数。

我们希望当一个对象对目标有依赖时,能够自动被收集,自动在目标改变时,自动执行

对上边的代码进行重新构建

javascript 复制代码
class Dep {
    constructor () {
        this.subscribers = new Set() //用于收集依赖
    }
    depend() {
        if (activeEffect) {
            this.subscribers.add(activeEffect)
        }
    }
    notify() {
        this.subscribers.forEach(effect => {
            //调用被影响的目标
            effect()
        })
    }
}
​
let activeEffect = null
function watchEffect(effect) {
    activeEffect = effect
    dep.depend()
    effect() //执行时,因为读取数据而被收集了依赖
    activeEffect = null
}
​
// 创建实例
const dep = new Dep()
const info = {
    counter: 100
}
​
//下面的两个函数依赖于info.counter
watchEffect(function() {
    console.log(info.counter * 2)
})
watchEffect(function() {
    console.log(info.counter * info.counter)
})
​
//数据发生改变
info.counter++  
dep.notify()

先理解上边的代码,不然下面的代码可能理解不了

上边的代码依旧有些缺陷

假设存在effect1、effect2、effect3,其中effect1、effect3依赖于info中的name属性,当只有name属性发生变化时,再次调用dep.nitify()显然是不合适的(因为,effect2并没有受影响)

javascript 复制代码
//实例
const dep = new Dep()
​
//数据
const info = {
    name: 'linming',
    counter: 10
}
const foo = {
    height: 1.88
}
​
//effect1
watchEffect(function () {
    console.log(info.counter * 2, info.name)
} )
//effect2
watchEffect(function () {
    console.log(info.counter * info.counter)
})
//effect3
watchEffect(function () {
    console.log(info.counter + 10, info.name)
})
​
info.name = "linlin"
dep.notify() //不适合

所以,我们不能随随便便地进行依赖收集。

也不能将所有的依赖都收集在一个dep中,而是应该不同的数据,应该创建不同的dep去收集。


> 思路:每一个属性,都应当有专门一个subscribers来收集其依赖

scss 复制代码
dep1(info.counter)=》subscribers //关于info.counter的订阅者
dep2(info.name)=》subscribers    //关于info.name的订阅者
dep3(foo.height)=》subscribers   //关于foo.height的订阅者

需要一种专门的数据结构来管理这些dep实例------MAP(可以将对象作为键),当然使用Weakmap会更好。

简单说明map和weakmap的区别

1、map与weakmap是两种不同的数据结构,都能以引用数据类型作为键,不同的是weakmap只能以引用类型作为键

2、weakmap的键是弱引用,而map是强引用。这也是vue响应式采用weakmap的原因

弱引用和强引用。

ini 复制代码
//用一个例子来说明
const obj = { xxx: 'xxx' }
const map = { key: obj },
const wmap = { key: obj }
obj = null

将obj置为null时,按理来说map引用着它,那么它不会被垃圾回收掉。但是如果是wmap,当obj为null时,weakmap会认为这是无效的引用,直接就将key所指向的地址垃圾回收掉。有效地防止了内存泄漏

3.2 vue2数据劫持

实现效果

封装一个reactive函数,希望实现以下效果

php 复制代码
//传入一个对象,将其变为响应式
const info = reactive({ name: 'linming', counter: 10 })
const foo = reactive({ height: 1.88 })

最重要的是,我们需要实现当info、foo对象里边的值发生变化时,能够进行数据劫持

在vue2中,数据响应原理使用了Object.defineProperty,而vue3使用了proxy

1、实现数据劫持

这里我们先写vue2的数据劫持方式

javascript 复制代码
// vue2的数据劫持
function reactive(raw) {
    Object.keys(raw).forEach(key => {
        Object.defineProperty(raw, key, {
            get() {  },
            set(newValue) {  }
        })
    })
    return raw
}
​
const info = reactive({ name: 'linming', counter: 10 })
const foo = reactive({ height: 1.88 })

这样一来,我们就实现了对info对象、foo对象的数据劫持

我们可以在get里边实现依赖的收集

javascript 复制代码
function reactive(raw) {
    const dep = new Dep()
    Object.keys(raw).forEach(key => {
        Object.defineProperty(raw, key, {
            get() {
                dep.depend()
            },
            set(newValue) {  }
        })
    })
    return raw
}

这样一来,一旦数据发生改变,例如counter,与counter相关的函数目标就会被添加依赖中

封装getDep函数

上边的代码中,每一次添加依赖,都是新创建一个dep实例,显然是不合适的

所以,我们需要封装一个这个工具函数,基本机构如下

arduino 复制代码
// 结构图:
//1.最外层的targetMap(WeakMap类型,键为对象)
//2.targetMap的属性为:target(键,对象):depsMap(值,对象)
//3.depsMap对象的属性有:key(键,字符串):dep(值,对象)
scss 复制代码
const targetMap = new WeakMap()
function getDep(target, key) {
    // 1.根据对象(target)取出对应的map对象
    let depsMap = targetMap.get(target)
    //如果没有则创建
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
​
    // 2. 取出具体的dep对象
    let dep = depsMap.get(key)
    //如果没有则创建
    if(!dep) {
        dep = new Dep()
        depsMap.set(key, dep)
    }
    return dep
}

调用getDep函数,最终实现效果如下

javascript 复制代码
let activeEffect = null
function watchEffect(effect) {
    activeEffect = effect
    effect()
    activeEffect = null
}
​
//创建getDate工具函数
const targetMap = new WeakMap()
function getDep(target, key) {
    // 1.根据对象(target)取出对应的map对象
    let depsMap = targetMap.get(target)
    //如果没有则创建
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
​
    // 2. 取出具体的dep对象
    let dep = depsMap.get(key)
    //如果没有则创建
    if(!dep) {
        dep = new Dep()
        depsMap.set(key, dep)
    }
    return dep
}
​
// vue2的数据劫持
function reactive(raw) {
    Object.keys(raw).forEach(key => {
        //创建dep实例
        const dep = getDep(raw, key)
        let value = raw[key]
​
        Object.defineProperty(raw, key, {
            get() {
                // 获取值时
                dep.depend()
                return value
            },
            set(newValue) {
                // 设置值时
                value = newValue
                dep.notify() //响应
            }
        })
    })
    return raw
}
​
​
// 创建实例
const dep = new Dep()
​
//测试代码
const info = reactive({ name: 'linming', counter: 10 })
const foo = reactive({ height: 1.88 })
​
//effect1
watchEffect(function () {
    console.log("effect1:", info.counter * 2, info.name)
} )
//effect2
watchEffect(function () {
    console.log("effect2:", info.counter * info.counter)
})
//effect3
watchEffect(function () {
    console.log("effect3:", info.counter + 10, info.name)
})
//effect4
watchEffect(function () {
    console.log("effect4:", foo.height);
})
​
//数据发生改变
info.counter++  

info.counter++发生变化时,结果如下

arduino 复制代码
//effect1: 20 linming
//effect2: 100
//effect3: 20 linming
//effect4: 1.88
//effect1: 22 linming
//effect2: 121
//effect3: 21 linming

前四个是收集依赖,必然会执行。后边的effect1、2、3因为依赖了info.counter,所以也执行了,而effect4并没有依赖它,就没有执行

3.3 vue3数据劫持

为什么vue3选择Proxy?两者区别

1、主要的原因:Object.defineProperty是劫持对象的属性,如果新增元素,那么vue2需要再次调用definedProperty。而Proxy劫持的是整个对象,不需要做特殊处理

2、修改对象的不同:使用Object.defineProperty时,修改原来的obj对象就可以触发拦截;而使用proxy,就必须修改代理对象,即Proxy的实例才可以触发拦截

3、Proxy能观察的类型比defineProperty更丰富

具体代码如下

scss 复制代码
​
// vue3的数据劫持
function reactive(raw) {
    return new Proxy(raw, {
        get(target, key) {
            const dep = getDep(target, key)
            dep.depend()
            return target[key]
        },  
        set(target, key, newValue) {
            const dep = getDep(target, key)
            target[key] = newValue
            dep.notify()
        }
    })
} 
 

四、实现

4.1 mini-vue实现

1、创建一个index.html文件,导入renderer函数和reactive函数

并创建一个根组件

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./引入上边所实现的的代码"></script>
    <script>
        // 1.创建根组件
        const app = {
            data: reactive({
                counter : 100
            }),
            render() { //返回一个vnode
                return h("div", null, [
                    h("h2", null, `当前计数:${ this.data.counter }`),
                    h("button", {
                        onclick: () =>{
                            this.data.counter++
                        }
                    }, "+1")
                ])
            }
        }
​
        //挂载根组件
        createApp(App).mount("#app")
        //或者这样写
        const app = createApp(App)
        app.mount("#app")
        
    </script>
</body>
</html>

2、创建createApp函数

第一次进入该函数是挂载根组件,第二次是更新

javascript 复制代码
function createApp(rootComponent) {
    return {
        mount(selector) {
            const container = document.querySelector(selector)
            let isMounted = false  //是否完成挂载
            let oldVnode = null
​
            //注:传入的函数我们将其称为effect函数,方便理解
            watchEffect(function() { //挂载时,将进行依赖的收集
                if (!isMounted) {
                    //尚未挂载
                    oldVnode = rootComponent.render()
                    mount(oldVnode, container)
                    isMounted = true  //将其转态改为已挂载
                } else {
                    //已实现挂载
                    const newVnode = rootComponent.render()
                    //更新数据
                    patch(oldVnode, newVnode)
                    oldVnode = newVnode //为了可再次更新
                }
            })
        }
    }
}

运行流程

1、调用createApp(App),返回vue实例,该实例包含了mount方法

2、调用app.mount("#app"),会执行watchEffect,并将传入的函数执行一遍。第一次调用必定是未挂载状态,则执行render()函数形成虚拟Dom

注意:在执行render过程中,访问了data.counter(因为我们counter已事先被reactive劫持),所以此处将形成counter ===> [effect]的订阅关系

3、虚拟Dom形成后将调用mount(oldVnode, container)挂载到页面,至此页面初次渲染完毕

更新流程

1、当counter发生更新,触发get操作,便会更新counter引用了counter的依赖,即此处会调用effect函数

2、第二次调用effect函数组件已挂载,patch(oldVnode, newVnode)对比新旧数据,并最终将变化更新到页面上

建议通过debugger一步步看整体的代码运行过程,可以比较清晰的知道各个方法变量之间的联系

本文是从自己的学习笔记中整理出来,有很多内容不够严谨,但是大体的思路是对的。如果有不对的地方,请谅解。

相关推荐
小镇程序员2 小时前
vue2 src自定义事件
前端·javascript·vue.js
AlgorithmAce5 小时前
Live2D嵌入前端页面
前端
nameofworld5 小时前
前端面试笔试(六)
前端·javascript·面试·学习方法·递归回溯
前端fighter5 小时前
js基本数据新增的Symbol到底是啥呢?
前端·javascript·面试
GISer_Jing5 小时前
从0开始分享一个React项目:React-ant-admin
前端·react.js·前端框架
川石教育5 小时前
Vue前端开发子组件向父组件传参
前端·vue.js·前端开发·vue前端开发·vue组件传参
GISer_Jing6 小时前
Vue前端进阶面试题目(二)
前端·vue.js·面试
乐闻x6 小时前
Pinia 实战教程:构建高效的 Vue 3 状态管理系统
前端·javascript·vue.js
weixin_431449687 小时前
web组态软件
前端·物联网·低代码·编辑器·组态
橘子味小白菜7 小时前
el-table的树形结构后端返回的id没有唯一键怎么办
前端·vue.js