vue——ViewModel 简易原理

本文介绍了Vue数据双向绑定的简易原理,涉及Observer、Watcher和Compile。Observer通过Object.defineProperty监听属性变化,Watcher作为通信桥梁,Compile负责模板解析。同时,文章讨论了为何Vue3.0转向使用Proxy替代Object.defineProperty,并指出Proxy的性能和兼容性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据双向绑定简易原理

<input type="text" id="username">
<span id="uName"></span>
  let obj = {}
  Object.defineProperty(obj, 'username', {
  	// 使用 defineProperty 中的 get 来双向绑定数据
    set (v) {
      document.getElementById('uName').innerText = v;
      document.getElementById('username').value = v;
    },
    get () {
      console.log('get')
    }
  })
  document.getElementById('username').addEventListener('keyup', function (e) {
    obj.username = e.target.value
  })

defineProperty 实现原理

const vm = {
    name: 'bob',
    age: 11,
    enjoy: ['1', 'a', 3],
    hobby: { a: 'eat' }
}

const orginalProto = Array.prototype
const arrayProto = Object.create(orginalProto) // 先克隆份 Array 原型
const methodToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]
methodToPatch.forEach(method => {
    arrayProto[method] = function () {
        console.log('method changed: ', method, arguments)
        orginalProto[method].apply(this, arguments)
    }
})

const observe = (data) => {
    if (!data || typeof data !== 'object') return
    if (Array.isArray(data)) {
        // 如果是数组,重写原型链 data.__proto__ = arrayProto
        Object.setPrototypeOf(data, arrayProto);
        for (let i = 0; i < data.length; i++) {
            observe(data[i])
        }
    } else {
        Object.keys(data).forEach(key => {
            dr(data, key, data[key])
        })
    }
}

const dr = (data, key, val) => {
    observe(val) // 递归制造响应式数据
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        set(nv) {
            console.log('val changed: ', nv)
            val = nv
        },
        get() {
            return val
        }
    })
}

observe(vm)

vm.name = 'lucy'
vm.age = 11
vm.hobby.a = 'play'
vm.enjoy.push(123)
vm.sex = 'female' // 监听不到

// 问题:直接 watch 对象下某个属性监听不到
@Watch('searchList', { deep: true, immediate: true })
onSearchListWatch(newVal, oldVal) {
    const getSubBu = (list) => list?.find((item) => item.index === 1)?.subBuCode;
    if (getSubBu(newVal) !== getSubBu(oldVal)) {
        this.initLineList();
    }
}

// 解决
@Watch('oneSearchList', { deep: true, immediate: true })
onSearchListWatch(newVal, oldVal) {
    if (newVal !== oldVal) {
        this.initLineList();
    }
}

get oneSearchList() {
    return this.searchList?.find((item) => item.index === 1)?.subBuCode;
}

缺陷

Object.defineProperty 无法监听新增加的属性
Object.defineProperty 无法一次性监听对象所有属性,如对象属性的子属性
Object.defineProperty 无法响应数组操作
Proxy 拦截方式更多, Object.defineProperty 只有 get 和 set

const vm = {
    name: 'bob',
    age: 11,
    enjoy: ['1', 'a', 3],
    hobby: { a: 'eat' }
}

const observe = (data) => {
    if (!data || typeof data !== 'object') return
    Object.keys(data).forEach(key => {
        if (typeof data[key] === 'object') {
            data[key] = proxyData(data[key])
            observe(data[key])
        }
    })
    return proxyData(data);
}

// Reflect
// Reflect 是一个内建对象,可简化 Proxy 的创建。
// 前面所讲过的内部方法,例如 [[Get]] 和 [[Set]] 等,都只是规范性的,不能直接调用。
// Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。
const proxyData = (data) => new Proxy(data, {
    get(target, propKey, receiver) {
        return Reflect.get(target, propKey, receiver)
    },
    set(target, propKey, value, receiver) {
        console.log('val changed: ', value)
        // target[propKey] = value
        Reflect.set(target, propKey, value, receiver)
        return true
    }
})

