点进来!和尤大一块儿写vue3源码!

vue3原理 - mini-vue3

Vue3 beta已经发布一段时间了,八月份Vue3也要正式上线了,准备好了解Vue3的基本原理了么?vue

如下代码只介绍VUE的实现逻辑,不会覆盖全部的实际应用用例。react

挂载dom

首先,仍是从挂载一个dom元素开始git

vue3中咱们能够利用暴露出来的 h 函数来渲染一个模版,他接收的参数就是一个用js表示的dom结构 tag标签,属性,孩子节点 假设咱们有一个用js构建的vdom的模版,长这样github

const vdom = h('div', {
    class: 'red'
}, [
    h('span', null, 'hello')
])
复制代码

那么接下来咱们就该解析这个结构,用以把他挂载到真实的dom结构上算法

假设咱们已经实现了一个mount方法,那么咱们就只须要调用mount方法完成挂载数组

// 传入要挂载的虚拟dom,和父节点
mount(vdom, document.getElementById('app'))
复制代码

如此mount方法须要解决的就是app

  • 解析vdom
  • 解析props(假设这里只有attr,没有props,一些其余自定义属性)
  • 解析children子节点
// 挂载元素
function mount(vNode, container) {
    const elm = document.createElement(vNode.tag)

    // props
    if (vNode.props) {
        for (const key in vNode.props) {
            const attr = vNode.props[key]
            if (key.startsWith('on')) {
                // 事件监听
                const type = key.substr(2).toLocaleLowerCase()
                elm.addEventListener(type, attr)
            } else {
                elm.setAttribute(key, attr)
            }
        }
    }
    // children
    if (vNode.children) {
        if (typeof vNode.children === 'string') {
            elm.textContent = vNode.children
        } else {
            // 递归解析子元素
            vNode.children.forEach(child => {
                mount(child, elm)
            })
        }
    }
    container.appendChild(elm)
}
复制代码

OK,这样咱们就完成了一个很是简单的dom挂载过程。框架

更新dom

dom挂载了以后,咱们还可能触发一些操做来更新dom,好比点击按钮改变颜色,数字++这种操做,这个时候咱们不须要去操做真实的dom,只须要根据新的dom,对旧的dom打补丁dom

假设咱们已经实现了这样一个patch函数,他能够对旧元素打补丁。函数

const vdom2 = h('div', {
    class: 'red'
}, [
    h('span', null, 'hello')
])
patch(vdom, vdom2)
复制代码

这里须要对比新旧的vdom,因此咱们对mount改造一下,存储一下真实dom结构,方便后面操做。

const elm = vNode.elm = document.createElement(vNode.tag)
复制代码

这要就能够经过vdom.elm访问真实的dom结构

接下来考虑一下,patch须要作什么?

比较两个节点是否是同一种节点,若是是的话继续比较属性和子节点。

若是不是的话,就须要进行节点的替换,节点的替换也是很复杂的处理,这里不讨论。

  1. 比较新旧两个props

这里须要注意的是,会出现不少分支状况,新旧节点的props可能都不存在,都存在,或者新的存在,或者旧的存在。而后每个props可能出现变化,或者未变化。

一样的,这里只讨论attribute的状况,而且只讨论都有props的状况。在vue里面处理这个状况是很复杂的,这里只探讨 补丁 的思路

const oldProps = n1.props || {}
const newProps = n2.props || {}

// 若是这个属性存在 或者新增
for (const key in newProps) {
    const oldValue = oldProps[key]
    const newValue = newProps[key]
    // 新增了属性 或者 两个属性的值 不相等,须要变动节点内容了
    if (oldValue !== newValue) {
        elm.setAttribute(key, newValue)
    }
}
// 删除了属性
for (const key in oldProps) {
    if (!(key in newProps)) {
        // 删除了一个属性
        elm.removeAttribute(key)
    }
}
复制代码
  1. 比较新旧两个children

一样的,children的比较也会遇到相同的问题,会有不少分支状况须要去考虑。

并且最复杂的实际上是两个children都是数组的时候,vue里面会要求显示的设定key值,以减轻 diff 算法的压力,这个模式叫作 key模式 这里就假设没有key值,而且简单粗暴的比较两个children的每一个节点。

