vue3奇淫技巧--如何让视图同步更新

2,698 阅读3分钟

前言

之所以写这篇是因为我之前写了一篇 vue中data改变后,如何让视图同步更新,之前的是vue2的,现在vue3出了,所以写一篇vue3的。

事先声明,文章中的内容了解即可,千万千万不要用于生产环境

众所周知,在vue中,更改数据后,会在微任务中更新dom,这是一个异步操作。如图所示在 Run Micrortasks内。

image.png

那么,我们要做的事情就是,把这个微任务干掉,把更新dom这个行为放到当前这个宏任务中,变成同步操作。

如何做

首先我们整体了解一下vue的更新流程

在我们修改一个数据后,会触发setter,然后将对应的更新任务推导队列中,并调用queueFlush函数,这个函数会在一个Promise中调用flushjobs,这时候vue就会去做更新组件,更新dom的一些操作。

image.png

既然如此,如果不让vue在这个Promise中去更新组件。

我们要做的有两件事情

  1. 数据更改后,手动更新组件
  2. 数据更改后,不触发reactive中的setter。期望是数据改变后到dom更新前的这一段流程由我们自己控制。

数据更改后,手动更新组件

怎么手动去更新组件呢?我们研究一下vue3中的this,在vue组件中这个位置把this打印出来

// xxx.vue 
export default {
  setup() {
    return function () { 
      console.log(this) // 打印this,注意:不能使用箭头函数
      return <div></div>
    }
  }
}

就可以发现里面有这么多的属性,粗略一看,似乎并没有可以更新组件的api。

image.png

但是,眼尖的我们发现,存在一个$的属性,把它点开,就可以看到有一个update,这就是我们要找的东西了,调用它,就可以更新组件。手动更新组件的问题就解决了。

image.png

数据更改后,不触发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,即setupreturn出来的函数,每次都会被调用一次,那么里面的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 MicrortasksPromise产生的微任务不见了,diff等流程全部在当前的宏任务中执行完毕。大功告成!

image.png

所有的代码我放到了 CodeSandbox 上,点击链接即可体验。如果链接打不开,复制上面的代码到你的vue3项目中也可以。

再次重申

本文看看就好,实际写代码时千万千万不要这么干。