# 👉 Vue 的双向绑定

Vue.js 主要采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,然后触发相应的监听回调。

# Object.defineProperty()是用来做什么的

Object.defineProperty 可以在一个对象上定义新属性或修改现有属性,并返回此对象。同时,它也可以用于控制一个对象属性的一些特有操作,比如读写权、是否可以枚举。

const obj1 = {};
Object.defineProperty(obj1, "property1", {
    value: "i am property1",
    writable: false, // 不可写
});

obj1.property1 = "i has changed";

console.log(obj1); // {property1: "i am property1"}

这里先简单了解一下Object.defineProperty的两个描述属性 get 和 set 用法:

一个简单的栗子:

const book1 = {};
Object.defineProperty(book1, "name", {
    set: (val) => {
        name = val;
        console.log("将 book1 取名为:", val);
    },
    get: () => {
        return "《" + name + "》";
    },
});

book1.name = "锦鲤";
console.log("获取 book1 的书名:", book1.name);

// 将 book1 取名为: 锦鲤
// 获取 book1 的书名:《锦鲤》

# 简述

Vue 在初始化数据的时候,通过 Object.defineProperty 的 getter(数据依赖收集处理) 和 setter(监听数据的变化,并通知订阅) 对 data 里面的每个属性对象进行劫持监听和属性代理。

当页面使用对应属性时,首先会通过 Dep 进行依赖收集(收集 Watcher) ;如果属更新了,会通知相关的依赖进行更新操作(发布订阅) 。

过程:

(1)初次渲染,根据传入的 data 和 render 函数,生成对应的 VNode Tree。

这期间会有一次依赖收集过程,建立一个直接对应 render 的 Watcher,且 v-if 为 false 的元素不会出现在生成的 VNode Tree 中,最后调用 patch,因为没有 oldVnode, 会直接将 VNode 渲染为真实的 DOM。

(2)新建实例时,vue 会调用 Compile 将 el 转换成 VNode(解析 new Vue 传入的 el 或 template 元素中的元素,生成 AST 树,返回构建虚拟节点 VNode);

(3)开始监听数据变化,创建 props、data 的钩子以及其对象成员的 Observer,同时进行 data 的属性代理;

    // new Vue 执行流程。
    // 1. Vue.prototype._init(option)
    // 2. vm.$mount(vm.$options.el)
    // 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
    // 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
    // 5. mountComponent

    // 6. 创建 Watcher 实例
    // 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
    new Watcher(vm, updateComponent);

    // 7. new Watcher 会执行 Watcher.get 方法
    // 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法

    // 9. updateComponent 会执行 vm._update(vm._render())
    function updateComponent() {
    // 将生成的JS模版当参数传递
    vm._update(vm._render())
    }

    // 10.调用 vm._render 会生成虚拟 dom
    Vue.prototype._render = function () {
    const vm = this;
    const { render } = vm.$options;
    let vnode = render.call(vm._renderProxy, vm.$createElement);
    return vnode;
    };

    // 11. 调用 vm._update(vnode) 渲染虚拟 dom
    Vue.prototype._update = function (vnode:) {
    const vm = this;

    // 上一次更新的vnode
    const prevVnode = vm._vnode;

    // 下一次对比的vnode
    vm._vnode = vnode;

    if (!prevVnode) {
        // 第一次执行,初次渲染
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
    } else {
        // 更新
        vm.$el = vm.__patch__(prevVnode, vnode);
    }
    };

    // 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom。

(4)之后,每当属性更新时,都将通知相应的 watcher 执行回调函数,更新视图:
a. 当给这个对象的某属性赋值时,就会触发 set 方法;
b. set 函数调用,触发属性消息器 Dep 的 notify 向对应的 watcher 通知变化;
c. watcher 调用 upddate 方法更新视图。

# 实现思路

要实现响应式( MVVM 的双向绑定),主要需要实现以下几个部分:
(1)Observe:数据监听器,用于监听数据对象的所有属性,有变动时获取最新值并通知订阅者;

(2)DepWatcher:发布者和订阅者,连接 Observe 和 Compile,用于订阅并且接收到每个属性变动的消息,并触发解析器中绑定的相应回调函数,从而跟新视图。

(3)Compile:指令解析器,用于对每个元素节点指令的扫描和解析,以及绑定对应的回调函数;

(4)MVVM 入口函数

