vue2简易实现(响应式数据原理)

// 需了解
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
// configurable当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
// enumerable当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
// value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
// writable当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (en-US)改变。
// get属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
// set属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

// vue2基本原理
// 1. new Vue()初始化
// 2.1 data属性 -> Observer劫持data对象(监听所有danta属性),遍历所有属性,使用Object.defineProperty把这些属性转为getter、setter,并监听data对象属性的变化
// 2.2 el模板 -> Compiler解析模板
// 3.1 Dep管理订阅数据对象,数据变化时通知所有订阅数据对象
// 3.2 view -> 解析模板后初始化视图
// 4. Watcher -> 添加订阅数据对象,Dep通知变化notice(),updata()更新视图,Compiler订阅数据,绑定数据更新方法


class Vue {
    constructor(options = {}) {
        // 1.保存数据
        this.$options = options;
        this.$data = typeof options.data === 'function' ? options.data() : options.data;
        this.$el = options.el;

        // 2.将this.$data中的数据加入到响应式系统中,监听data所有属性
        new Observer(this.$data);

        // 3.用this代理this.$data中的数据
        this._proxy();

        // 4.解析el模板
        new Compiler(this.$el, this.$data);
    }
    _proxy() {
        // 代理,把data的属性一个一个添加Vue中
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                configurable: true,
                enumerable: true,
                get() {
                    // 获取this.xx的值 --> 获取this.$data.xx的值
                    return this.$data[key];
                },
                set(newValue) {
                    // 设置this.xx=val --> 设置this.$data.xx=val
                    this.$data[key] = newValue;
                }
            })
        })
    }
}
// 响应式系统
class Observer {
    constructor(data) {
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(data, key, value) {
        // 为每个vm.$data中的属性创建一个依赖对象dep,用于管理vm.$el模板中使用了该属性的订阅对象
        // 1.将使用该属性的所有订阅对象添加到依赖对象dep中的订阅对象数组中
        // 2.该属性的值改变,将通知订阅对象数组中的所有成员更新数据
        // 监听子属性
        const dep = new Dep();
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get() {
                // 添加订阅对象
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue) {
                if (value === newValue) return;
                value = newValue;
                // 数据发生改变,通知订阅对象更新数据
                dep.notify();
            }
        })
    }
}
// 依赖
class Dep {
    constructor() {
        // 订阅对象数组
        this.subs = [];
    }
    addSub(sub) {
        // 添加订阅者
        this.subs.push(sub);
    }
    notify() {
        // 遍历订阅对象,更新视图
        this.subs.forEach(sub => {
            sub.update();
        })
    }
}
// 订阅
// 首先设定whacher为订阅者,要想往dep里添加订阅者,由于在defineReactive方法里操作,需要使用闭包
class Watcher {
    constructor(node, name, data) {
        //node节点
        this.node = node;
        //node节点使用vm.$data中的属性名称
        this.name = name;
        this.data = data;
        //临时保存订阅对象
        //Dep.target指向Watcher本身
        Dep.target = this;
        // 更新视图,并将订阅对象添加到订阅对象数组中
        this.update();
        Dep.target = null;
    }

    // 更新视图
    update() {
        if (this.node.nodeType === 1) { //标签节点
            this.node.value = this.data[this.name];
        } else if (this.node.nodeType === 3) { //文本节点
            this.node.nodeValue = this.data[this.name];
        }
    }
}

const reg = new RegExp(/\{\{(.+)\}\}/);
// 解析模板
class Compiler {
    constructor(el, data) {
        // 获取this.el模板元素节点
        this.el = document.querySelector(el);
        // data数据
        this.data = data;
        // 创建虚拟dom节点,并将模板解析后的内容插入到虚拟dom节点内
        // 使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。
        const frag = this.createFragment();
        // 将虚拟dom节点内容插入到this.el节点内,插入的是虚拟dom节点的子孙节点,而虚拟dom节点自身不会插入
        // 注:模板解析完后,this.el节点内为空
        this.el.appendChild(frag);
    }
    createFragment() {
        let child;
        // 创建虚拟dom节点
        let frag = document.createDocumentFragment();
        // 遍历this.el子节点
        // 获取第一个节点
        while (child = this.el.firstChild) {
            // 解析this.el子节点child
            this._compiler(child);
            // 将解析后的child节点插入到frag虚拟dom节点内,child节点会从this.el中移除,
            // this.el.firstChild就变成了child的下一个兄弟节点,直到this.el中没有子节点,跳出循环
            frag.appendChild(child);
        }
        return frag;
    }

    // 解析节点
    _compiler(node) {
        // 标签节点
        if (node.nodeType === 1) {
            const attrs = node.attributes;
            if (attrs.hasOwnProperty('v-model')) {
                const name = attrs['v-model'].nodeValue;
                // v-model双向数据绑定
                // 创建订阅对象,并初始化视图
                new Watcher(node, name, this.data);
                // 给节点绑定input事件
                node.addEventListener('input', (e) => {
                    this.data[name] = e.target.value;
                })
            }
            // 遍历标签节点的子节点
            node.childNodes.forEach(item => {
                // 递归解析子节点
                this._compiler(item);
            })
        }
        // 文本节点
        else if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                const name = RegExp.$1.trim();
                // 创建订阅对象,并初始化视图
                new Watcher(node, name, this.data);
            }
        }
    }
}


<!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>YY</title>
</head>

<body>
    <div id="app">
        <h3>注:代码是简单实现,因此一对标签内只能解析一个数据</h3>
        <p>{{ num }}</p>
        <input type="submit" onclick="btn()" value="添加">
        <p>{{ message }}</p>
        <input type="text" v-model="message">
    </div>
    <script src="./demo-vue2.js"></script>
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                message: '鱼鱼想上岸',
                num: 1
            }
        })
        function btn() {
            vm.num++
        }
    </script>
</body>

</html>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值