【面试】Vue 双向数据绑定代码

在这里插入图片描述

0. 目标

  • Vue
    • 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  • Observer
    • 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
  • Compiler
    • 解析每个元素中的指令/插值表达式,并替换成相应的数据
  • Dep
    • 添加观察者(watcher),当数据变化通知所有观察者
  • Watcher
    • 数据变化更新视图

目标

<div id="app">
    <h2>差值表达式</h2>
    <p>{{msg}}</p>
    <p>{{count}}</p>
    <hr>
    <h2>v-text</h2>
    <p v-text="msg"></p>
    <hr>
    <h2>v-model</h2>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
</div>

1. Vue

  • 功能

    • 负责接收初始化的参数(选项)
    • 负责把 data 中的数据代理到 Vue 实例(此时使用 Vue 实例访问 data 中的第一层简单数据类型,具备劫持效果)
    • 负责调用 observer 监听 data 中所有属性的变化
    • 负责调用 compiler 解析指令/插值表达式
  • 结构

    • 属性:$options、$el、$data
    • 方法:_proxyData

负责把 data 中的属性以数据劫持的形式代理到 Vue 实例

class Vue {
    constructor(options) {
        // 1. 通过属性保存选项的数据
        this.$options = options || {};
        this.$data = options.data || {};
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
        // 2. 数据代理(把 data 中的成员注入到 Vue 实例中)
        this._proxyData(this.$data);
        // 3. 调用 observer 对象,监听数据的变化
        // 4. 调用 compiler 对象,解析指令和差值表达式
    }
    _proxyData(data) {
        // 遍历 data 中的所有属性,注入到 vue 实例中
        Object.keys(data).forEach(key => {
            // this 就是 Vue 实例
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    // 数据是在 data 中的
                    return data[key];
                },
                set(newValue) {
                    if (newValue === data[key]) return;
                    data[key] = newValue;
                }
            });
        });
    }
}

2. Observer

  • 功能
    • 负责把 data 选项中的属性转换成响应式数据(数据劫持)
    • data 中的某个属性值也是对象,把该属性值也转换成响应式数据(数据递归劫持)
    • 数据变化发送通知
  • 结构
    • 方法:walk(data)、defineReactive(data, key, value)

负责把 data 选项中的属性转换成响应式数据

class Observer {
    constructor(data) {
        this.walk(data);
    }
    walk(data) {
        // 1. 判断 data 是否是空置或不是对象
        if (!data || typeof data !== 'object') return;
        // 2. 遍历 data 对象的所有属性
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
        });
    }
    defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                return val;
            },
            set(newValue) {
                if (newValue === val) return;
                val = newValue;
                // 发送通知
            }
        });
    }
}
// 测试
vm.msg
// 先触发 vue.js 中 _proxyData 中的 get 方法,get 方法中进行了 `data[key]`,所以会再触发 observer.js 中 defineReactive 中的 get 方法

为什么修改 defineReactive(obj, key, val) 中的局部变量 val 能影响到 obj[key] 中数据,闭包!

const obj = {
    msg: 'hello'
};
/* Object.defineProperty(obj, 'msg', {
    enumerable: true,
    configurable: true,
    get() {
        return obj['msg'];
    },
    set(newValue) {
        if (newValue === obj['msg']) return;
        obj['msg'] = newValue;
    }
}); */

/* let xxx = obj.msg;
Object.defineProperty(obj, 'msg', {
    enumerable: true,
    configurable: true,
    get() {
        return xxx;
    },
    set(newValue) {
        if (newValue === xxx) return;
        xxx = newValue;
    }
}); */

defineReactive(obj, 'msg', obj['msg']);

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 形成闭包,不释放
            return val;
        },
        set(newValue) {
            if (newValue === val) return;
            val = newValue;
        }
    });
}

递归递归劫持

const obj = {
    person: {
        name: 'ifer'
    }
};

walk(obj);

function walk(data) {
    if (!data || typeof data !== 'object') return;
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key]);
    });
}

function defineReactive(obj, key, val) {
    walk(val);
    // console.log(key, val); // 'name','ifer' 和 'person', {name: 'ifer'}
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 形成闭包,不释放
            return val;
        },
        set(newValue) {
            if (newValue === val) return;
            val = newValue;
        }
    });
}

已存在的属性重新赋值一个对象,也期望是响应式的

class Observer {
    // ...
    defineReactive(obj, key, val) {
        let that = this;
        this.walk(val);

        Object.defineProperty(obj, key, {
            // ...
            set(newValue) {
                if (newValue === val) return;
                val = newValue;
                // 给已存在的属性重新赋值一个对象
                that.walk(newValue);
                // 发送通知
            }
        });
    }
}
// 注意 vm.msg 会先触发 _proxyData 中的 set 操作
// set 操作中有进行 data[key] 取值的操作,所以下面操作即会触发 defineReactive 中的 get,也会触发 set
vm.msg = { name: 'ifer' };

3. Compiler

  • 功能
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图
  • 结构
    • 属性:el、vm
    • 方法:compile(el)、compileElement(node)、compileText(node)、isDirective(attrName)、isTextNode(node)、isElementNode(node)

基本框架

class Compiler {
    constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
    }
    // 编译模板,处理【文本节点】和【元素节点】
    compile(el) {

    }
    // 编译元素节点,处理指令
    compileElement(node) {}
    // 编译文本节点,处理差值表达式
    compileText(node) {

    }
    // 判断元素属性是否是指令
    isDirective(attrName) {
        return attrName.startsWith('v-');
    }
    // 判断节点是否是文本节点
    isTextNode(node) {
        return node.nodeType === 3;
    }
    // 判断节点是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

实现 compile 方法