# 实现代码

# Observer

Observer 的主要功能是监听数据对象的所有属性,通过遍历所有属性,为属性添加订阅者,并在属性发生变化后通知订阅者。

// Observer
const Observer = function(data) {
    this.data = data;
    this.mapToConvet(data);
};

const observer = function(data) {
    if (!data || typeof data !== "object") {
        return;
    }
    return new Observer(data);
};

Observer.prototype = {
    constructor: Observer,

    mapToConvet: function(data) {
        const _this = this;

        // 遍历每个属性进行监听
        Object.keys(data).forEach((key) => {
            _this.defineReactive(data, key, data[key]);
        });
    },

    defineReactive: function(data, key, val) {
        const dep = new Dep();

        // 监听子属性
        let childObj = observer(val);

        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function() {
                // 需要为属性添加订阅者,而订阅者是Watcher,因此需要在闭包内添加 watcher,所以通过Dep定义一个全局target属性,暂存 watcher, 添加完移除
                if (Dep.target) {
                    dep.depend();
                }

                console.log("get: ", key, " = ", val);
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }

                console.log("change detected: ", val, " -> ", newVal);
                val = newVal;

                // 新的值是object的话,需要进行监听
                childObj = observer(newVal);

                // 通知所有订阅者
                dep.notify();
            },
        });
    },
};

# Dep

Dep 相当于发布者的角色,主要负责收集订阅者,并在属性更新后触发 notify,去通知对应的订阅者,调用订阅者的 update。

let uid = 0;

const Dep = function() {
    //每个Dep都有唯一的ID
    this.id = uid++;

    //subs用于存放依赖
    this.subs = [];
};

// 用于暂时缓存 watcher
Dep.target = null;

Dep.prototype = {
    // 向subs数组添加依赖
    addSub: function(sub) {
        this.subs.push(sub);
    },

    // 移除依赖
    removeSub: function(sub) {
        const index = this.subs.indexOf(sub);
        if (index > -1) {
            this.subs.splice(index, 1);
        }
    },

    // 设置某个Watcher的依赖
    // 这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
    // 也就是说判断是否通过的this.get调用的,而不是普通调用
    depend: function() {
        if (Dep.target) {
            // 触发watcher的addDep
            Dep.target.addDep(this);
        }
    },

    notify: function() {
        this.subs.forEach(function(sub) {
            // 调用订阅者的update方法,通知变化
            sub.update();
        });
    },
};

# Watcher

Watcher,作为订阅者主要做的事情是:
(1)需要在自身实例化的时候往属性订阅器里添加自己;
(2)自身需有一个 update() 的方法;
(3)在属性变动触发属性订阅器 dep.notice() 时,能调用自身的 update() 方法,并且进一步触发 Compile 中绑定的更新回调函数。

/**
    @param vm 节点
    @param exp 节点指令绑定的变量名/函数事件名
    @param cb 节点指令的更新回调函数
    */
const Watcher = function(vm, expOrFn, cb) {
    this.callback = cb;
    this.vm = vm;
    this.exp = expOrFn;

    // 自己被哪些订阅收集器给收集
    this.depsIds = {};

    if (typeof expOrFn === "function") {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expOrFn.trim());
    }

    // 此处为了触发属性的getter,从而在dep添加自己
    this.value = this.get();
};

