前言

vue2中的响应式核心是es5的Object.defineProperty,缺点有:

  1. 深度监听需要递归到底,性能层面考虑不太好
  2. 无法监听对象新增属性和删除属性,需要vue特殊处理
  3. 无法监听原生数组,需要在数组原型上做拦截

所以vue3采用了es6之后的proxy去重构了响应式原理,proxy能够很好的解决Object.defineProperty的缺点。


proxy和defineProperty的本质区别

defineProperty是对象的基本操作之一,而proxy是对整个对象所有基本操作的拦截器。也就是proxy包含了defineProperty等等基本操作,而且在调用基本操作的时候,都会被proxy拦截到。

defineProperty里的set和get只是属性描述符。

而proxy里的set和get是真正的基本操作。

换句话来说,defineProperty监听的是属性,而proxy监听的是整个对象。


proxy的基本使用

这里简单记录一下proxy的基本使用,想学习具体的使用细节还需自行查阅资料(下面的Reflect函数如果不理解没关系,不影响我们认识proxy的作用)。

const data = ["a", "b", "c"];

const proxyData = new Proxy(data, { // data对象被Proxy处理后就成了一个Proxy对象
    get(target, key, receiver) {
        console.log("Reflect get", Reflect.ownKeys(target));
        console.log("get", key); // 监听
        const result = Reflect.get(target, key, receiver);
        return result; // 返回执行的方法
    },
    set(target, key, val, receiver) {
        const result = Reflect.set(target, key, val, receiver);
        console.log("set", key, val);
        return result; // 是否设置成功,为一个布尔值
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key);
        console.log("delete property", key);
        return result; // 是否删除成功,为一个布尔值
    },
});

咱们是将原变量做了层代理,以后的操作都只针对代理出来的对象,例如,当我们执行proxyData.push('e')时,就会打印出:

Reflect get (4) ['0', '1', '2', 'length']
get push
Reflect get (4) ['0', '1', '2', 'length']
get length
set 3 e
set length 4

可以发现proxy触发了一些我们不需要的操作,get中不需要读取push,set不需要重新设置length(因为已经自动改变了),所以可以这样改写:

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target);
        if (ownKeys.includes(key)) {
            console.log("get", key); // 监听
        }
        const result = Reflect.get(target, key, receiver);
        return result; // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true;
        }

        const result = Reflect.set(target, key, val, receiver);
        console.log("set", key, val);
        return result; 
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key);
        console.log("delete property", key);
        return result; 
    },
});

这样再次执行就是:

get length
set 3 e

注意:proxy要触发get才能被处理成proxy对象

对于Reflect函数,具体的还需自行查看,不影响理解响应式原理。这里还可以了解一下Reflect的作用:

  1. 和Proxy能力是一一对应的,配合着实现响应式
  2. 让代码书写更加规范标准化,例如原来的'a' in objdelete obj.a
  3. 慢慢替代掉Object上的一些功能方法,让它专心的成为一个类型的构造函数

总结

通过Proxy代理的形式,能够完美的监听到对象和数组,并且采用get触发深度监听的方式大大减少性能上的损耗。

唯一的缺点就是不能兼容所有的浏览器,也无法polyfill。


响应式原理的简单实现

咱们写一个简单的响应式原理,能够更好的加深印象:

// 创建响应式
function reactive(target = {}) {
    if (typeof target !== "object" || target == null) {
        // 不是对象或数组,则返回
        return target;
    }

    // proxy的代理配置,单独拿出来写
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target);
            if (ownKeys.includes(key)) {
                console.log("get", key); // 监听
            }

            const result = Reflect.get(target, key, receiver);

            // 深度监听,因为是触发了get,才会进行递归处理,所以性能会更好些
            return reactive(result);
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true;
            }

            const ownKeys = Reflect.ownKeys(target);
            if (ownKeys.includes(key)) {
                console.log("已有的 key", key);
            } else {
                console.log("新增的 key", key);
            }

            const result = Reflect.set(target, key, val, receiver);
            console.log("set", key, val);
            return result; // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key);
            console.log("delete property", key);
            return result; // 是否删除成功
        },
    };

    // 生成代理对象
    const observed = new Proxy(target, proxyConf);
    return observed;
}

