Vue双向绑定实现

原理就不说明了,直接贴代码,加了很多注释


index.html

<!--

    JS 有两种方法可以侦测到对象的变化:Object.defineProperty 和 Proxy

    Vue 2.x 用的是 Object.defineProperty,3.x 用的是 Proxy

    --------------------------------

    Vue 双向绑定是通过 “数据劫持” + “发布订阅模式” 实现的

    --------------------------------

    这里实现的功能有:

    input 和 textarea 标签的 v-model
    
    文本的 {{ }} 数据绑定

-->

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Vue Study Log</title>
    </head>
    <body>
        <div id="app">
            <input v-model="message">
            <div>
                <p>{{ message }}</p>
            </div>
            <input v-model="message">
            <div>
                <br>
                <textarea v-model="message"></textarea>
            </div>
            {{ message }}
            <div>
                <p>{{ number }}</p>
                <input v-model="number">
            </div>
            <div>
                <div>
                    <p>{{ number }}</p>
                </div>
            </div>
        </div>

        <script src="./observer.js"></script>
        <script src="./watcher.js"></script>
        <script src="./compile.js"></script>
        <script src="./vue.js"></script>
        <script>
            var app = new Vue({
                el: '#app',
                data: {
                    message: '我厉害吗',
                    number: 1234567
                }
            });
        </script>
    </body>
</html>

 observer.js


// 使一个属性变为可观察的
function defineReactive(obj, key, val) {
    // 收集 watcher 的容器
    let dep = new Dep();
    // 在这里,一个 defineProperty 会给 obj 添加三个属性
    // 比如 message、set message、get message
    Object.defineProperty(obj, key, {
        get: function () {
            dep.depend();
            return val;
        },
        set: function (newVal) {
            val = newVal;
            dep.notify();
        }
    });
}

// 使一个对象的每一属性都变为可侦测的
function observable(obj, vm) {
    Object.keys(obj).forEach(key => {
        defineReactive(vm, key, obj[key]);
    });
}

//----------------------------------------------

// 订阅器(依赖收集) Dep
class Dep {
    constructor() {
        this.subs = [];
    }
    // 添加订阅者
    depend() {
        if (Dep.target) {
            this.subs.push(Dep.target);
        }
    }
    // 通知订阅者更新
    notify() {
       this.subs.forEach(sub => {
            sub.update();
       });
    }
}

 watcher.js


// 订阅者 Watcher
class Watcher {
    constructor(vm, node, name) {
        this.vm = vm; // vue 实例
        this.node = node; // 监听绑定的节点,比如监听的是 {{ }} 文本或者是 input 标签的值等
        this.name = name; // 绑定的数据名称,v-model="name" 的 name
        
        // 把这个 watcher 添加进 dep
        Dep.target = this;
        this.vm[this.name]; // 会自动调用绑定好的 get 方法
        Dep.target = undefined;
    }

    // 当数据更新时 dep 会通知所有 watcher 执行这个 update 函数
    update() {
        // 如果是元素节点
        if (this.node.nodeType === 1) {
            this.node.value = this.vm[this.name];
        }
        // 如果是文本节点
        else if (this.node.nodeType === 3){
            this.node.nodeValue = this.vm[this.name];
        }
    }
}

 compile.js


// 将目标节点下的子节点都添加进 fragment
function nodeToFragment(node, vm) {
    // 创建 fragment(轻量级的 document 对象)
    let fragment = document.createDocumentFragment();
    let child = node.firstChild;
    // 遍历根节点下的所有一级节点
    while (child) {
        compile(child, vm);
        fragment.appendChild(child);
        child = node.firstChild;
    }
    return fragment;
}

// 解析节点
function compile(node, vm) {
    // 如果是元素节点,比如 input、div、p 标签等
    if (node.nodeType === 1) {
        compileElement(node, vm);
    }
    // 如果元素是文本节点(比如"<div>{{ xxx }}</div> 的 {{ xxx }}")
    else if (node.nodeType === 3) {
        compileText(node, vm);
    }
}

// 解析元素节点
function compileElement(node, vm) {
    // 如果是 input 标签或者 textarea 标签
    if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
        // 遍历这个元素的所有属性
        for (let attr of node.attributes) {
            // 如果这个属性是 v-model
            if (attr.nodeName === 'v-model') {
                // 检测绑定的数据名称,即 v-model="name" 的 name
                let name = attr.nodeValue;
                // 给这个 input 或 textarea 添加事件
                node.addEventListener('input', e => {
                    // 更改 vue 实例上的数据为这个 input 里的数据
                    vm[name] = e.target.value;
                });
                // 初始化此节点的值
                node.value = vm[name];
                // 移除 v-model 这个属性
                node.removeAttribute('v-model');
                // 给这个节点绑定一个订阅者,否则当别的 input 更新时收不到数据
                new Watcher(vm, node, name);
            }
        }
    }
    // 如果是其他标签,比如 div、p 等
    else {
        // 递归解析他们的子节点
        for (let child of node.childNodes) {
            compile(child, vm);
        }
    }
}

// 解析文本节点
function compileText(node, vm) {
    // 来匹配 {{ xxx }} 中的 xxx
    let reg = /\{\{(.*)\}\}/;
    // 如果这个文本里有 {{ xxx }} 这样的文本
    if (reg.test(node.nodeValue)) {
        // 获取匹配到的第一个字符串,比如 “  message”
        let name = RegExp.$1;
        // 去掉字符串的首尾空格,“message”
        name = name.trim();
        // 初始化此节点的值
        node.nodeValue = vm[name];
        // 绑定一个订阅者
        new Watcher(vm, node, name);
    }
}

vue.js


// Vue 构造函数
function Vue(options) {
    // 使 data 里的属性都变为可侦测的
    observable(options.data, this);
    // 得到根元素,一般都是 <div id="app"></div>
    let node = document.querySelector(options.el);
    // 将根元素下的节点都移到 fragment 去编译,就是查找 v-model 和 {{ }}
    let fragment = nodeToFragment(node, this);
    // 再将这个 fragment 重新挂到根元素上
    node.appendChild(fragment);
}

代码地址

https://github.com/hu243285237/WebRepository/tree/master/%E4%BB%A3%E7%A0%81%E7%AC%94%E8%AE%B0/vue/Vue%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A%E5%8E%9F%E7%90%86

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值