美文网首页
Vue 双向数据绑定 原理详解

Vue 双向数据绑定 原理详解

作者: Upcccz | 来源:发表于2019-05-08 00:42 被阅读0次
示意图
<!--  index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <div id="app">
        <input type="text" v-model="message" />
        {{ message }}
        <p>
           姓名: <span>{{ obj.name }}</span>
           年龄: <span>{{ obj.age }}</span>
        </p>
    </div>
    <script src="Watcher.js"></script>
    <script src="Observer.js"></script>
    <script src="Compiler.js"></script>
    <script src="Vue.js"> </script>
    <script>
        let vm = new Vue({
            el:'#app',
            data:{
                message: 'hello wrold',
                obj: {
                    name:'jack',
                    age:19
                }
            }
        })
    </script>
</body>
</html>
// Vue.js
class Vue {
    constructor(options) {
        // 将数据缓存在vue实例属性上,方便实例中的函数能够使用this.xx访问到
        this.$el = options.el;
        this.$data = options.data;
        if (this.$el) {
            // 数据劫持 监听data这个对象 监听其中的属性 映射为get 和 set 
            new Observer(this.$data)

            // 代理数据 this.$data.message => this.message
            this.proxyData(this.$data)

            // 如果$el存在 就进行编译 (编译需要数据和元素)
            // 这里第二个参数把this传过去 那边要什么直接通过this取。
            new Compiler(this.$el, this);
        }
    }
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this,key,{
                get() { // this.message => this.$data.message
                    return data[key]
                },
                set(newVal) {
                    // this.$data.message = '新值'
                    data[key] = newVal;
                }
            })
        })
    }
}
// Compiler.js
class Compiler {
    constructor(el, vm) {
        // vm是传过来的vue实例 初始化数据 方便函数中调用

        // 如果el是一个元素直接赋值给this.el 如果是一个字符串'#app' 则使用dom方法自己去取。
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // 这里的vm就是 Vue.js中的this  拥有 $el 和 $data 属性的。
        this.vm = vm;
        if (this.el) {
            // 如果这个元素存在(使用dom方法能够获取到)则开始编译。

            // 1.先把真实的dom即this.el 存在fragment中 (文档节点,可以使用dom的方法,但是不会影响页面)
            let fragment = this.node2fragment(this.el);
            // 此时的fragment 就相当于是el的副本 只不过不存在于真实dom中 存在内存中 不用担心过多操作影响性能。

            // 2.编译fragment : 就是从中提取 插值表达式 {{}} 和 v-xx指令 换成数据
            this.compile(fragment)
            // 3.将编译好之后的fragment塞到页面中区,替换#app那个div 
            // 经过第二步 fragment已经编译好了 将他塞回页面
            this.el.appendChild(fragment)
        }
    }
    
    // 定义一个方法判断传进来的node是否是一个元素
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 定义一个方法判断是不是一个指令
    isDirective(name) {
        return name.includes('v-')
    }

    // 将真实dom取出存在fragment中
    node2fragment(el) {
        // 创建一个文档碎片 
        let fragment = document.createDocumentFragment();

        let firstChild;
        // 定义个变量firstChild,每次都将元素的第一个节点赋值给firstChild
        // 当调用fragment.appendChild()时,el中的第一个节点就会从el中**移除**然后添加到文档碎片中
        // 当el所有的节点都移除是,el.ffirstChild就是null 那么firstChild也就是null 循环结束
        // 这样就所有的节点中el中移入到了fragment中 然后将文档碎片返回。
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }

    // 就是从中提取 插值表达式 {{}} 和 v-xx指令 换成数据 
    compile(fragment) {
        // 先获取所有的子节点
        let childNodes = fragment.childNodes;

        // 遍历节点集合,针对性编译
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) { // 如果是元素节点
                // 编译元素
                this.compileElement(node)
                // 并且如果是元素节点则还需要递归 目的是为了拿到所有的插值表达式及指令
                this.compile(node)

            } else { // 文本节点
                // 编译文本
                this.complieText(node)
            }
        })
    }

    // 编译元素 => 取出指令即元素的v-xx 属性。
    compileElement(node) {
        // 取出元素身上的所有属性 
        let attrs = node.attributes;
        // 编译属性 找到v-xx attr.name拿到属性名 attr.value拿到属性值
        Array.from(attrs).forEach(attr => {
            if (this.isDirective(attr.name)) {
                //  如果是一个指令 取到对应指令的值渲染成数据放到节点中
                //  需要 属性值、数据、节点
                let val = attr.value; // val == message 表达式
                // 解构赋值 v-model 取到model = type
                let [,type] = attr.name.split('-');
                //  通过vm就能取到实例上的data
                CompileUtil[type](val, this.vm, node)
            }
        });
    }

    // 编译文本 => 取出插值表达式
    complieText(node) {
        // 取出节点中的文本
        let txt = node.textContent; 

        // 定义正则取出表达式
        let reg = /\{\{([^}]+)\}\}/g;

        if (reg.test(txt)) { 
            // 如果为true则说明有插值表达式
            // 取出表达式 渲染成数据 插到节点中
            //  需要 表达式、数据、节点

            //  通过vm就能取到实例上的data  将整个文本传过去 在函数里面进行表达式的抽取
            CompileUtil['text'](txt, this.vm, node)
        }
    }
}
//  定义一个专门用来编译的工具
CompileUtil = {
    // 定义一个方法 从vm.$data中取值 
    getVal(vm, expr) {
        // message.a.b 转换成 vm.$data.message.a.b
        // vm.$data[message.a.b] 很明显是错误的写法 取不到 所以需要借助reduce
        expr = expr.split('.');
        return expr.reduce((prev,next)=>{
            return prev[next]
        },vm.$data)
    },
    getTxtVal(vm, expr) {
        return expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
            // 这里的arg[1] 就是 message / message.a
            return this.getVal(vm, arg[1].trim())
        })
    },
    setVal(vm,expr,newVal) {
        expr = expr.split('.')
        return expr.reduce((prev,next,cIndex)=>{
            if (cIndex === expr.length-1) {
                // 循坏到最后的时候 message => message.a => message.a.b 赋新值
                return prev[next] = newVal;
            }
            return prev[next]
        },vm.$data)
    },
    text(expr, vm, node) { // 插值文本处理
        // 取出更新函数
        let updateFn = this.updater['txtUpdater'];

        // 更新
        // 通过正则取出真正的表达式 {{ message.a }} == vm.$data.message.a
        // 此时的value就是 vm.$data.message / vm.$data.message.a
        let value = this.getTxtVal(vm, expr)

        // 这里应该加一个监控 数据变化了 重新编译模板 **数据=>视图**
        expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
            // 这里的arg[1] 就是 message / message.a
            new Watcher(vm,arg[1].trim(),()=>{
                // 如果数据变化了 文本节点需要重新获取新的数据 然后更新dom
                // 回调会在Watcher.update()调用时执行 , 什么时候会调用呢
                // 数据更新的时候 应该调用 就是在劫持数据映射set哪里
                updateFn && updateFn(node, this.getTxtVal(vm,expr))
            })
        })

        updateFn && updateFn(node, value)
    },
    model(expr, vm, node) { // v-model处理
        // 取出更新函数
        let updateFn = this.updater['modelUpdater'];

        // 这里应该加一个监控 数据变化了 重新编译模板 **数据=>视图**
        new Watcher(vm, expr, (newVal)=>{
            // 回调会在Watcher.update()调用时执行 , 什么时候会调用呢
            // 数据更新的时候 应该调用 就是在劫持数据映射set那里
            updateFn && updateFn(node, newVal)
        })

        // 更新 : vm.$data[expr] == vm.$data.message即数据
        // updateFn && updateFn(node, vm.$data[expr])
        // 因为这个expr 很可能是 message.a.b 所以需要在定义一个专门取值的函数
        updateFn && updateFn(node, this.getVal(vm, expr))

        // 处理 v-model时 监听node的input事件 **视图改变=>数据更新**
        node.addEventListener('input',(e)=>{
            // 因为可能v-model 绑定的是一个深层属性 所以同样要去reduce 修改 最深层属性的值
            this.setVal(vm, expr, e.target.value)
        })
        
    },
    updater: {
        txtUpdater(node, value) { // 编译更新插值表达式
            // 传入一个节点 一个新值,在fragment中更新这个新值 最后渲染到页面上去
            // 作用:即可以初始时将 message 替换成 'hellowrold' 也可以将新的message 替换 旧的message  
            node.textContent = value;
        },
        modelUpdater(node, value) { // 编译更新v-model
            // v-model 即绑定的是表单元素的value属性
            // 传入一个节点(表单元素) 一个新值,更新表单元素的value
            node.value = value;
        }
    }
}
// Observer.js
// 劫持数据
class Observer {
    constructor(data) {
        // 监听数据
        this.observer(data)
    }
    observer(data) {
        // 将data原有的属性改成get和set
        if (!data || typeof data !== 'object') {
            // 如果数据不存在 或者不是一个对象 则直接return 不监听
            return; 
        }

        // 将数据一一劫持
        Object.keys(data).forEach(key => {
            // 定义一个劫持数据的方法
            // data message 'hello wrold'
            this.defineReactive(data, key, data[key])

            // 递归进行深度劫持 因为data[key]有可能一个对象
            // 如果不是对象 则在上面的判断就会直接return 不会走到这里进行深度劫持了
            this.observer(data[key])
        })
    }
    defineReactive(obj,key,val) {
        let that = this;
        let dep = new Dep();
        // 每一个变化的数据 都对应一个Dep 这里面的subs数组 存放着所有更新的操作
        Object.defineProperty(obj,key,{
            enumerable: true,
            configurable: true,
            get(){
                // 每次编译更新模板 new Watcher的时候  就会把watcher实例赋值给Dep.target
                // 然后调用addSub订阅  
                Dep.target && dep.addSub(Dep.target)
                return val
            },
            set(newVal){
                if (newVal !== val) {
                    // 这个新值可能是一个对象 就需要再次劫持
                    // vm.$data.message是'helloword' => vm.$data.message = {a:1}
                    // 不需要去判断这个值到底是不是一个对象 因为在observer中如果不是对象 就直接return了
                    that.observer(newVal)

                    // 把新值赋值给val 则在取的时候 调用get返回的就是新值
                    val = newVal;
                    // 数据变化的时候 调用更新操作 在编译阶段new Watcher中的回调就会被执行。 编译新的dom
                    dep.notify();
                }
            }
        })
    }
}

