Vue.js数据双向绑定原理与实现

13 篇文章 0 订阅

参考视频链接
1、html
引入自己写的vue.js

<body>
    <div id="app">
        <span>阿婆主:{{name}}</span>
        <input type="text" v-model="name">
        <span>更多:{{more.like}}</span>
        <input type="text" v-model="more.like">
    </div>
    <script src="./vue.js"></script>
    <script>
        const vm = new Vue(
            {
            el:'#app',
            data:{
                name:'技术蛋老师',
                more:{
                    like:'一键三连'
                    }
                }
            }
        );
        // console.log('vue实例:',vm);
    </script>
</body>

2、vue.js
2.1

class Vue{
    //实例
    constructor(obj){
        this.$data = obj.data;
        //Observer函数进行数据劫持   监听实例里的数据
        Observer(this.$data)
        //vue实例挂载的元素   vue实例
        Compile(obj.el,this)
    }
}

2.2数据劫持
取实例监听的属性值时,触发getter。
触发getter 时添加订阅者到订阅者数组中。
在setter通知每一个订阅者更新dom。

//数据劫持
function Observer(data_instance){
    //递归出口  结束条件
    if(!data_instance ||typeof data_instance !=='object'){
        return
    }
    //创建后     在getter中 添加订阅者
    const dep = new Dep();
    //以数组的方式 获取对象的key 
    Object.keys(data_instance).forEach(key =>{
        let value = data_instance[key]
        //属性值可能是对象  对子属性也要数据劫持
        Observer(value)
        //数据代理
        Object.defineProperty(data_instance,key,{
            enumerable:true,//可枚举
            configurable:true,//可修改
            get(){
                console.log(`get访问了属性:${key}-->值为:${value}`);
                //temp 不为空时执行 '与操作'     
                Dep.temp&&dep.addSub(Dep.temp)
                return value
            },
            set(newValue){
                console.log(`set属性:${key}修改为-->为${newValue}`);
                value = newValue;
                //新赋值的属性也要
                Observer(value)
                dep.notify()
            }
        })
    })
}

2.3模板解析
模板解析时,把节点值 替换内容后,需要告诉订阅者要更新自己:创建实例。
在update中获取最新值 传给回调函数。

//HTML模板解析  替换DOM内
//vue实例挂载的元素   Vue实例
function Compile(element,vm){
    vm.$el = document.querySelector(element)
    //创建文档碎片  提高dom性能
    const fragment = document.createDocumentFragment();
    let child;
    //while中child = vm.$el.firstChild  再判断child是否为空
    while(child = vm.$el.firstChild){
        fragment.append(child) //节点添加到文档碎片后 这时页面没有数据了
    }
    fragment_compile(fragment)
    //修改fragment里的内容
    function fragment_compile(node){
        //    \s :匹配空格    *:0个或多个  (\S+): 至少一个字符
        //{{ xxx }}
        const pattern = /\{\{\s*(\S+)\s*\}\}/; 

        if(node.nodeType=== 3){// 文本节点
            const value = node.nodeValue;
            console.log(value);// 阿婆主:{{name}}      更多:{{more.like}}
            //exec()  返回数组形式  
            const res_regex = pattern.exec(node.nodeValue);
            if(res_regex){
            //(2) ['{{name}}', 'name', index: 4, input: '阿婆主:{{name}}', groups: undefined]
            //(2) ['{{more.like}}', 'more.like', index: 3, input: '更多:{{more.like}}', groups: undefined]
                // console.log(res_regex);
                const val = res_regex[1].split('.').reduce((total,current)=> total[current],vm.$data);
                //这里替换了  插值表达式 value  的内容
                //如由' 阿婆主:{{ name}}'  变为 '阿婆主:技术蛋老师'
                node.nodeValue = value.replace(pattern,val)
                // 继续遍历节点  节点里面可能还会有节点   递归

                //把节点值 替换内容后,需要告诉订阅者要更新自己。
                //创建订阅者
                //回调函数 在update中执行时  传来参数
                new Watcher(vm,res_regex[1],(newValue)=>{
                    //node.nodeValue = ' 阿婆主:{{ name}}' 插值表达式
                    //在回调时已经是替换过的内容 '阿婆主:技术蛋老师'  了,不是我们要的结果
                    //所以需要在最上面暂存const value = node.nodeValue;
                    node.nodeValue = value.replace(pattern,newValue)
                })

            }
            //  递归出口  终止
            return  
        }

        if(node.nodeType===1 &&node.nodeName==='INPUT'){
            const attr = Array.from(node.attributes);
            attr.forEach(i=>{
                if(i.nodeName==='v-model'){
                    const value = i.nodeValue.split('.').reduce(
                        (total,current)=>total[current],vm.$data
                    )
                    node.value = value;
  
                    //数据改变视图
                    //数据更新后 订阅者更新自己 
                    //回调函数 在update中执行时  传来参数
                    new Watcher(vm,i.nodeValue,(newValue)=>{
                        node.value = newValue;
                    });

                    //视图改变数据
                    node.addEventListener('input',(e)=>{
                        const arr1 = i.nodeValue.split('.');// ['more','like']
                        const arr2 = arr1.slice(0,arr1.length - 1);//['more']
                        //vm.$data.more
                        const final = arr2.reduce((total,current)=>total[current],vm.$data);
                        final[arr1[arr1.length-1]] = e.target.value
                    })
                }
            })
        }
        node.childNodes.forEach(child => fragment_compile(child))
    }
    //把文档碎片应用到页面    页面显示出数据了
    vm.$el.appendChild(fragment)
}

