最近,Vue3成为前端领域热门研究和讨论的框架,尽管它还没有发布正式版,但这没办法阻止万能的前端程序员研究其新的实现模式与实现原理。这里附送一个Vue3(即Vue-next传送门)vue-next。
我们可以看出,从Vue3开始,Vue将原本使用Flow进行类型校验全面该为采用TypeScript(TS)作为其开发语言,这将使Vue3更加的严谨并可避免很多因为js弱语言所带来的一些不可预料的问题。
Vue3除了将js该为TS之外,还有一个改动值得我们注意的是Vue3应用了ES6提供的元编程能力(即:Proxy与Reflect)实现数据响应化,以此来替代Vue2.x中采用Object.definePropety方式实现数据响应化(详细讲解可以参考本人撰写的 手写简易版Vue源码之数据响应化的实现),而我们本文要讨论和研究的便是Vue3是如何使用他们实现数据响应化的。
以下为本人参考Vue-next源码,个人实现的一个简易版Vue3响应化的示例代码,详细实现在代码注释中皆有说明,在这里就不再赘述了。
PS:以下源码实现了数据响应、computed-计算属性、副作用函数的实现
// 用户缓存数据
// 基于WeakMap键值可为对象类型且为弱引用的原理,此处直接使用WeakMap作为缓存数据的工具,可以实现当缓存的键值对象被销毁
// 或内存回收时自动释放,不需要人工释放
// 根据原始数据查询响应后的数据
let toProxy = new WeakMap()
// 根据响应后的数据查询原始数据
let toRaw = new WeakMap()
// 判断是否为非空对象
let isObject = obj => obj !== null && typeof obj === 'object'
// 用于处理getter和setter
const baseHandler = {
get(target, key) {
const res = Reflect.get(target, key)
// 收集依赖
track(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, value) {
// 先收集新旧值变化的信息
const info = { oldValue: target[key], newVal: value }
const res = Reflect.set(target, key, value)
// 触发更新
trigger(target, key, info)
return res
}
}
// 处理数据响应化
function reactive(target) {
console.log(target)
// 查询缓存,看看传入的对象是否已经响应过了
// 根据原始对象查询响应后的对象
let observed = toProxy.get(target)
// 若该数据已经在响应数据列表中,则无需重复响应化,直接返回已响应的数据
if (observed) {
return observed
}
// 根据响应后的对象查询原始对象
const observedRaw = toRaw.get(target)
// 若改数据在已响应原始数据列表中可以找到,则无需重复响应化,直接返回target
if (observedRaw) {
return target
}
// 未缓存过则创建新的Proxy对象进行代理
observed = new Proxy(target, baseHandler)
// 设置缓存
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
// 用于存储依赖
let targetMap = new WeakMap()
// 副作用栈
let effectStack = []
// 依赖收集
function track(target, key) {
let effect = effectStack[effectStack.length - 1]
if (effect) {
// 若存在依赖
if (targetMap.get(target)) {
let depsMap = targetMap.get(target)
// 若没有依赖列表,则初始化依赖列表
if (depsMap === undefined) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
// 若不存在依赖,则初始化一个set(具有去重功能)
if (dep === undefined) {
dep = new Set()
depsMap.set(key, dep)
}
// 如果依赖里面没有effect,则添加
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
}
} else {
// 初始化依赖列表
let depsMap = new Map()
targetMap.set(target, depsMap)
// 初始化依赖
dep = new Set()
depsMap.set(key, dep)
// 如果依赖里面没有effect,则添加
if (!dep.has(effect)) {
dep.add(effect)
// 在副作用对象的依赖列表中也将依赖加入进去
effect.deps.push(dep)
}
}
}
}
// 触发更新
function trigger(target, key, info) {
// 获取依赖列表
const depsMap = targetMap.get(target)
// 依赖列表不存在则直接返回,不触发副作用
if (!depsMap) return
// 初始化普通副作用列表、计算属性副作用列表
const effects = new Set()
const computedRunner = new Set()
if (key) {
// 获取依赖列表
let deps = depsMap.get(key)
// 将副作用函数分类为普通副作用函数和计算属性副作用函数
deps.forEach(effect => {
// 若副作用对象属性中包含computed属性,则代表改副作用为计算属性副作用,直接添加入计算属性副作用列表中,否则加入普通副作用列表中
if (effect.computed) {
computedRunner.add(effect)
} else {
effects.add(effect)
}
})
}
// 统一定义运行函数
const run = effect => effect()
// 分别运行普通副作用函数和计算属性副作用函数
effects.forEach(run)
computedRunner.forEach(run)
}
// 处理计算属性
// computed其实就是一个特殊的副作用函数
function computed(fn) {
// 创建一个标记为计算属性,首次加载不执行的副作用函数
const runner = effect(fn, { computed: true, lazy: true })
return {
effect: runner,
get value() {
// 获取值时直接返回副作用函数的返回值,如示例中,副作用函数返回值是:state.age * 2
return runner()
}
}
}
// 处理副作用
function effect(fn, options = {}) {
// 创建一个副作用函数
let e = createRelativeEffect(fn, options)
// 首次执行副作用函数
e()
return e
}
// 创建响应式副作用函数
// 这是一个高阶函数,接收一个函数和基础配置,并返回一个新的副作用函数,
// 该副作用函数将会返回目标值并在再副作用列表中加入当前创建的副作用对象
function createRelativeEffect(fn, options) {
// 定义一个函数用于执行副作用中传递过来的函数并接收其返回值
const effect = function(...args) {
return run(effect, fn, args)
}
// 定义依赖列表
effect.deps = []
// 当前创建的响应式副作用函数是否为计算属性副作用函数
effect.computed = options.computed
// 当前副作用函数是否为首次加载不执行函数
effect.lazy = options.lazy
return effect
}
// 执行副作用函数中传递过来的方法
function run(effect, fn, args) {
// 仅当副作用对象中不存在给定的副作用函数时执行添加并执行的操作,若已存在,无需重复添加
if (effectStack.indexOf(effect) === -1) {
try {
// 将副作用函数加入到副作用列表中
//// 将自身加入到订阅列表这种做法其实在Vue2.*上也出现过,比如在Dep中定义一个静态变量target用于存储其订阅的Wather,
//// 当Wather实例化时便会将自己放入这个target中,然后出发getter实现依赖收集
effectStack.push(effect)
// 执行副作用函数传递过来的函数并返回其返回值
return fn(args)
} finally {
// 无论最终是否成功,都要讲此副作用函数弹出堆栈,以免重复执行
effectStack.pop()
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<button id="btn">点我</button>
<script src="./kinerVue3.js"></script>
<script>
const root = document.querySelector('#app');
const btn = document.querySelector('#btn');
const state = reactive({
name: "kiner",
age: 25
});
const double = computed(() => state.age * 2);
effect(() => root.innerHTML = `<h1>我叫${state.name},今年${state.age}岁了,乘以2是${double.value}</h1>`);
btn.addEventListener('click', () => {
state.age++;
}, false);
</script>
</body>
</html>