const proxyVm = observe(vm);

proxyVm.name = 'lucy'
proxyVm.age = 11
proxyVm.hobby.a = 'play'
proxyVm.enjoy.push(123)
proxyVm.sex = 'female'

3.0 改进

为什么使用 Proxy 可以解决上面的问题呢?主要是因为Proxy是拦截对象,对对象进行一个"拦截",外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中
vue 实现简易原理,其内部主要由 Observer、Compile、Watcher 组成,中间包括 Dep(变化通知)以及 Updater(视图更新)等模块,最终再由 MVVM(new Vue)去汇合

区分 Proxy 和 Decorator 的使用场景,可以概括为:Proxy 的核心作用是控制外界对被代理者内部的访问,Decorator 的核心作用是增强被装饰者的功能

Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
Proxy 用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改

Proxy 性能问题

Proxy 的性能比 Promise 还差
Proxy 作为新标准,从长远来看,JS 引擎会继续优化 Proxy
Proxy 兼容性差(Vue 3.0 中放弃了对于IE的支持)

完整 github 地址
在这里插入图片描述

Observer

  • 利用 Obeject.defineProperty() 来监听属性变动
class Observer {
    constructor(data) {
        this.data = data;
        this.walk(data);
    }
    walk(data) {
        var me = this;
        Object.keys(data).forEach(function (key) {
            me.convert(key, data[key]);
        });
    }
    convert(key, val) {
        this.defineReactive(this.data, key, val);
    }
    defineReactive(data, key, val) {
        var dep = new Dep();
        observe(val); // 监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get() {
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是 object 的话,进行监听
                if (typeof newVal === 'object') observe(newVal);
                // 通知订阅者
                dep.notify();
            }
        });
    }
}
  • 那么将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter
function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
};
  • 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化,其中通过 Dep 来作为调度者,对 Watcher 通知变化以及添加订阅者
var uid = 0;

class Dep {
    constructor() {
        this.id = uid++;
        this.subs = [];
    }
    addSub(sub) { // 添加订阅
        this.subs.push(sub);
    }
    depend() {
        // 此时 Dep.target 指向 Watcher 的 this
        Dep.target.addDep(this);
    }
    removeSub(sub) { // 移除订阅
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    }
    notify() { // 通知订阅者
        this.subs.forEach(function (sub) {
            // sub 为 getter 时添加到 subs 的 Watcher 的前一个状态
            sub.update();
        });
    }
}

Dep.target = null;

Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

  • 1、在自身实例化时往属性订阅器 (dep) 里面添加自己
  • 2、自身必须有一个 update() 方法
  • 3、待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退
