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

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

<!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

// 一共需要实现:
// 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://www.cnblogs.com/beileixinqing/p/16661753.html 

posted @ 2022-09-06 14:43  蓓蕾心晴  阅读(52)  评论(0编辑  收藏  举报