前言
之所以写这篇是因为我之前写了一篇 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项目中也可以。
再次重申
本文看看就好,实际写代码时千万千万不要这么干。