Watcher.prototype = {
    constructor: Watcher,

    get: function() {
        // 将当前订阅者指向自己
        // 通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。
        Dep.target = this;

        // 强制触发执行监听器里的getter,把自己添加到属性订阅器中
        const value = this.getter.call(this.vm, this.vm);

        // 添加完毕,释放自己重置
        Dep.target = null;
        return value;
    },

    parseGetter: function(exp) {
        // 不匹配字母、数字、下划线、点号和$符号
        if (/[^\w.$]/.test(exp)) return;

        const exps = exp.split(".");

        return function(obj) {
            for (let i = 0; i < exps.length; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        };
    },

    update: function() {
        // dep.notice()触发通知后执行
        this.run();
    },

    run: function() {
        const newVal = this.get();
        const oldVal = this.value;

        if (newVal !== oldVal) {
            // 更新最新值
            this.value = newVal;

            // 执行Compile中绑定的回调,更新视图
            this.callback.call(this.vm, newVal, oldVal);
        }
    },

    addDep: function(dep) {
        // 1.每次调用 run 的时候都会触发相应属性的getter,然后触发到这里的addDep。进而往属性订阅器里面添加自己,并将这个属性发布者id存进depsId里。

        // 2.假如相应属性的dep.id已存在watcher的depIds里,说明这不是个新属性,仅需改变其值,而不需将当前watcher添加至该属性的dep里。
        // 3.假如相应属性是新的属性,则将当前watcher添加到新属性订阅器dep里。

        // 若通过 vm.child = {name: 'a'},改变了 child.name 的值,child.name就是个新属性,则需要将当前 watcher(child.name) 加入到新的child.name的dep中
        // 若通过 child.name = xxx 进行重新赋值的时候,对应的 watcher 则不会收到通知,等于失效了

        // 4.每个子属性的 watcher 添加到子属性的 dep 同时,也会添加到父属性的 dep
        // 监听子属性的同时监听父属性的变更,这样子父属性改变时,子属性的 watcher 也能收到通知进行 update
        // 这一步是在 this.get() -> this.getVMVal 中完成,forEach 时会从父级开始取值,间接调用了它的 getter,进而触发了 addDep()
        // addDep()中,在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep

        // 如: 当前 wacther 是 child.child.name,那么 child、child.child、child.child.name 这三个属性的dep都会加入当前watcher

        if (!this.depsIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depsIds[dep.id] = dep;
        }
    },
};

# Compile

对 template 模板中进行编译,编译成真正的 html,在编译的过程中对 vue 的指令解析。

Compile 主要做的事情是:
1) 首先是深度遍历 dom 树,遍历每个节点以及子节点;
2) 解析模板指令,将模板的变量替换成数据,然后初始化渲染页面视图;
3) 添加监听数据的订阅者,将每个指令对应的节点绑定更新函数。一旦数据有变动,收到通知,更新视图。

const Compile = function(el, vm) {
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);

    if (this.$el) {
        // 因为遍历解析过程有多次操作DOM节点,为提高性能和效率,会先将vue实例根节点的 el 转换成文档碎片 fragments 进行解析编译操作,再将 fragments 添加回原来的真实Dom节点中
        this.$fragments = this.nodeTofragments(this.$el);

        // 初始化
        this.init();

        this.$el.appendChild(this.$fragments);
    }
};

Compile.prototype = {
    constructor: Compile,

    isElementNode: function(node) {
        // nodeType 为 1 时代表为元素 Element
        return node.nodeType === 1;
    },

    isTextNode: function(node) {
        // nodeType 为 3 时代表为元素 Element
        return node.nodeType === 3;
    },

    isDirective: function(attr) {
        return attr.indexOf("v-") === 0;
    },

    isEventDirective: function(directive) {
        return directive.indexOf("on") === 0;
    },

    init: function() {
        this.compileElement(this.$fragments);
    },

    nodeTofragments: function(el) {
        const fragments = document.createDocumentfragments();
        let child;

        // 将原生节点拷贝到 fragments
        // while(child = el.firstChild) 执行两个操作:
        // 先把el.firstChild(即el.children[0])抽出赋值给child,然后将child插入至fragments中。
        // 这个操作是移动dom,一旦el.children[0]被抽出,在下次循环执行child = el.firstChild时,读取的是el.children[1]

        while ((child = el.firstChild)) {
            // 将从文档树中删除,然后按序重新插入fragments当前节点的 childNodes[] 数组的末尾
            // 相当于把el中节点移动过去
            fragments.appendChild(child);
        }

        return fragments;
    },

    // 遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应指令更新函数进行绑定
    compileElement: function(el) {
        const childNodes = el.childNodes;

        const _this = this;
        [].slice.call(childNodes).forEach(function(node) {
            // 获取节点文本内容
            const text = node.textContent;

            // 模板数据匹配正则
            const reg = /\{\{(.*)\}\}/;

            // 根据不同节点类型选择编译方式
            if (_this.isElementNode(node)) {
                _this.compileElementNode(node);
            } else if (_this.isTextNode(node) && reg.test(text)) {
                _this.compileTextNode(node, RegExp.$1.trim());
            }

            // 遍历编译其子节点
            if (node.hasChildNodes() && node.childNodes.length) {
                _this.compileElement(node);
            }
        });
    },

    compileElementNode: function(node) {
        const _this = this;
        const nodeAttrs = node.attributes;

        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如:v-on:click="open"、v-text="content"
            const attrName = attr.name;

            // 处理指令属性
            if (_this.isDirective(attrName)) {
                // 指令绑定的函数事件名
                const exp = attr.value;

                // 如:on:click、text
                const directive = attrName.substring(2);

                // 事件指令
                if (_this.isEventDirective(directive)) {
                    compileUtils.eventHandler(node, _this.$vm, exp, directive);

                    // 普通指令
                } else {
                    compileUtils[directive] &&
                        compileUtils[directive](node, _this.$vm, exp);
                }

                node.removeAttribute(attrName);
            }
        });
    },

    compileTextNode: function(node, exp) {
        compileUtils.text(node, this.$vm, exp);
    },
};

