前言
之所以写这篇是因为我之前写了一篇 vue中data改变后,如何让视图同步更新,之前的是vue2的,现在vue3出了,所以写一篇vue3的。
事先声明,文章中的内容了解即可,千万千万不要用于生产环境。
众所周知,在vue中,更改数据后,会在微任务中更新dom,这是一个异步操作。如图所示在 Run Micrortasks
内。
那么,我们要做的事情就是,把这个微任务干掉,把更新dom这个行为放到当前这个宏任务中,变成同步操作。
如何做
首先我们整体了解一下vue的更新流程
在我们修改一个数据后,会触发setter,然后将对应的更新任务推导队列中,并调用queueFlush
函数,这个函数会在一个Promise中调用flushjobs
,这时候vue就会去做更新组件,更新dom的一些操作。
既然如此,如果不让vue在这个Promise中去更新组件。
我们要做的有两件事情
- 数据更改后,手动更新组件
- 数据更改后,不触发
reactive
中的setter。期望是数据改变后到dom更新前的这一段流程由我们自己控制。
数据更改后,手动更新组件
怎么手动去更新组件呢?我们研究一下vue3中的this
,在vue组件中这个位置把this
打印出来
// xxx.vue
export default {
setup() {
return function () {
console.log(this) // 打印this,注意:不能使用箭头函数
return <div></div>
}
}
}
就可以发现里面有这么多的属性,粗略一看,似乎并没有可以更新组件的api。
但是,眼尖的我们发现,存在一个$
的属性,把它点开,就可以看到有一个update
,这就是我们要找的东西了,调用它,就可以更新组件。手动更新组件的问题就解决了。
数据更改后,不触发vue的setter
解决了组件更新的问题,就要解决setter
的问题。这种情况下,我们就不能再使用reactive
这个api,于是我需要自己对数据封装一层。
直接看代码,我实现了一个selfReactive
,原理很简单,对所有传入selfReactive
中的对象做代理,检测它的变更,如果发生了变更,则立刻更新视图。
/**
* vue组件中的写法
* 在这个组件中,我们每点击一次按钮,number.value都会+1
* selfReactive 跟 reactive写的位置不一样,reactive一般我们会写在第一个return之前
* 因为我们要手动去调用$.update这个方法,依赖了this,所以必须放在内层
*/
export default {
setup() {
return function () {
const number = selfReactive({ value: 0 }, this);
return <div>
<button onClick={() => { number.value ++ }}>btn</button>
<div>
{number.value}
</div>
</div>
}
}
}
但是,vue在每次更新的过程中,都会调一次render
,即setup
中return
出来的函数,每次都会被调用一次,那么里面的selfReactive
也会被调用,正常情况下,都会生成一个新的number对象。导致number.value
的值每次都是0,最终渲染到页面上也是0。
怎么去解决这个问题?用过hooks
的朋友可能就想到了,这跟hooks
里的useState
很像啊。
在下面selfReactive
实现中,我们用了一个全局的states
数组按顺序保存了所有传入selfReactive
中的对象。每次调用selfReactive
时,都会去通过index
检查states
中是否已经存在了该对象。
- 为什么用过
index
可以判断states
中是否已经存在了该对象?因为在render中,每次调用selfReactive
都是有顺序的,使用调用的次序作为索引,就可以判断出来,前提是不能在条件语句中使用selfReative
,就像useState
也不能在条件语句中使用一样。
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
function isArray(arr) {
return Array.isArray(arr)
}
const states = [];
let index = 0;
// 对所有的对象做代理,检测它的变更,如果发生了变更,立刻调用实例的$.update方法,更新视图
function setProxy (obj, instance) {
const handler = {
set: function(obj, prop, value) {
obj[prop] = value;
index = 0;
instance.$.update()
return true;
},
};
Object.keys(obj).forEach(key => {
if (isObject(obj[key]) || isArray(obj[key])) {
obj[key] = setProxy(obj[key], instance);
}
});
return new Proxy(obj, handler)
}
function selfReactive (obj, instance) {
if (!isObject(obj) && !isArray(obj)) {
throw new Error(`obj must be Object`)
}
// 如果states中已经存在了该对象,直接return这个对象
// 不存在,则需要做一层proxy代理
const curState = states[index] || setProxy(obj, instance)
states[index++] = curState
return curState;
}
做完以上这些,我们来看看现在的视图是不是能同步更新了。在下图中,我们发现,没有Run Micrortasks
,Promise
产生的微任务不见了,diff等流程全部在当前的宏任务中执行完毕。大功告成!
所有的代码我放到了 CodeSandbox 上,点击链接即可体验。如果链接打不开,复制上面的代码到你的vue3项目中也可以。
再次重申
本文看看就好,实际写代码时千万千万不要这么干。