// 发布-订阅
class Dep {
    constructor(){
        // 订阅的数组
        this.subs= [];
    }
    addSub(watcher) { // 订阅
        this.subs.push(watcher)
    }
    notify() { // 发布
        this.subs.forEach(watcher=>{
            watcher.update();
        })
    }
}
// Watcher.js
//  增加一个观察者,来监听数据的改变,然后更新数据
// 给需要变化的元素增加一个观察者,当数据变化之后 执行对应的方法 更新dom 或者 数据

// 数据 => 视图
// <input type="text" v-model="message" />
// message:'hello wrold' => message: '123'
// input.value = 123

// 视图 => 数据
// 当用户在input标签中进行输入的时候
// 监听oninput事件 将vm.$data.message = e.target.value 

// 双向数据绑定 

// 接受三个参数 vm实例 / expr表达式= message / cb 回调 当值发生变化之后执行的回调 
class Watcher {
    constructor(vm,expr,cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;

        // 缓存老的值 就是初始时的值
        this.oldVal = this.get();
    }

    //  复用一下complier.js中获取值的方法
    getVal(vm, expr) {
        // message.a.b 转换成 vm.$data.message.a.b
        // vm.$data[message.a.b] 很明显是错误的写法 取不到 所以需要借助reduce
        expr = expr.split('.');
        return expr.reduce((prev,next)=>{
            return prev[next]
        },vm.$data)
    }

    get() {
        // 缓存初始值的时候 将这个watch实例赋值给Dep.target
        Dep.target = this;

        // getVal 去取的时候就会调用 数据劫持中的get方法
        let value = this.getVal(this.vm, this.expr);
        
        // 取值的时候即上面getVal的时候 将watcher传过去 取完之后 加Dep.target置空
        // 因为别的watch在调用的时候 
        Dep.target = null;
        return value
    }

    // 对外暴露方法,更新数据
    update() {
        // 拿到新值 调用这个方法时 重新去获取的值就是新值。 数据变化了
        let newVal = this.getVal(this.vm, this.expr);
        // 拿到老值
        let oldVal = this.oldVal;
        if(newVal !== oldVal) {
            // 执行回调,传递新值过去
            this.cb(newVal)
        }
    }
}

相关文章

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • vue 双向数据绑定

    Vue实现数据双向绑定的原理:Object.defineProperty()vue实现数据双向绑定主要是:采用数据...

  • 前端理论面试--VUE

    vue双向绑定的原理(详细链接) VUE实现双向数据绑定的原理就是利用了 Object.definePropert...

  • Vue实现数据双向绑定的原理

    Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数...

  • 【转】JavaScript的观察者模式(Vue双向绑定原理)

    关于Vue实现数据双向绑定的原理,请点击:Vue实现数据双向绑定的原理原文链接:JavaScript设计模式之观察...

  • vue面试知识点

    vue 数据双向绑定原理 vue实现数据双向绑定原理主要是:采用数据劫持结合发布订阅设计模式的方式,通过对data...

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 前端面试题:VUE

    1. vue的双向数据绑定实现原理? 2. vue如何在组件之间进行传值? 3. vuex和vue的双向数据绑定...

  • vue

    1、vue的双向数据绑定实现原理 2、vue如何在组件之间进行传值 3、vuex和vue的双向数据绑定有什么冲突 ...

网友评论

      本文标题:Vue 双向数据绑定 原理详解

      本文链接:https://www.haomeiwen.com/subject/hvvdwqtx.html