const compileUtils = {
    // 事件处理
    eventHandler: function(node, vm, exp, directive) {
        const eventType = directive.split(":")[1];
        const fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, "model");

        const _this = this;
        let val = this._getVMVal(vm, exp);
        node.addEventListener("input", function(e) {
            const newVal = e.target.value;
            if (val === newVal) {
                return;
            }

            _this._setVMVal(vm, exp, newVal);
            val = newVal;
        });
    },

    text: function(node, vm, exp) {
        this.bind(node, vm, exp, "text");
    },

    bind: function(node, vm, exp, directive) {
        const updaterFn = updater[directive + "Updater"];

        // 第一次初始化视图
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));

        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加该订阅者 watcherInstance
        new Watcher(vm, exp, function(value, oldVal) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldVal);
        });
    },

    _getVMVal: function(vm, exp) {
        let val = vm;
        exp = exp.split(".");

        // 获取链式调用最后一个事件的值??
        exp.forEach(function(k) {
            val = val[k];
        });

        return val;
    },

    _setVMVal: function(vm, exp, value) {
        let val = vm;
        exp = exp.split(".");

        // 执行链式调用最后一个事件并赋值
        exp.forEach(function(key, index) {
            if (index < exp.length - 1) {
                val = val[key];
            } else {
                // 执行最后一个key,更新val
                val[key] = value;
            }
        });
    },
};

// 更新函数
const updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == "undefined" ? "" : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == "undefined" ? "" : value;
    },

    modelUpdater: function(node, value) {
        node.value = typeof value == "undefined" ? "" : value;
    },

    classUpdater: function(node, vlaue, oldValue) {
        const className = node.className;
        className = className.replace(oldValue, "").replace(/\s$/, "");

        const space = className && String(value) ? " " : "";
        node.className = className + space + value;
    },
};

# MVVM

MVVM 是数据绑定的入口,整合了 Observe、Compile 和 Watcher 三者。

简单的 MVVM 构造器实现是:

const MVVM = function(options) {
    this.$options = options;
    this._data = this.$options.data;

    const data = this._data;
    const _this = this;

    // 属性代理,实现 vm.xxx === vm._data.xxx
    Object.keys(data).forEach(function(key) {
        _this._proxy(key);
    });

    this._initComputed();

    observer(data, this);

    this.$compile = new Compile(options.el || document.body, this);
};

MVVM.prototype = {
    constructor: MVVM,

    // 通过 Object.defineProperty()来劫持vm实例对象属性的读写权限,使得对 vm.xxx 的读写都转成 vm._data.xxx的方式
    _proxy: function(key) {
        const _this = this;
        Object.defineProperty(this, key, {
            configurable: false,
            enumerable: true,
            get: function() {
                return _this._data[key];
            },
            set: function(newVal) {
                _this._data[key] = newVal;
            },
        });
    },

    _initComputed: function() {
        const _this = this;
        const computed = this.$options.computed;
        if (typeof computed === "object") {
            Object.keys(computed).forEach(function(key) {
                Object.defineProperty(_this, key, {
                    get:
                        typeof computed[key] === "function"
                            ? computed[key]
                            : computed[key].get,
                    set: function() {},
                });
            });
        }
    },
};
  • 实现例子
<div id="mvvm-app">
    <input type="text" v-model="word" />
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>
var vm = new MVVM({
    el: "#mvvm-app",
    data: {
        word: "Hello World!",
    },
    methods: {
        sayHi: function() {
            this.word = "Hi, everybody!";
        },
    },
});

参考文章:
https://www.freesion.com/article/62001224032/