class Watcher {
    constructor(vm, expOrFn, cb) {
        this.cb = cb;
        this.vm = vm;
        this.expOrFn = expOrFn;
        this.depIds = {};

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

        // 此处为了触发属性的 getter,从而在 dep 添加自己,结合 Observer 更易理解
        this.value = this.get();
    }
    update() {
        this.run();
    }
    run() {
        var value = this.get(); // 收到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    }
    addDep(dep) {
        // 1. 每次调用run()的时候会触发相应属性的getter
        // getter里面会触发dep.depend(),继而触发这里的addDep
        // 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 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
        // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
        // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
        // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
        // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
        // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
        // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher

        // dep 指向 Observer 的 this
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this); // 添加当前状态的 this 对象(value、expOrFn 等等)到 Observer
            this.depIds[dep.id] = dep;
        }
    }
    get() {
        Dep.target = this; // 将当前订阅者指向自己
        var value = this.getter.call(this.vm, this.vm); // 为 getter 传入自身的 obj,从而获取值
        Dep.target = null; // 添加完毕,重置
        return value;
    }
    parseGetter(exp) {
        if (/[^\w.$]/.test(exp)) return;

        var exps = exp.split('.');

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

compile

  • compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,
  • 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
const compileUtil = {
    text(node, vm, exp) { // v-text
        this.bind(node, vm, exp, 'text');
    },
    html(node, vm, exp) { // v-html
        this.bind(node, vm, exp, 'html');
    },
    bind(node, vm, exp, dir) { // 绑定更新节点
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        // 实例化订阅者,此操作会在对应的属性消息订阅器(有绑定的对象值)中添加了该订阅者 watcher
        new Watcher(vm, exp, function (value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
    // ...省略
}

更新 updater

const updater = { // 更新节点
    textUpdater(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    htmlUpdater(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },
    classUpdater(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');
        
        var space = className && String(value) ? ' ' : '';
        
        node.className = className + space + value;
    },
    modelUpdater(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

compile

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

        if (this.$el) {
            this.$fragment = this.node2Fragment(this.$el);
            this.init();
            this.$el.appendChild(this.$fragment);
        }
    }
    node2Fragment(el) { // 虚拟 dom
        var fragment = document.createDocumentFragment(),
            child;

        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            // fragment.appendChild() 具有移动性,此操作是 move dom
            // 将 el.children[0] 被抽出,在下次操作从 el.children[1] 开始,以达到循环的目的
            fragment.appendChild(child);
        }

        return fragment;
    }
    init() {
        this.compileElement(this.$fragment);
    }
    // 递归遍历所有节点及其子节点,进行扫描解析编译,调用对应指令渲染,并调用对应指令更新函数进行绑定
    compileElement(el) {
        var childNodes = el.childNodes,
            me = this;

        [].slice.call(childNodes).forEach(function (node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;
            // 按元素节点方式编译
            if (me.isElementNode(node)) { // 是否元素节点
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) { // 是否文本节点并符合 {{xxx}} 形式
                // 指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
                // 以此类推,RegExp.$2,RegExp.$3,..RegExp.$99 总共可以有99个匹配
                me.compileText(node, RegExp.$1.trim());
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    }
    // 修改元素节点对应属性的赋值操作
    compile(node) {
        var nodeAttrs = node.attributes,
            me = this;

        [].slice.call(nodeAttrs).forEach(function (attr) {
            var attrName = attr.name;
            // 规定:指令以 v-xxx 命名(v-text、v-html 等)
            if (me.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                if (me.isEventDirective(dir)) {
                    // 事件指令,如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }

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

    compileText(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    }

    isDirective(attr) {
        return attr.indexOf('v-') == 0;
    }

    isEventDirective(dir) {
        return dir.indexOf('on') === 0;
    }

    isElementNode(node) {
        return node.nodeType == 1;
    }

    isTextNode(node) {
        return node.nodeType == 3;
    }
}

MVVM

  • MVVM作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,
  • 通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;
  • 视图交互变化 (input) -> 数据 model 变更的双向绑定效果
class MVVM {
    constructor(options) {
        this.$options = options || {};
        var data = this._data = this.$options.data;
        var me = this;
        // 属性代理,实现 vm.xxx -> vm._data.xxx
        Object.keys(data).forEach(function (key) {
            me._proxyData(key);
        });

        this._initComputed();

        // 对所有对象启动监听
        observe(data, this); // new Observer

        // 启动编译
        this.$compile = new Compile(options.el || document.body, this)
    }

    // 监听需要用到的值
    $watch(key, cb, options) {
        new Watcher(this, key, cb);
    }

    // 代理 this 中的 data
    _proxyData(key, setter, getter) {
        var me = this;
        setter = setter ||
            Object.defineProperty(me, key, {
                configurable: false,
                enumerable: true,
                get: function proxyGetter() {
                    return me._data[key];
                },
                set: function proxySetter(newVal) {
                    me._data[key] = newVal;
                }
            });
    }

    // 代理 this 中的 computed
    _initComputed() {
        var me = this;
        var computed = this.$options.computed;
        if (typeof computed === 'object') {
            Object.keys(computed).forEach(function (key) {
                Object.defineProperty(me, key, {
                    get: typeof computed[key] === 'function'
                        ? computed[key]
                        : computed[key].get,
                    set: function () { }
                });
            });
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值