前言
vue3x版本即将迎来大面积使用,在此我们一起对vue3x版本的数据响应以及dom更新进行深入理解
vue核心类
- 实现渲染器扩展
- 模板初始化,数据更新
创建Vue
- createApp 用户使用创建一个app实例
- createRenderer 渲染器,里面的createApp 是实际的创建函数
- createRenderer -> mount 挂载节点,保存用户传入的 data setup 等数据源
- createRenderer -> proxy 这里的数据拦截只是为了进行 data 和 setup 的数据优先级问题,不是响应式触发的地点
- effect 副作用函数中是真正数据响应时被触发的回调。
- compile 在此只是粗暴的进行了dom的创建赋值,理解核心概念思路就行,vue3中使用的是 ast(抽象语法树) ,由template => ast => js(render => 得到虚拟dom)
const Vue = {
createApp(options) {
// 获取渲染器 平台扩展性
const renderer = Vue.createRenderer({
querySelector(selector) { // 真正的平台特有操作,由平台传入, 目前实现的是web平台
return document.querySelector(selector);
},
insert(child, parent, anchor) { // anchor 参考节点,如果未传入,insertBefore 等同于 appendChild
parent.insertBefore(child, anchor || null)
}
})
return renderer.createApp(options);
},
createRenderer({ querySelector, insert }) { // 扩展成高阶函数,平台特有操作,可以直接编写对应的渲染器直接进行多平台扩展使用
// 获得渲染器
return {
createApp(options) {
return {
mount(selector) {
// 宿主元素
const parent = querySelector(selector); // 扩展的平台宿主获取函数
this.setupState = {};
this.data = {};
console.log('selector', parent);
// 获取渲染函数,编译结果
if (!options.render) { // 如果选项中没有render,那么就获取
options.render = this.compile(parent.innerHTML);
}
if (options.setup) { // 保存setup中的值
this.setupState = options.setup();
}
if (options.data) { // 保存data中的值
this.data = options.data();
}
// 监听数据 vue3中使用 proxy
this.proxy = new Proxy(this, {
get(target, key) { // 代理目标和访问的key
console.log('target', target);
if (key in target.setupState) { // setup优先级高,如果访问的key在其中,直接返回
return target.setupState[key];
} else {
return target.data[key]
}
},
set(target, key, val) {
if (key in target.setupState) { // setup优先级高,如果访问的key在其中,直接更新
target.setupState[key] = val
} else {
target.data[key] = val
}
}
})
// 更新函数设置为副作用回调函数即可,options.render中获取了响应式数据,那么effect中会自动收集依赖,当数据变化时,会执行这个回调,重新渲染dom
this.update = effect(() => {
// 渲染dom,并追加宿主元素
const el = options.render.call(this.proxy); // 绑定上下文 能够获取实例中data或者setup中的值
parent.innerHTML = ""; // vue3中是直接清空
insert(el, parent); // 平台对应的插入函数
})
},
compile(template) { // 返回render
return function render() {
// 描述视图
const h1 = document.createElement("h1");
h1.textContent = this.title;
return h1;
}
}
}
}
}
}
}
复制代码
reactive 数据响应
reactive实现了数据响应,依赖收集,是vue3中数据响应的核心机制,比vue2中的更容易理解,且更强大。 使用 Proxy 进行数据监听,无需进行数组拦截额外处理,
const isObject = v => typeof v === "object" && v !== null;
// 响应式监听数据
const reactive = function(obj) {
if (!isObject(obj)) return obj; // 如果不是对象,不需要代理
return new Proxy(obj, {
get(target, key) {
console.log("get: ", key);
const res = Reflect.get(target, key); // 处理异常,可以被捕获并且一定会返回一个值
track(target, key); // 依赖收集触发点 (订阅)
return isObject(res) ? reactive(res) : res; // 懒处理,如果我访问的数据是一个对象,那么将这个对象也设置成响应式的,而不是一开始全部递归,节省了初始化时间,将深层对象的响应移动到了运行时
},
set(target, key, val) {
console.log("set: ", key);
const res = Reflect.set(target, key, val);
trigger(target, key); // 取出依赖收集时放入的更新函数,进行调用更新dom (发布)
return res;
},
deleteProperty(target, key) {
console.log("delete: ", key);
const res = Reflect.deleteProperty(target, key);
trigger(target, key);
return res;
}
})
}
复制代码
effect 副作用函数
接收一个使用了响应式数据的回调,并调用(初始化),当对应回调中访问了响应式数据时,就会进行依赖收集过程(在reactive get 中)
// 临时保存响应式函数,也就是传入的fn
const effectStack = [];
// 副作用函数,如果副作用函数中用了响应式数据,那么尝试建立它们之间的关系
const effect = function(fn) {
// fn可能有异常 进行封装捕获 防止程序卡死
const eff = () => {
try {
effectStack.push(fn); // 传入响应式副作用函数。使用数组是因为假如 effect 有嵌套的情况,那么js执行栈就会往深层执行,此时就会出现多个fn需要存储
fn(); // 执行触发依赖收集过程
} finally {
effectStack.pop(); // 收集完成后出栈
}
}
eff(); // 初始化
return eff;
}
复制代码
track 依赖收集
// 存储依赖关系的map,使用weakmap,好处,可以使用对象当做key,且是弱引用,如内部对象消失或删除,不用担心内存泄露垃圾释放不了
const targetMap = new WeakMap();
// 依赖收集
const track = function(target, key) {
// 当effect接受的回调中,有响应式数据,那么执行该函数时,会立即触发proxy中的get,get中会立即调用track,这就是建立联系的关键时刻
const eff = effectStack[effectStack.length - 1]; // 拿到最新的副作用函数
if (eff) {
// 获取响应式数据对象 对应的 map (target是一个对象)
let depMap = targetMap.get(target);
if (!depMap) { // 不存在则创建
depMap = new Map();
targetMap.set(target, depMap); // 存入
}
// 获取key 对应的 set(key是target对象中的某个key)
let deps = depMap.get(key);
if (!deps) { // 不存在则创建
deps = new Set(); // set结构可以去重,相同的回调函数只需添加一次
depMap.set(key, deps);
}
// 建立 target key 与 eff 之间的关系,建立之后就可以在需要的地方取出来使用
deps.add(eff);
}
}
复制代码
trigger 依赖触发 调用对应更新函数
// 依赖触发
const trigger = function(target, key) {
// 获取target对应的map
const depMap = targetMap.get(target);
if (depMap) {
// 通过key获取对应的set
const deps = depMap.get(key);
if (deps) { // 如果有,那么遍历执行其中所有的副作用函数(有响应式数据的更新函数)
console.log(`依赖触发 target: `,target)
console.log(`依赖触发 key:`, key)
console.log(`依赖触发 deps: `, deps)
deps.forEach(dep => dep());
}
}
}
复制代码
html示例
将以上代码导入,使用createApp创建app实例,定义响应式数据,并挂载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
</div>
</body>
<script src="./vue3x.js"></script>
<script>
const { createApp } = Vue;
const app = createApp({
data () {
return {
title: "hello vue3!"
}
},
setup() {
const state = reactive({
title: "hello vue3 setup"
})
setTimeout(() => {
state.title = " hello vue3 setup change"
}, 2000)
return state;
}
})
app.mount("#app");
</script>
</html>
复制代码
结语
vue3x对比vue2x,进行了大量的优化更新,如:响应式机制变更(初始化更快,对任何类型都可监听,如Array,Set, Map),函数式(去掉大量的上下文this.xx调用,更好的支持TS,可针对性打包,摇树优化),复用性(compositions api,可以提出大量通用逻辑,更好的复用,对比mixins中不知到底是那个mixin的数据,可以更明确的知道来源,不会命名冲突)等等。 还有更多的好处请前往官方文档仔细阅读体验,并推荐看 vue conf 2021 视频。