如何在vue3中进行渲染优化

297 阅读2分钟

下面是在我们平常在vue中发起请求时常常编写的代码

const data = ref({
    name: "liaoliao666",
    subscribers_count: 1,
    forks_count: 0
});

const getData = () => {
     fetch('https://api.github.com/repos/liaoliao666/vu-query').then(async res =>
        {
            // {
            //     name: "vu-query",
            //     subscribers_count: 1,
            //     forks_count: 0
            // }
            const newData = await res.json();
            data.value = newData
        }
      )
}

watchEffect(() => {
    console.log("name 变化了", data.value?.name)
})

watchEffect(() => {
    console.log("subscribers_count 变化了", data.value?.subscribers_count)
})

watchEffect(() => {
    console.log("forks_count 变化了", data.value?.forks_count)
})

而当我们每次调用getData这个方法时,你会发现所有依赖于data的属性的所有组件和副作用都会重新渲染和执行,尽管subscribers_countforks_count的原来一致

那么我们如何才能让新旧data的数据结构共享呢?

其实很简单,只要我们对两个数据的属性进行对比,如果全等的话就跳过,不相等的情况下才赋值就可以了。

以下是代码的实现

// `replaceEqualDeep` 会深层地保留任何a中与b相同的属性
function replaceEqualDeep(a, b) {
  if (a === b) return a

  const array = Array.isArray(a) && Array.isArray(b)
  const isSameObject = array || (isPlainObject(a) && isPlainObject(b))

  // 当a和b都为数组或都为对象时,比较a和b的属性
  if (isSameObject) {
    if (array) {
      const bSize = b.length
      // 删除a中多余的长度
      if (a.length > bSize) {
        a.splice(bSize)
      }

      for (let i = 0; i < bSize; i++) {
        // 递归比较
        a[i] = replaceEqualDeep(a[i], b[i])
      }
    } else {
      const aKeys = Object.keys(a)
      const bKeys = Object.keys(b)
      const hash = new Set(bKeys)

      for (let i = 0, len = aKeys.length; i < len; i++) {
        const key = aKeys[i]
        // 删除a中多余的属性
        if (!hash.has(key)) {
          delete a[key]
        }
      }

      for (let i = 0, len = bKeys.length; i < len; i++) {
        const key = bKeys[i]
        // 递归比较
        a[key] = replaceEqualDeep(a[key], b[key])
      }
    }
  }

  // 为一样的对象时返回本身,否则返回新对象
  return isSameObject ? a : b
}


// Copied from: https://github.com/jonschlinkert/is-plain-object
// 判断是否为普通的对象
function isPlainObject(o) {
  if (!hasObjectPrototype(o)) {
    return false
  }

  // If has modified constructor
  const ctor = o.constructor
  if (typeof ctor === 'undefined') {
    return true
  }

  // If has modified prototype
  const prot = ctor.prototype
  if (!hasObjectPrototype(prot)) {
    return false
  }

  // If constructor does not have an Object-specific method
  if (!prot.hasOwnProperty('isPrototypeOf')) {
    return false
  }

  // Most likely a plain Object
  return true
}

function hasObjectPrototype(o) {
  return Object.prototype.toString.call(o) === '[object Object]'
}

下面是经过优化过的getData方法,你会发现这次只会打印 name 变化了 vu-query

    const getData = () => {
     fetch('https://api.github.com/repos/liaoliao666/vu-query').then(async res =>
        {
            // {
            //     name: "vu-query",
            //     subscribers_count: 1,
            //     forks_count: 2
            // }
            const newData = await res.json();
            data.value = replaceEqualDeep(data.value, newData)
        }
      )
}

注意: 如果你有以下两个需求请不要使用该方法

  • 如果由于数据过大而导致性能问题
  • 如果对对象的keys顺序有需求,两比较对象的keys不同,则可能会改变对象keys的顺序,数组则不会,例如
const a = {
    a: "a",
    c: "c",
}

const b = {
    a: "a",
    b: "b",
    c: "c"
}

console.log(replaceEqualDeep(a, b)) // {a: "a", c: "c", b: "b"}

在vu-query中已经默认内置了该功能,你如果想要禁用此功能(99.9%的场景您不需要禁用此功能),可以使用config.structuralSharing,例

const query = useQuery('repoData', () =>
    fetch('https://api.github.com/repos/liaoliao666/vu-query').then(res =>
        res.json()
    ), {
        structuralSharing: false
    }
)

vu-query 仓库与文档