vue双向数据绑定v2版本

vue双向数据绑定v2版本

代码如下:

html

<div id="app">
  {{age}}
    标题:<p v-html="html">123</p>
    <input type="text" v-model="name">
    <input type="text" v-model="age">
    <input type="text" v-model="obj.b">
    {{name}}
    {{name}}
    <p>我是{{name}}一名前端工程师{{name}}{{name}}</p>
    <p>我是{{obj.a}}一名前端工程师{{obj.b}}{{obj.b}}</p>
    <hr>
    --{{jl}}--
    <button v-on:click="mychange">按钮,点我让age+1</button>
    <ul>
        <li v-for="item in ul"></li>
    </ul>
</div>

js

// 1. 定义Vue基础类
        // 2. 定义编译器类---找到模板中的内容编译为具体数据    把v-model='name'、{{name}}的内容 => data.name => 前端
        // 2.1 创建文档碎片 提高性能
        // 2.2 找到文档碎片中的v-model和{{}}
        // 2.3 处理v-model
        // 2.3 处理{{}}
        // 2.4 定义具体处理的工具函数
        // 3. 定义劫持数据
        // 3.1 定义数据劫持类
        // 3.2 递归保证对象嵌套
        // 3.3 劫持数据 
        // 4. 定义观察者模式
        // 4.1 定义观察者类
        // 4.2 准备一个获取观察的数据的方法
        // 4.2 准备一个更新观察者数据的方法,等待(发布订阅那边)被通知才执行
        // 5. 定义发布订阅
        // 5.1 定义发布订阅模式
        // 5.1 准备一个空数组,专门收集所有观察者
        // 5.2 准备一个添加观察者的方法--订阅
        // 5.3 准备一个发布通知的方法,通知所有订阅过的观察者--发布
        // 6 完成单向数据绑定
        // 6.1 在指令处添加观察者
        // 6.2 在文本渲染处添加观察者
        // 6.3 在首次触动get方法处创建new Dep(),收集所有相关观察者
        // 6.4 只能首次触发收集 Dep.target = this 
        // 7. 完成双向数据绑定
        // 7.1 给v-model所在表单添加监听事件
        // 7.2 获取表单数据,赋值到data中
        // 8. 简化数据、代理数据
        // 8.1 代理 this的属性
        // 9. 处理computed
        // 10. 处理methods
        // 11. 处理v-html


        // 抓住核心:
        // 数据改变--视图改变
        // 一个数据可能同时在影响多个视图(一个name 对应多个{{name}}、多个v-model='name'等)  一对多的关系
        // 如何做到改变一个数据,同时影响到多个视图的改变呢?
        // 发布、订阅,先订阅,再发布
        // 先视图关联渲染的name全部订阅dep、再数据有改变时dep发布
        // 订阅的时候因为数据劫持里面的get并不会触发,只有在触发的时候才会订阅
        // 但订阅的时候可能导致重复订阅(比如:obj.a.b.c在触发complieUitls.getValue时,因为存在reduce,逐级调用获取value,就执行了4次get)
        // 只有在首次才可以加入dep队列,防止后续取值get的时候也重复添加观察者

        class Vue {
            constructor(options) {
                // 把参数定义到实例身上,方便调用
                this.$el = options.el
                this.$data = options.data
                let computed = options.computed
                let methods = options.methods
                if (this.$el) {
                    // 劫持数据
                    new Observer(this.$data)

                    // 处理computed
                    for (let key in computed) {
                        // 有计算属性就会被代理到this.$data上(所以data和computed不能同名)
                        // 一旦computed被读取,就会被劫持返回执行computed函数中的内容(调用computed函数)注意this指向
                        // 在被computed函数执行的时候,又会触发data里面的数据依赖,触发到data的数据劫持
                        // 对该数据形成发布订阅和观察者(之前的Observer),创建渲染节点处创建观察者,添加到dep订阅集合中,在触发set方法的时候,发布已经订阅的观察者更新
                        Object.defineProperty(this.$data, key, {
                            get: () => {
                                return computed[key].call(this)
                            }
                        })
                    }
                    // 处理methods/同理computed
                    for (let key in methods) {
                        Object.defineProperty(this.$data, key, {
                            get() {
                                return methods[key]
                            },
                            enumerable:true
                        })
                    }
                    // 代理数据,让用户少写一层  this.name  而不是this.$data.name
                    // 先把methods、computed绑定到$data中,再代理数据
                    this.porxyVm(this.$data)

                    // 把模板编译成对应数据渲染
                    new Compiler(this, this.$el, this.$data)
                }
            }
            porxyVm(data) {
                for (let key in data) {
                    Object.defineProperty(this, key, {
                        get() {
                            return data[key];
                        },
                        set(newValue) {
                            data[key] = newValue
                        }
                    })
                }
            }
        }
        // 一个属性数据对应一个dep订阅器
        // 一个dep订阅器数组中收集了N个wather观察者
        // 一旦属性数据在set中被触发
        // 就会通知该dep订阅器中的所有观察者
        // 让每一个观察者都触发自己的更新功能,从而做到数据改变影响所有视图
        class Dep {
            constructor() {
                // 用数组专门收集观察者
                this.subs = []
            }
            // 订阅
            addSub(watcher) {
                this.subs.push(watcher)
            }
            // 发布
            notify() {
                console.log(this.subs);
                this.subs.forEach(watcher => watcher.update())
            }
        }
        class Wather {
            constructor(vm, expr, cb) {
                // expr 表达式
                this.vm = vm;
                this.expr = expr;
                this.cb = cb;
                this.oldValue = this.get()
            }
            // new 观察者,但是没有添加到dep订阅器中,所以手动获取一次数据就会自动触发数据劫持中的get
            get() {
                // 核心套路:第一次get的时候,把this(new出来的观察者实例)放到当前Dep类身上,证明是首次(第一次添加完毕之后会置空)
                Dep.target = this
                // 手动读取数据,相当于触发了数据劫持中的get,会把当前this添加到dep队列身上(dep订阅器完成收集观察者的任务)
                let oldValue = complieUitls.getValue(this.expr, this.vm)
                // 置空,防止观察者重复加入队列(更新数据时,又会读取数据劫持的get)
                Dep.target = null
                return oldValue
            }
            update() {
                let newValue = complieUitls.getValue(this.expr, this.vm)
                if (newValue != this.oldValue) {
                    this.cb && this.cb(newValue)
                }
            }
        }
        // 数据劫持
        class Observer {
            constructor(data) {
                this.observer(data)
            }
            observer(data) {
                // 是对象才遍历
                if (data?.constructor === Object) {
                    for (let key in data) {
                        this.defineDeactive(data, key, data[key])
                    }
                }
            }
            defineDeactive(data, key, value) {
                // 防止对象中存在对象,所以调自己一次
                this.observer(value)
                // 每一个属性都会进入一次,创建一个新的dep订阅器的数据集合
                let dep = new Dep()
                Object.defineProperty(data, key, {
                    get() {
                        // 只有第一次在 new 观察者的时候Dep.target才会有值(防止反复添加观察者入dep订阅器队列)
                        if (Dep.target) {
                            dep.addSub(Dep.target)
                        }
                        return value
                    },
                    set: newValue => {
                        if (value != newValue) {
                            // 防止属性被替换为对象,get、set失效,所以重新劫持新对象
                            this.observer(newValue)
                            value = newValue
                            dep.notify()
                        }
                    }
                })
            }

        }
        class Compiler {
            constructor(vm, el, data) {
                this.$vm = vm;
                this.$el = this.isElementNode(el) ? el : document.querySelector(el);
                this.$data = data;
                // 获取文档碎片
                let fragment = this.getFragment();
                // 把文档碎片去编译
                this.complie(fragment)
                // 把编译完的文档碎片放入html
                this.$el.appendChild(fragment)
            }
            // 判断是否为元素节点
            isElementNode(node) {
                return node.nodeType === 1
            }
            // 判断是否为vue指令
            isDirective(attrName) {
                return attrName.startsWith('v-')
            }
            // 获取文档碎片提高性能
            getFragment() {
                let fragment = document.createDocumentFragment();
                let child
                while (child = this.$el.firstChild) {
                    fragment.appendChild(child)
                }
                return fragment
            }
            // 编译指令
            complieElement(node) {
                [...node.attributes].forEach(attr => {
                    // 检测元素身上的所有属性
                    const { name, value: expr } = attr
                    // name 属性名 v-model
                    // expr 属性值 name、age
                    if (this.isDirective(name)) {
                        // 如果是指令(存在v-),继续结构拿到 - 后面的内容
                        const [, directive] = name.split('-')
                        // directive 指令 model、html、for、on 等
                        // 防止出现注册事件(除了指令还要获取click)  v-on:click 要分别拿到on和click
                        let [directiveName, eventName] = directive.split(':')
                        // directiveName 指令 on/if/html/for
                        // eventName 事件类型 click/mouseenter
                        // 分别调用各自的指令
                        if (complieUitls.directive[directiveName]) {
                            complieUitls.directive[directiveName](node, expr, this.$vm, eventName)
                        }
                    }
                })
            }
            // 编译文本内容
            complieText(node) {
                if (/\{\{(.+?)\}\}/.test(node.textContent)) {
                    complieUitls.txt(node, this)
                }
            }
            // 编译
            complie(fragment) {
                [...fragment.childNodes].forEach(node => {
                    if (node.nodeType === 1) {
                        // 如果是元素节点
                        // 检测是否存在指令,有指令就编译 <p v-model="name"></p> 没有就放弃
                        this.complieElement(node)
                        // 防止元素本身有儿子,儿子可能有指令、差值表达式 没有就放弃
                        this.complie(node)
                    }
                    if (node.nodeType === 3) {
                        // 如果是内容节点
                        // 检测是否存在差值表达式,有就编译 {{}},没有就放弃
                        this.complieText(node)
                    }
                })
                return fragment
            }
        }
        const complieUitls = {
            // 获取对象的值(可多层嵌套)  obj.a.b.c = 5 取值
            getValue(objStr, vm) {
                return objStr.split('.').reduce((data, current) => data[current], vm.$data)
            },
            // 设置对象的值(可多层嵌套) obj.a.b.c = 5  赋值(相当于已经更新了数据)
            setValue(objStr, vm, newValue) {
                // newValue 用户操作的最新数据
                objStr.split('.').reduce((data, current, index, arr) => {
                    // 找到最后的obj.a.b.c的时候(根据长度来判断),赋值
                    if (index === arr.length - 1) {
                        data[current] = newValue
                    }
                    return data[current]
                }, vm.$data)
            },
            // 插值表达式内容的替换 {{name}} => vm.$data.name => 前端
            txt(node, vm) {
                // node.textContent = xxx
                let template = node.textContent // 带{{}}的原始模板,后续才用到,这里保存起来
                template.replace(/\{\{(.+?)\}\}/g, (...args) => {
                    new Wather(vm, args[1], () => {
                        // 这里不能直接用新值替换 node.textContent = newValue 会出问题,比如:
                        // 原始 js name=前端 ------ html  我是一名{{name}}{{name}}工程师 => 我是一名前端前端工程师
                        // 错误 js name=java ------ html  我是一名{{name}}{{name}}工程师 => java
                        // 正确 js name=java ------ html  我是一名{{name}}{{name}}工程师 => 我是一名java工程师
                        complieUitls.update.txt(node, template, vm)
                    })
                })
                // 初始化
                complieUitls.update.txt(node, template, vm)
            },
            // 更新
            update: {
                // 更新插值表达式
                txt(node, template, vm) {
                    // template 原始插值表达式模板
                    //{{name}}--{{name}}  前端--前端
                    node.textContent = template.replace(/\{\{(.+?)\}\}/g, (...args) => complieUitls.getValue(args[1], vm))
                },
                // 更新指令内容
                model(node, expr, vm) {
                    // complieUitls.setValue(value,vm)
                    // expr 表达式
                    // node.value = xxx
                    node.value = complieUitls.getValue(expr, vm)
                },
                html(node, expr, vm) {
                    // complieUitls.setValue(value,vm)
                    // expr 表达式
                    // node.innerHTML = xxx
                    node.innerHTML = complieUitls.getValue(expr, vm)
                },
            },
            // 各种v-指令
            directive: {
                model(node, expr, vm) {
                    // 给v-model指令 添加一个观察者,如果有改变,触发后面的回调函数
                    // new Wather(vm,value,newValue => node.value = newValue)
                    // node.value = complieUitls.getValue(node,newValue, vm)
                    // 提取了一个公共方法(具体操作),显得正规一些
                    // 初始化,编译模板中的内容 ---- v-model="name" =>  前端
                    complieUitls.update.model(node, expr, vm)
                    // 添加观察者,观察该位置的指令,一旦发现数据更新后,收到dep的通知,更新视图
                    new Wather(vm, expr, () => complieUitls.update.model(node, expr, vm))
                    // v-model 是双向数据绑定的,所以实时监听事件
                    node.addEventListener('input', function (e) {
                        complieUitls.setValue(expr, vm, e.target.value)
                    })
                },
                on(node, expr, vm, eventName) {
                    //  v-on:click='mychange'
                    //  expr 指令中的表达式  mychange
                    //  eventName 绑定事件的名字 是 click
                    node.addEventListener(eventName, function (e) {
                        vm[expr](e)
                    })
                },
                html(node, expr, vm) {
                    // node.innerHTML = xxx
                    // 添加观察者,让数据改变的时候,v-html也可以改变
                    new Wather(vm, expr, newValue => complieUitls.update.html(node, expr, vm))
                    complieUitls.update.html(node, expr, vm)
                },
                for(node, expr, vm) {
                    const [item, , list] = expr.split(' ')
                    let nodeName = node.nodeName
                    vm.$data[list].forEach(li => {
                        let n = document.createElement(nodeName);
                        n.innerHTML = li
                        node.appendChild(n)
                    })
                }
            }
        }

        let vm = new Vue({
            el: '#app',
            data: {
                name: '前端',
                age: 3,
                obj: {
                    a: 'obj1',
                    b: 'obj2',
                    c: {
                        cc: {
                            ccc: 'obj4'
                        },

                    }
                },
                ul: ['yes', 'ok', 'go', 'ya'],
                html: '<h1>h1</h1>'
            },
            computed: {
                jl() {
                    return '我的简历' + this.$data.name + this.$data.age
                }
            },
            methods: {
                mychange(e) {
                    // console.log('e',e);
                    this.$data.age += 1
                }
            }
        })
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值