https://www.cnblogs.com/libin-1/p/6893712.html

https://github.com/DMQ/mvvm

https://segmentfault.com/a/1190000016208088#item-3

https://blog.51cto.com/zhoulujun/2350337

# Vue3.x 响应式数据原理

Vue3.x 和 Vue2.x 本质上就是基于 Proxy 和基于 Object.defineProperty 之间的差异。

# Vue2.x 的响应规则

对象: 递归遍历每个属性,给每个属性增加 getter 和 setter;数组: 重写数组的方法,调用数组方法会触发更新,也会监控数组中的每一项;缺点: 对象只能监控自带属性,新增属性不能监控(除非 vue.$set);数组的索引更新或长度更新不会触发实体更新,针对数组需要重写数组方法。

export function set(target: Array<any> | Object, key: any, val: any): any {
    // target 为数组
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
        target.length = Math.max(target.length, key);
        // 利用数组的splice变异方法触发响应式
        target.splice(key, 1, val);
        return val;
    }
    // key 已经存在,直接修改属性值
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
    }
    const ob = (target: any).__ob__;

    // target 本身就不是响应式数据, 直接赋值
    if (!ob) {
        target[key] = val;
        return val;
    }
    // 对属性进行响应式处理
    defineReactive(ob.value, key, val);
    ob.dep.notify();
    return val;
}

# Vue3.x 的 proxy

Vue3.x 为什么要用 proxy 替代 defineProperty?

Proxy 在对目标对象的操作之前提供了拦截,可以对外界操作进行过滤或改写。通过操作对象的代理对象来间接操作对象,而无需直接操作对象本身。例子:

const obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };

const handler = {
    get(target, key) {
        console.log("get:", target);
        return target[key];
    },
    set(target, key, value) {
        target[key] = value;
        console.log("set:", target);
    },
};

const proxyObj = new Proxy(obj, handler);

console.log(proxyObj.arr);
// get:  {arr: ["eat", "drink", "play"], name: "ccc"}
// ["eat", "drink", "play"]

proxyObj.name = "ccc";
// set: {name: "ccc", arr: ["eat", "drink", "play"]}

Proxy 支持的拦截操作一共 13 种,更详细的可以戳 MDN (opens new window)

新的写法中会用 Reflect 处理,Reflect 是内置对象,为操作对象而提供的新 API。
将 Object 对象的属于语言内部的方法放到 Reflect 对象上,即从 Reflect 对象上拿 Object 对象内容方法

上面的例子改写:

const obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };

const handler = {
    get(target, key) {
        console.log("get:", target, target[key]);

        // Reflect 不是函数对象,因此不能和 new 运算符一起用,类似于 Math 对象,这个方法里面包含了多个和 Object 对应 API
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        // 这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        // target[key] = value;

        console.log("set:", target, key, target[key], " -> ", value);
        return Reflect.set(target, key, value);
    },
};

const proxyObj = new Proxy(obj, handler);

console.log(proxyObj.arr);
// get:  {arr: ["eat", "drink", "play"], name: "ccc"}
// ["eat", "drink", "play"]

proxyObj.name = "ccc";
// set: {name: "ccc", arr: ["eat", "drink", "play"]},name,{name: "hhh"}  ->  ccc

# Proxy 代理对象是数组时,多次触发 set / get,如何优化?

由上一个问题的最后实例代码可知,当 Proxy 代理一个数组时,会引起多次 get 或 set 的问题。

set 会执被触发两次,一次赋值时触发,另外还会在修改 length 时触发。对此,Vue3.0 的优化方法:

let obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };

const reactive = function(data, fn) {
    const handler = {
        get(target, key, receiver) {
            console.log("get:", target, key, target[key]);

            if (typeof target[key] === "object" && target[key] !== null) {
                // 递归代理
                return reactive(target[key]);
            }

            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            const hadKey = Object.hasOwnProperty.call(target, key);
            const oldVal = target[key];
            const result = Reflect.set(target, key, value, receiver);

            // 新增属性首次触发set,hadKey为false,会触发
            // 修改属性时,hadKey为false,当属性值和旧值不一样时会触发

            // 当代理对象为数组时,传入的key为length时,length为自身属性因此hadKey为false,接着因为value是数组当前的length,oldValue也为target['length'],因此判断oldVal !== value的值也为false,所以会跳过trigger,实现重复触发trigger的效果
            if (!hadKey || oldVal !== value) {
                console.log(
                    "trigger set:",
                    target,
                    key,
                    oldVal,
                    " -> ",
                    value
                );
            }

            return result;
        },
    };

    return new Proxy(data, handler);
};

