Vue3 beta已经发布一段时间了,八月份Vue3也要正式上线了,准备好了解Vue3的基本原理了么?vue
如下代码只介绍VUE的实现逻辑,不会覆盖全部的实际应用用例。react
首先,仍是从挂载一个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
// 挂载元素
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
假设咱们已经实现了这样一个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须要作什么?
比较两个节点是否是同一种节点,若是是的话继续比较属性和子节点。
若是不是的话,就须要进行节点的替换,节点的替换也是很复杂的处理,这里不讨论。
这里须要注意的是,会出现不少分支状况,新旧节点的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)
}
}
复制代码
一样的,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 传入一个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!'
复制代码
前面实现的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方法触发了依赖收集
如今咱们有了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