2.4Dep
构造watcher实例时 把实例存到Dep实例中。

//构造watcher实例时  把实例存到Dep实例中
//触发getter 时添加订阅者到订阅者数组中
//依赖    收集和通知订阅者
class Dep{
	//构造函数
    constructor(){
        //数组: 存放所有订阅者的方法
        this.subs = []  
    }
    // 添加  订阅者的方法
    addSub(watcher) {
        this.subs.push(watcher)
    }

    // 发布订阅的方法
    notify() {
        this.subs.forEach(watcher => {
        	//调用Watcher实例的update方法,相当于通知它更新自己的dom
            watcher.update()
        })
    }
}

2.5watcher
在update中获取最新值 传给回调函数。
收集完watcher实例的依赖后,置空,防止订阅者 多次加入到依赖数组中。

//什么时候创建订阅者实例
//在修改文档内容 —— 模板解析时,把节点值 替换内容后,需要告诉订阅者要更新自己。
// 订阅者
class Watcher{
    //vm: vue实例     key :vue实例对应的属性   name、more.like
	//callback:创建实例时传进来的回调函数。函数内部定义更新dom的一些操作
    constructor(vm,key,callback) {
        this.vm = vm;
        this.key = key
    	//将回调函数 挂载到 生成的 Watcher实例上
        this.callback = callback
        //temp 临时变量 
        Dep.temp = this;
        key.split('.').reduce((total,current)=>total[current],vm.$data);//触发getter 会执行get()函数   获取watcher实例  添加订阅者
        Dep.temp = null;//防止订阅者 多次加入到依赖数组中
    }
    update() {
        //获取最新值 传给回调函数
        const value = this.key.split('.').reduce((total,current)=>total[current],this.vm.$data);
        //更新dom操作  也就是去执行它
        this.callback(value)
    }
}

总的vue.js

class Vue{
    //实例
    constructor(obj){
        this.$data = obj.data;
        //Observer函数进行数据劫持   监听实例里的数据
        Observer(this.$data)
        //vue实例挂载的元素   vue实例
        Compile(obj.el,this)
    }
}
//数据劫持
function Observer(data_instance){
    //递归出口  结束条件
    if(!data_instance ||typeof data_instance !=='object'){
        return
    }
    //在getter 添加订阅者
    const dep = new Dep();
    //以数组的方式 获取对象的key 
    Object.keys(data_instance).forEach(key =>{
        let value = data_instance[key]
        //属性值可能是对象  对子属性也要数据劫持
        Observer(value)
        //数据代理
        Object.defineProperty(data_instance,key,{
            enumerable:true,//可枚举
            configurable:true,//可修改
            get(){
                console.log(`get访问了属性:${key}-->值为:${value}`);
                //temp 不为空时执行 '与操作'     
                Dep.temp&&dep.addSub(Dep.temp)
                return value
            },
            set(newValue){
                console.log(`set属性:${key}修改为-->为${newValue}`);
                value = newValue;
                //新赋值的属性也要
                Observer(value);
                dep.notify()
            }
        })
    })
}