// 比较children
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
    if (typeof oldChildren === 'string') {
        // 两个节点都是字符节点,内不一样时,修改内容
    } else {
        // 新节是字符 旧节点是 数组 直接替换
    }
} else if (Array.isArray(newChildren)) {
    if (typeof oldChildren === 'string') {
        // 旧节点只是字符 新节点是数组 挂载新节点
    } else if (Array.isArray(oldChildren)) {
        // 若是两个节点都是数组 这里vue中用到key的模式去判断是否是同一个元素
        // 假设没有key 咱们只比较两个数组的 index 相同的部分
        const commonLength = Math.min(newChildren.length, oldChildren.length)
        for (let i = 0; i < commonLength; i++) {
            // 比较一下 公共部分的 每个child
        }
        // 接下来比较一下差别部分
        if (newChildren.length > oldChildren.length) {
            // 新的子节点多一些,挂载新的子节点
        }
        if (newChildren.length < oldChildren.length) {
            // 新的子节点少一些 删除了子节点
        }
    }
}
复制代码

响应式

假设咱们有一个这样的程序

let a = 10
let b = a * 10
复制代码

咱们但愿a被修改的时候,b也跟着被修改。

这里咱们就能够叫作b的修改 是 a的修改的 反作用 effect 想像一个EXCEL表格中,咱们定义了一个 公式(function) ,B列 = A列 * 10,当A的值改变时,B也会随之改变。

事实上,就至关于有个onAchange函数,在a改变时输出b = a * 10