// 测试数据
const data = {
    name: "xiaoming",
    age: {
        young: 18,
        old: 26,
    },
};

const proxyData = reactive(data);

加入视图更新逻辑,首先写好第一步操作:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <ul>
        <li id="name"></li>
        <li id="age"></li>
    </ul>
    <script src="./vue.js">

    </script>
    <script>
        let dataObj = {
            name: '小米',
            age: 1
        }

        // 监听数据
        let proxyObj = reactive(dataObj);
        function renderName() {
            document.querySelector('#name').textContent = proxyObj.name
        }

        function renderAge() {
            document.querySelector('#age').textContent = proxyObj.age
        }
        // 自动第一次获取数据更新视图
        addWatcher(renderName)
        addWatcher(renderAge)
    </script>
</body>

</html>

然后改造下原来的核心逻辑:

// 触发更新视图(新增代码)
let watchers = new Set(); // 要触发更新具体位置的函数集合
function updateView() {
  for (let watcher of watchers) watcher(); // 遍历执行
}

// 传入一个读取函数,通过defineProperty把函数添加到watchers里(新增代码)
function addWatcher(fn) {
  window.__watcher = fn; // 暂时挂在全局上,在get中可以读取到
  fn(); // 第一次读取
  window.__watcher = null;
}

// 创建响应式
function reactive(target = {}) {
  if (typeof target !== "object" || target == null) {
    // 不是对象或数组,则返回
    return target;
  }

  // proxy的代理配置,单独拿出来写
  const proxyConf = {
    get(target, key, receiver) {
      // 只处理本身(非原型的)属性
      const ownKeys = Reflect.ownKeys(target);
      if (ownKeys.includes(key)) {
        // console.log("get", key); // 监听
      }

      const result = Reflect.get(target, key, receiver);
      if (window.__watcher) watchers.add(window.__watcher); // 第一次读取就埋入依赖 !!!!!新增代码
      // 深度监听,因为是触发了get,才会进行递归处理,所以性能会更好些
      return reactive(result);
    },
    set(target, key, val, receiver) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true;
      }

      const ownKeys = Reflect.ownKeys(target);
      if (ownKeys.includes(key)) {
        // console.log("已有的 key", key);
      } else {
        // console.log("新增的 key", key);
      }

      const result = Reflect.set(target, key, val, receiver);
      updateView(); // 更新视图
      return result; // 是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      return result; // 是否删除成功
    },
  };

  // 生成代理对象
  const observed = new Proxy(target, proxyConf);
  return observed;
}

控制台里直接修改proxyObj就可以看到效果了


简单实现下reactive和effect

代码来自慕课网3-13:

const fns = new Set()

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            if (activeFn) fns.add(activeFn)
            return target[key]

            // // obj 嵌套属性 传入的是个多维对象
            // const val = target[key]
            // if (typeof val === 'object' && val != null) { // 只考虑 object ,其他未考虑
            //     return reactive(val) // 直接返回一个 Proxy 对象即可
            // } else {
            //     return val
            // }
        },
        set(target, key, newVal) {
            target[key] = newVal
            fns.forEach(fn => fn())
            return true
        }
    })
}

let activeFn
function effect(fn) {
    activeFn = fn
    fn() // 执行一次,触发 proxy get
}

const user = reactive({ name: '双越' })
effect(() => {
    console.log('name', user.name)
})
user.name = '张三'
setTimeout(() => {
    user.name = '李四'
}, 1000)

// // obj 嵌套属性
// const user = reactive({ name: '双越', info: { city: '北京' } })
// effect(() => {
//     console.log('city', user.info.city)
// })
// user.info.city = '上海'
// setTimeout(() => {
//     user.info.city = '杭州'
// }, 1000)

Ref的原理

前面讲的都是引用类型的响应式机制,那基本类型的呢,其实就是把值弄成{value: '1'}的形式,然后让reactive去处理。

Logo

前往低代码交流专区

更多推荐