const proxyObj = reactive(obj);

proxyObj.arr.push("sleeping");
// get: {name: {…}, arr: ["eat", "drink", "play"]}  arr  ["eat", "drink", "play"]
// get: ["eat", "drink", "play"]  push   ƒ push() { [native code] }
// get: ["eat", "drink", "play"]  length   3
// set: ["eat", "drink", "play", "sleeping"] 3 undefined  ->  sleeping

proxyObj.arr[1] = "play";
// get: {name: {…}, arr: ["eat", "drink", "play"]}  arr  ["eat", "drink", "play"]
// trigger set:["eat", "play", "play"] 1 drink  ->  play

# Proxy 深度侦测数据

Proxy 只会代理对象的第一层,Vue3 是怎样处理这个问题(深度侦测数据)的呢?

判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。

proxy 可以直接监听整个对象。省去了对对象属性 for in 的遍历过程,提升了效率,并且可以监听数组,不用再去单独的对数组做特异性操作。

但值得注意的是,当对象是多层嵌套的对象时,对多层级的对象操作时,set 并不能感知到,但是 get 会被触发,此时可利用 Reflect.get() 返回的“多层级对象中内层”,判断返回值是否为 Object,如果是则再对“内层数据” reactive(递归进行new proxy())方法做代理,这样才能实现深度观测。

// 我们可以通过它们,找到任何代理过的数据是否存在,以及通过代理数据找到原始的数据。
// 存放原始数据
const rawToReactive = new WeakMap();

// 存放响应数据
const reactiveToRaw = new WeakMap();

const handler = {
    get(target, key, receiver) {
        let result = Reflect.get(target, key, receiver);
        console.log("get:", target, key, target[key]);

        return typeof result === "object" ? reactive(target[key]) : result;
    },

    set(target, key, value, receiver) {
        const hadKey = Object.hasOwnProperty.call(target, key);
        const oldVal = target[key];

        // 从响应数据中获取
        value = reactiveToRaw.get(value) || value;

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

        // 新增属性首次触发set,hadKey为false,会触发
        // 修改属性时,hadKey为false,当属性值和旧值不一样时会触发

        // 当代理对象为数组时,传入的key为length时,length为自身属性因此hadKey为false,接着因为value是数组当前的length,oldValue也为target['length'],因此判断oldVal !== value的值也为false,所以会跳过trigger,实现重复触发trigger的效果
        if (!hadKey || oldVal !== value) {
            console.log("trigger set:", target, key, oldVal, " -> ", value);
        }

        return result;
    },
};

const reactive = function(target) {
    let observed = rawToReactive.get(target);

    // 原数据已经有相应的可响应数据, 返回可响应数据
    if (observed) {
        return observed;
    }

    // 原数据已经是可响应数据
    if (reactiveToRaw.has(target)) {
        return target;
    }

    observed = new Proxy(target, handler);
    // 原数据保存对应的可响应数据
    reactiveToRaw.set(target, observed);

    // 保存响应数据
    rawToReactive.set(observed, target);

    return observed;
};

// 实例
const obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };
const proxyObj = reactive(obj);

proxyObj.name.name = "ccc";
// get: {name: {name: "ccc"}, arr: Array(3)} name {name: "hhh"}
// trigger set: {name: "ccc"} name hhh  ->  ccc

proxyObj.arr.push("sleeping");
// get:{name: {name: "ccc"}, arr: ["eat", "drink", "play"]} arr ["eat", "drink", "play"]
// get:["eat", "drink", "play"] push ƒ push() { [native code] }
// get:["eat", "drink", "play"] length 3
// trigger set: (4) ["eat", "drink", "play", "sleeping"] 3 undefined  ->  sleeping

参考文章: https://juejin.im/post/5d99be7c6fb9a04e1e7baa34

由上代码可知, Vue3 也并非简单的通过 Proxy 来递归侦测数据。深度侦测数据主要利用 get 操作递归侦测来实现内部数据的代理,并且结合 WeakMap 来对数据保存,这将大大提高响应式数据的性能。