美文网首页
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 双向数据绑定 原理详解

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