onAchange(() => {
    b = a * 10
})
/** * () => { b = a * 10 } 这个函数就是a改变 所执行 的 反作用 / 复制代码

那么 如何实现这个 onAchange呢? 联想一下react的 setState

let _state, _update // 定义一个_state 保存state 定义一个_update保存 执行更改的反作用

const onStateChange = update => {
    _update = update // 保存反作用
}

const setState = newState => {
    _state = newState
    _update() // 触发反作用
}
复制代码

setState能够暴露给框架的使用者,显示的调用setState 告诉 框架 应该触发我这个操做的反作用了。

可是在vue中,咱们是 state.a = newValue 这样去更新一个值得,那么Vue是如何作的呢?

先来看一个简单的vue3提供的新的API使用示例

import {
    reactive watchEffect
} from 'vue'

// 调用reactive包装state的值 就会返回以一个状态响应式的值
// 包含了依赖收集
const state = reactive({
    count: 0
})
// 追踪这个函数使用过的全部的东西,他执行过程当中使用的每个响应式属性
// 当咱们修改state.count的时候这个函数会被再次执行
watchEffect(() => {
    console.log(state.count)
}) // 0
state.count++ // 1
复制代码

这两个API 是 Composition API 中的一部分,彻底独立的,能够与options API共存的新的API

来看看这两个API是怎么实现的

  1. 第一步 咱们先让 watchEffect 和依赖跟踪生效

想一想这里要作什么

1. 调用watchEffect 传入一个effect 以后,这个 effect 应该被做为一个反作用,被依赖被收集起来,等待调用
2. 这个effect 所依赖的参数发生改变时,effct 应该被再次执行
复制代码
let activeEffect
// 依赖关系
class Dep {
    constructor(value) {
        this.subscribers = new Set()
        this._value = value
    }
    get value() { // 利用getter自动执行依赖收集
        this.depend()
        return this._value
    }
    set value(newVlue) { // 利用setter自动执行反作用
        this._value = newVlue
        this.notify()
    }
    // 收集依赖
    depend() {
        if (activeEffect) this.subscribers.add(activeEffect)
    }
    // 触发依赖
    notify() {
        this.subscribers.forEach(effect => effect())
    }
}

function watchEffect(effect) {
    activeEffect = effect
    effect()
    activeEffect = null
}
const dep = new Dep('hello')
watchEffect(() => {
    console.log(dep.value)
})
dep.value = 'world!'
复制代码
  1. 第二步 实现 reactive 响应式的部分

前面实现的dep类,让dep去保存value,以便触发value变动的时候去触发notify,调用反作用函数。在真正的vue中,则是代理整个对象,让对象的每个属性,对应一个dep,value是对象的,而不是dep的

因此这里首先,咱们要实现这个reactive,那么reavtive究竟作了什么呢?

1. 代理整个对象,当咱们访问对象的属性时,对整个属性加上依赖追踪
2. 当属性的值改变时,触发依赖追踪,触发反作用
复制代码

那么,在vue2中,这个事情是由 Object.defineProperty 去完成的,他确实完成了代理对象的工做,表现也还不错。可是不可避免的他存在一些缺点:

1. 须要遍历对象的每个属性去为每个属性绑定,遇到对象嵌套的状况还须要递归
2. 没法处理这个对象身上自己没有的属性的变动
3. 代理数组时,须要hack到数组的原型上去改变原有的方法,这也是为何在vue2中直接用 `array[index]` 这样的方式修改数组,不会触发响应式的缘由
复制代码

在vue3中,这个功能的核心就是 proxy ,proxy的特性这就就不详细说了,感兴趣的能够自行查阅API,proxy也很好的解决了 Object.defineProperty 的痛点。

首先,咱们确定仍是须要 Dep 这个依赖类,那咱们在访问对象的属性时,经过proxy拦截一下这个动做,为这个属性绑定一个依赖追踪,把全部属性都绑定上依赖追踪,就须要有一个东西存储起来,这里选择 Map ,那还有就是每个对象都须要为每个属性绑定依赖追踪,因此要定位到 这个属性是这个对象的 ,就还须要在外层再来一个 Map ,告诉咱们哪一个对象对应哪个 属性依赖Map

结构就是这样的 对象 => 对象属性的map( 对象属性 => 属性对应的依赖 )

const targetMap = new WeakMap() // 收集全部 对象和 整个对象 的依赖映射
// 对象 => 对象属性的map( 对象属性 => 属性对应的依赖 )
function getDep(target, key) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        depsMap = new Map() // 对象的每个属性 与 对应的依赖 的映射
        targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (!dep) {
        dep = new Dep() // 
        depsMap.set(key, dep)
    }
    return dep
}

const reactiveHandler = {
    get(target, key, receiver) {
        const dep = getDep(target, key)
        dep.depend() // 依赖收集
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        const dep = getDep(target, key)
        const result = Reflect.set(target, key, value, receiver)
        dep.notify() // 触发反作用
        return result
    }
}

function reactive(obj) {
    // 代理对象
    return new Proxy(obj, reactiveHandler)
}
复制代码

为何 proxy 解决了 Object.defineProperty 的痛点呢? 你能够看到,这里没有循环,没有递归了。也不用去特殊处理数组了

好比 array.push ,它其实会先访问array.length,触发length + 1的操做,这里隐式的调用了get方法触发了依赖收集

mini-vue3

如今咱们有了h函数,有了挂载函数mount,dep依赖类, ractive响应式,watchEffect反作用监听

那么咱们如今就实现了一个简单的vue程序,把他们放到一块儿,写一个 $mount 函数,也就是挂载APP的函数

function mountApp(component, container) {
    let isMounted = false
    let oldDom
    // 当依赖改变时 会再次进入这个反作用函数
    watchEffect(() => {
        // 若是是mounted以前,那就先挂载app
        if (!isMounted) { 
            oldDom = component.render()
            mount(oldDom, container)
            isMounted = true
        } else {
            // 若是app已经挂载,就比较两个Vdom 打补丁
            const newDom = component.render()
            patch(oldDom, newDom)
            oldDom = newDom
        }
    })
}
const App = {
    data: reactive({
        count: 0
    }),
    render() {
        return h('span', {
            onClick: () => {
                this.data.count++
            }
        }, this.data.count + '')

    }
}
// ok你已经实现了一个mini-vue3程序
mountApp(App, document.getElementById('app'))
复制代码

当你点击屏幕的div时,你会神奇的发现,数字在累加!这就说明你已经实现了一个mini-vue3程序

事实上 composition API = reactivity API + Lifecycle API,并且在vue3中实现依赖收集能够直接使用ref

完整代码地址

相关文章
相关标签/搜索