class Compiler {
    constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
        this.compile(this.el);
    }
    // 编译模板,处理【文本节点】和【元素节点】
    compile(el) {
        // childNodes => 包括元素节点和文本节点
        let childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isTextNode(node)) {
                // 处理文本节点
                this.compileText(node);
            } else if (this.isElementNode(node)) {
                // 处理元素节点
                this.compileElement(node);
            }

            // 判断 node 是否有子节点,如果有,则递归调用 compile
            if (node.childNodes && node.childNodes.length) {
                this.compile(node);
            }
        });
    }
    // ...
}

实现 compileText 方法

class Compiler {
    // ...
    // 编译文本节点,处理差值表达式
    compileText(node) {
        // 以原始对象的形式进行输出
        // console.dir(node);
        // ? 代表非贪婪,尽可能早的结束匹配
        let reg = /\{\{(.+?)\}\}/;
        let value = node.textContent; // {{msg}}
        if (reg.test(value)) {
            let key = RegExp.$1.trim(); // msg
            node.textContent = value.replace(reg, this.vm[key]);
        }
    }
    // ...
}

实现 compileElement 方法

class Compiler {
    // ...
    // 编译元素节点,处理指令
    compileElement(node) {
        // console.log(node.attributes);
        Array.from(node.attributes).forEach(attr => {
            // 判断是否是指令
            // <div v-text="msg"></div>
            let attrName = attr.name; // 'v-text'
            if(this.isDirective(attrName)) {
                attrName = attrName.substring(2); // 'text'、'model'
                let key = attr.value; // 'msg'
                this.update(node, key, attrName);
            }
        });
    }

    update(node, key, attrName) {
        let updateFn = this[attrName+'Updater'];
        updateFn && updateFn(node, this.vm[key]);
    }
    // 处理 v-text 指令
    textUpdater(node, value) {
        node.textContent = value;
    }
    // 处理 v-model 指令
    modelUpdater(node, value) {
        node.value = value;
    }
    // ...
}

4. Dep

  • 功能
    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
  • 结构
    • 属性:subs
    • 方法:addSub(sub)、notify()

在这里插入图片描述

// dep.js
class Dep {
    constructor() {
        // 存储所有的观察者(订阅者)
        this.subs = [];
    }
    // 添加观察者
    addSub(sub) {
        if(sub && sub.update) {
            this.subs.push(sub);
        }
    }
    // 发送通知
    notify() {
        this.subs.forEach(sub => sub.update());
    }
}
// observer.js
class Observer {
    // ...
    // 负责 data 数据的递归劫持,一上来的时候就会调用
    defineReactive(obj, key, val) {
        let that = this;
        // #0 负责【收集依赖】,并【发送通知】
        let dep = new Dep();
        this.walk(val);

        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // #1 收集依赖
                Dep.target && dep.addSub(Dep.target);
                // obj 就是 data 对象,不能使用 obj[key]
                // 这里涉及到了闭包,来扩展了 val 的作用范围
                return val;
            },
            set(newValue) {
                if (newValue === val) return;
                val = newValue;
                // 给已存在的属性重新赋值一个对象
                that.walk(newValue);
                // #2 发送通知
                dep.notify();
            }
        });
    }
}

5. Wather

  • 功能
    • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
    • 自身实例化的时候往 dep 对象中添加自己
  • 结构
    • 属性:vm、key、cb、oldValue
    • 方法:update()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xTD3sGmZ-1615428428009)(./img/Watcher.png)]

class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm;
        // data 的属性名
        this.key = key;
        // 回调函数负责更新视图
        this.cb = cb;

        // 把 watcher 对象记录到 Dep 类的静态属性 target 上
        Dep.target = this;
        // 触发 get 方法,在 get 方法中会调用 addSub
        this.oldValue = vm[key]; // vm[key] 触发 get,会把 wather 添加到 dep
        Dep.target = null;
    }
    // 当数据发生变化的时候更新视图
    update() {
        let newValue = this.vm[this.key];
        if (this.oldValue === newValue) return;

        this.cb(newValue);
    }
}

调用 Watcher

// compiler.js
class Compiler {
    // ...
    // 编译文本节点,处理差值表达式
    compileText(node) {
        // 以原始对象的形式进行输出
        // console.dir(node);
        // ? 代表非贪婪,尽可能早的结束匹配
        let reg = /\{\{(.+?)\}\}/;
        let value = node.textContent;
        if (reg.test(value)) {
            let key = RegExp.$1.trim();
            node.textContent = value.replace(reg, this.vm[key]);

            // 创建 wather 对象,当数据改变更新视图
            new Watcher(this.vm, key, newValue => {
                node.textContent = newValue;
            });
        }
    }
    // ...
}

处理 v-text 和 v-model 的情况

class Compiler {
    // ...
    update(node, key, attrName) {
        let updateFn = this[attrName+'Updater'];
        updateFn && updateFn.call(this, node, this.vm[key], key);
    }
    // 处理 v-text 指令
    textUpdater(node, value, key) {
        node.textContent = value;

        new Watcher(this.vm, key, newValue => {
            node.textContent = newValue;
        });
    }
    // 处理 v-model 指令
    modelUpdater(node, value, key) {
        node.value = value;

        new Watcher(this.vm, key, newValue => {
            node.value = newValue;
        });
    }
}

视图影响数据

class Compiler {
    // ...
    // 处理 v-model 指令
    modelUpdater(node, value, key) {
        node.value = value;

        new Watcher(this.vm, key, newValue => {
            node.value = newValue;
        });

        // 视图影响数据
        node.addEventListener('input', () => {
            this.vm[key] = node.value;
        });
    }
    // ...
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

当铺鬼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值