前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现一个简单版 Vue2 双向数据绑定

实现一个简单版 Vue2 双向数据绑定

作者头像
蓓蕾心晴
发布2022-09-24 14:54:57
1750
发布2022-09-24 14:54:57
举报
文章被收录于专栏:前端小叙前端小叙

实现一个简单版本 Vue,仅实现了 数据响应式、依赖收集、compile编译中的html和文本编译,起名为nvue,即新 vue。

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="app">
            <p n-text="counter"></p>
            <p n-html="desc"></p>
            <div>{{desc}}</div>
        </div>
        <script src="./nvue.js"></script>
        <script>
            const app = new NVue({
                el: "#app",
                data: {
                    counter: 1,
                    desc: '<p>这是一个新 vue<span style="color:red">demo</span></p>',
                    name: "xiaohong",
                },
                methods: {
                    add() {
                        this.counter++;
                    },
                },
            });
            setInterval(() => {
                app.counter++;
            }, 2000);
        </script>
    </body>
</html>

nvue.js

代码语言:javascript
复制
// 一共需要实现:
// 1. Vue 类
// 2. Dep 类
// 3. Watcher 类
// 4. Compile 类 编译类
// 5. observer 函数
// 6. proxy 函数
// 7. reactive函数
// 一个对象代表一个 Observer
// 一个 key 代表 dep 实例
// 一个对象 key 的使用代表一个 watcher

// 实现数据响应式
function defineReactive(obj, key, val) {
    // key如果是对象,则需要递归绑定响应式
    observer(val);
    // 一个 key 代表一个 dep,这里是给每一个 key 做响应式,所以创建一个 dep 实例对象
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get() {
            console.log("get", key);
            // get 获取值,则创建 dep,传入的为 watcher,即 Dep.target
            Dep.target && dep.addDep(Dep.target);
            return val;
        },
        set(v) {
            if (v != val) {
                // 可能设置的还是为对象,需要递归传入做响应式
                observer(v);
                // set 修改值,则通知 dep 修改
                val = v;
                console.log("set", key, v);
                // 通知 dep 更新
                dep.notify();
            }
        },
    });
}

function observer(data) {
    // 保证仅对对象做响应式
    if (typeof data !== "object" || !data) {
        return data;
    }
    Object.keys(data).forEach((key) => {
        defineReactive(data, key, data[key]);
    });
}
// 给 vm 实例设置代理,可以通过 vue 实例直接获取到 data 对象属性
function proxy(vm) {
    // 因为仅对 vm.$data 上的 key 做拦截,所以需要遍历 vm.$data 的 keys
    Object.keys(vm.$data).forEach((key) => {
        // 拦截 vm
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key];
            },
            set(v) {
                if (v !== vm.$data[key]) {
                    vm.$data[key] = v;
                }
            },
        });
    });
}

class NVue {
    // 传入 el,及配置对象
    constructor(options) {
        this.el = options.el;
        this.$options = options; // 保存当前配置对象
        this.$data = options.data; // 保存 当前data
        this.$vm = this; // 保存当前 vue 实例
        // 创建 Dep 的实例
        // this.dep = new Dep();
        // Dep.target && this.dep.addDep(this)
        // 1.将所有 data 对象 变为响应式
        observer(this.$data);
        proxy(this);
        // 2.为 当前 vue 实例 this 增加代理,让用户访问的 data 可以直接从vue 实例上获取,而不是必须从 this.$data上获取
        // 3.data对象已变为响应式, 一切准备就绪,进行模版编译
        new Compile(this.el, this.$vm);
    }
}

class Compile {
    constructor(el, vm) {
        this.el = el;
        this.vm = vm;
        this.compile(document.querySelector(el));
    }
    // 编译函数
    compile(el) {
        // 传入的节点一定有 childNodes 子节点,对子节点进行遍历
        el.childNodes.forEach((node) => {
            // 元素
            if (node.nodeType === 1) {
                // 如果 nodeType=1 则为元素节点
                this.compileElements(node);
                // 如果 node 节点有 子节点
                if (node.childNodes.length > 0) {
                    // 递归编译
                    this.compile(node);
                }
            } else if (this.isInterText(node)) {
                // 如果 nodeTyppe=3 则为文本节点
                this.compileText(node);
            }
        });
    }
    isInterText(node) {
        // 判断插值文本,还要通过正则表达式验证 {{}} 格式
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
    // 编译元素节点
    compileElements(node) {
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach((attr) => {
            const key = attr.name; // 获取属性 name
            const exp = attr.value; // 获取属性值
            // 如果属性 key 是以 n-开头,说明为指令表达式
            if (key.startsWith("n-")) {
                // 获取指令
                const dir = key.substring("2");
                this[dir] && this[dir](node, exp);
            }
        });
    }
    // 编译文本节点
    compileText(node) {
        // 注意:RegExp.$1,这里是临时取法,如果有别的正则不可以直接这样获取
        this.update(node, RegExp.$1, "text");
    }
    // 统一更新函数,在这里创建 watcher
    update(node, exp, dir) {
        const fn = this[dir + "Updater"];
        console.log("update", this.vm[exp]);
        fn && fn(node, this.vm[exp]);
        new Watcher(this.vm, exp, (val) => {
            fn && fn(node, val);
        });
    }
    // 文本更新函数
    textUpdater(node, val) {
        node.textContent = val;
    }
    // html 更新函数
    htmlUpdater(node, val) {
        node.innerHTML = val;
    }
    // 文本
    text(node, exp) {
        this.update(node, exp, "text");
    }
    // html
    html(node, exp) {
        this.update(node, exp, "html");
    }
}

// 依赖类,一个 watcher 实例代表一个依赖
class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm;
        // 通过 key 和 vm 来获取最新的 value 值
        this.key = key;
        // 传入 updateFn 更新函数,需要再依赖被更新的时候调用,并传入最新的 value 值
        this.updateFn = updateFn;
        // 全局变量设为 Dep.target
        Dep.target = this;
        // 获取 vm[key],触发  对象 key get 方法,进行依赖收集
        this.vm[key];
        // 依赖收集后将 Dep.target 置空
        Dep.target = null;
    }
    update() {
        this.updateFn.call(this.vm, this.vm[this.key]);
    }
}
// 依赖收集类
class Dep {
    constructor() {
        // 定义依赖数组,存放依赖,每个依赖就是一个 watcher
        this.deps = [];
    }
    addDep(watcher) {
        // 依赖收集,即 watcher 收集
        this.deps.push(watcher);
    }
    notify() {
        // 触发依赖更新,将依赖中所有的 watcher 的 update 方法都遍历一遍
        this.deps.forEach((dep) => {
            dep.update();
        });
    }
}

本代码参考自前端杨村长。

转载请注明出处:https://cloud.tencent.com/developer/article/2121029

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档