//HTML模板解析  替换DOM内
//vue实例挂载的元素   Vue实例
function Compile(element,vm){
    vm.$el = document.querySelector(element)
    //创建文档碎片  提高dom性能
    const fragment = document.createDocumentFragment();
    let child;
    //while中child = vm.$el.firstChild  再判断child是否为空
    while(child = vm.$el.firstChild){
        fragment.append(child) //节点添加到文档碎片后 页面没有数据了
    }
    fragment_compile(fragment)
    //修改fragment里的内容
    function fragment_compile(node){
        //    \s :匹配空格    *:0个或多个  (\S+): 至少一个字符
        //{{ xxx }}
        const pattern = /\{\{\s*(\S+)\s*\}\}/; 

        if(node.nodeType=== 3){// 文本节点
            const value = node.nodeValue;
            console.log(value);// 阿婆主:{{name}}      更多:{{more.like}}
            //exec()  返回数组形式  
            const res_regex = pattern.exec(node.nodeValue);
            if(res_regex){
            //(2) ['{{name}}', 'name', index: 4, input: '阿婆主:{{name}}', groups: undefined]
            //(2) ['{{more.like}}', 'more.like', index: 3, input: '更多:{{more.like}}', groups: undefined]
                // console.log(res_regex);
                const val = res_regex[1].split('.').reduce((total,current)=> total[current],vm.$data);
                //这里替换了  插值表达式 value  的内容
                //如由' 阿婆主:{{ name}}'  变为 '阿婆主:技术蛋老师'
                node.nodeValue = value.replace(pattern,val)
                // 继续遍历节点  节点里面可能还会有节点   递归

                //把节点值 替换内容后,需要告诉订阅者要更新自己。
                //创建订阅者
                //回调函数 在update中执行时  传来参数
                new Watcher(vm,res_regex[1],(newValue)=>{
                    //node.nodeValue = ' 阿婆主:{{ name}}' 插值表达式
                    //在回调时已经是替换过的内容 '阿婆主:技术蛋老师'  了,不是我们要的结果
                    //所以需要在最上面暂存const value = node.nodeValue;
                    node.nodeValue = value.replace(pattern,newValue)
                })

            }
            //  递归出口  终止
            return  
        }

        if(node.nodeType===1 &&node.nodeName==='INPUT'){
            const attr = Array.from(node.attributes);
            attr.forEach(i=>{
                if(i.nodeName==='v-model'){
                    const value = i.nodeValue.split('.').reduce(
                        (total,current)=>total[current],vm.$data
                    )
                    node.value = value;

                    //数据改变视图
                    //数据更新后 订阅者更新自己   
                    //回调函数 在update中执行时  传来参数
                    new Watcher(vm,i.nodeValue,(newValue)=>{
                        node.value = newValue;
                    });

                    //视图改变数据
                    node.addEventListener('input',(e)=>{
                        const arr1 = i.nodeValue.split('.');// ['more','like']
                        const arr2 = arr1.slice(0,arr1.length - 1);//['more']
                        //vm.$data.more
                        const final = arr2.reduce((total,current)=>total[current],vm.$data);
                        final[arr1[arr1.length-1]] = e.target.value
                    })
                }
            })
        }
        node.childNodes.forEach(child => fragment_compile(child))
    }
    //把文档碎片应用到页面
    vm.$el.appendChild(fragment)
}

//构造watcher实例时  把实例存到Dep实例中
//触发getter 时添加订阅者到订阅者数组中
//依赖    收集和通知订阅者
class Dep{
	//构造函数
    constructor(){
        //数组: 存放所有订阅者的方法
        this.subs = []  
    }
    // 添加  订阅者的方法
    addSub(watcher) {
        this.subs.push(watcher)
    }

    // 发布订阅的方法
    notify() {
        this.subs.forEach(watcher => {
        	//调用Watcher实例的update方法,相当于通知它更新自己的dom
            watcher.update()
        })
    }
}


//什么时候创建订阅者实例
//在修改文档内容 —— 模板解析时,把节点值 替换内容后,需要告诉订阅者要更新自己。
// 订阅者
class Watcher{
    //vm: vue实例     key :vue实例对应的属性   name、more.like
	//callback:创建实例时传进来的回调函数。函数内部定义更新dom的一些操作
    constructor(vm,key,callback) {
        this.vm = vm;
        this.key = key
    	//将回调函数 挂载到 生成的 Watcher实例上
        this.callback = callback
        //temp 临时变量 
        Dep.temp = this;
        key.split('.').reduce((total,current)=>total[current],vm.$data);//触发getter 会执行get()函数  获取watcher实例  添加订阅者
        Dep.temp = null;//防止订阅者 多次加入到依赖数组中
    }
    update() {
        //获取最新值 传给回调函数
        const value = this.key.split('.').reduce((total,current)=>total[current],this.vm.$data);
        //更新dom操作  也就是去执行它
        this.callback(value)
    }
}

更详细的博文链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值