Skip to content

Vue2、3响应式对比

  • Vue2: Object.defineProperty 重写getter setter不能监听数组长度的变化
  • Vue3: Proxy
  • Vue 2 使用的是 Object.defineProperty 来实现响应式系统,对于每个属性都需要重新定义 getter 和 setter,这在大量数据或深层次的嵌套对象中可能会引入额外的性能开销
  • Vue 3 引入了基于 Proxy 的响应式系统,它提供了更高效的依赖收集和触发更新的机制,减少了 Vue 2 中存在的重复依赖问题

this.$set

Vue.set(target, key, value)

  • target:要修改的数据源
  • key:要修改、要添加的属性名
  • value: 对应key的值

实现逻辑:

  1. 如果target时是undefined、null、基础类型,直接报错
  2. 对于数组,[1,2] => [3,2] // [1,2].splice(0,1,3)
js
// 数组的处理
if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(tanget.length, key)
    target.splice(key, 1, val)
    return val
}
  1. 对于对象,如果key存在直接更新走响应式逻辑;如果不是响应式对象就重新收集

两种响应式的实现

js
const initData = { value: 1 }
const data = {}
Object.keys(initData).forEach(key => {
    Object.defineProperty(data, key, {
        get() {
            console.log("访问了", key)
            return initData[key]
        },
        set(v) {
            console.log("修改了", key)
            initData[key] = v
        }
    })
})

const initData1 = { value: 1 }
const proxy = new Proxy(initData1, {
    get: function (target, key, receiver) {
        console.log("访问了", key)
        return Reflect.get(target, key, receiver)
    },
    set: function (target, key, value, receiver) {
        console.log("修改了", key)
        return Reflect.set(target, key, value, receiver)
    }
})

Vue3新特性

  1. main.js可以支持多个实例的声明
  2. composition api 对比mixin
    • mixin:
      • 命名冲突
      • 不清楚谁暴露出来的内容
    • composition api:
      • setup允许按照功能分布代码而不是按照类型
  3. 生命周期
  4. 异步组件
    • build以后是独立文件
  5. 自定义hook
  6. teleport
    • 将子组件渲染到不存在的父组件之外的dom上(dialog等实现原理)

setup

代替beforeCreate、created生命周期之前执行

Vue2/3 diff

Vue3.0 性能提升主要是通过哪几个方面体现的?

  • ① 响应式系统提升
    • vue2 在初始化的时候,对 data 中的每个属性使用 definepropery 调用getter 和setter 使之变为响应式对象。如果属性值为对象,还会递归调用 defineproperty 使之变为响应式对象。vue3 使用 proxy 对象重写响应式。proxy 的性能本来比 defineproperty 好,proxy 可以拦截属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下一级的属性。
    • 优势:
      • 可以监听动态新增的属性;
      • 可以监听删除的属性 ;
      • 可以监听数组的索引和 length 属性;
  • ② 编译优化
    • 优化编译和重写虚拟 dom,让首次渲染和更新 dom 性能有更大的提升vue2 通过标记静态根节点,优化 diff 算法 vue3 标记和提升所有静态根节点,diff 的时候只比较动态节点内容Fragments, 模板里面不用创建唯一根节点,可以直接放同级标签和文本内容
    • 静态提升
    • patch flag, 跳过静态节点,直接对比动态节点,缓存事件处理函数
  • ③ 源码体积的优化
    • vue3 移除了一些不常用的 api,例如:inline-template、filter 等 使用tree-shaking

vue2 Diff 缺点

vue2的diff是全量Diff,数据发生变化会产生新的DOM,与之前的DOM比较找到不同,如果层级太深,很消耗内存。

vue3 Diff 最长递增子序列

  1. 静态标记 + 非全量Diff
  2. 使用最长递增子序列进行优化

最长递增子序列 的链接

算法
js
function lengthOfLIS(nums) {
    const tails = []; // 严格递增
    tails[0] = nums[0];
    for (let i = 1; i < nums.length; i++) {
        const num = nums[i]; // 当前值
        if (tails.length && tails[tails.length - 1] < num) {
            tails.push(num);
        } else {
            // 二分法插入当前值
            // 为什么可以无脑插入呢
            // 1.无脑插入不影响已有数组的长度,虽然数组不一定满足
            // 2.因为当前数组有序,后续存在的值如果大于栈顶元素,能保证取值的正确性
            // 省流:不断的去找尽可能每个值都尽可能小且尽可能长的严格递增子序列
            let left = 0;
            let right = tails.length;
            while (left <= right) {
                const mid = Math.floor((left + right) / 2);
                // 这里一定要把 = 的清空放在大于那边
                if (tails[mid] < num) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
            // 贪心算法覆盖
            tails[left] = num;
        }
    }
    return tails.length;
}

console.log(lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]))

上文中这个算法还是存在问题的,因为他能求出最大长度,但是子序列不一定正确,比如你用这个例子[102, 103, 101, 105, 106, 108, 107, 109, 104]得到的是[101,103,104,106,107,109],要解决这个问题,很显然,需要记录每次tails中移动的项目,或者记录每一项的前置节点,诚然都可以实现,但是后者更容易。

算法
js
function getLIS(nums) {
    const len = nums.length;
    const tails = [nums[0]]; // 严格递增
    const preNode = new Array(len).fill(0);
    for (let i = 1; i < len; i++) {
        const cur = nums[i]; // 当前值
        if (tails[tails.length - 1] < cur) {
            // 先存储前置节点,再push
            preNode[i] = tails[tails.length - 1];  
            tails.push(cur);
        } else {
            // 二分法插入当前值
            // 为什么可以无脑插入呢
            // 1.无脑插入不影响已有数组的长度,虽然数组不一定满足
            // 2.因为当前数组有序,后续存在的值如果大于栈顶元素,能保证取值的正确性
            // 省流:不断的去找尽可能每个值都尽可能小且尽可能长的严格递增子序列
            let left = 0;
            let right = tails.length;
            while (left <= right) {
                const mid = Math.floor((left + right) / 2);
                // 这里一定要把 = 的清空放在大于那边
                if (tails[mid] < cur) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
            // 贪心算法覆盖
            tails[left] = cur;
            preNode[i] = tails[left - 1];  
        }
    }
    // 首先要确定一点,tails的最后一项一定是正确的,所以可以从这里下手
    // 定义一个从后往前的游标 last 2 first pointer
    // 把跟当前值前置节点不对应的都替换
    let l2fp = tails.length - 1;  
    for (let i = len - 1; i >= 0; i--) {  
        const cur = nums[i];  
        if (tails[l2fp] === cur) {  
            if (preNode[i] !== tails[l2fp - 1]) tails[l2fp - 1] = preNode[i];  
            l2fp--;  //shou
            if (l2fp === 0) break
        }  
    }  
    return tails; 
}

console.log(getLIS([102, 103, 101, 105, 106, 108, 107, 109, 104]))
// [102, 103, 105, 106, 107, 109]

上述代码还存在改进空间,如果tails中存储的是index的话,那么最后处理的时候可以直接循环tails,不过时间复杂度虽然优化了一丢丢,但是区别不大

具体Vue2/3diff的区别可以看这个 掘金文章

鄂ICP备2024055897号