vue源码分析(六):数据绑定与数据劫持

前面几节我们说了vue初始化显示的一些内容,重点在指令解析上面。
当初始化的视图绘制好了之后,问题又来了——当数据发生了更新,视图怎么办呢?
这就引出了今天的知识点——数据绑定。

什么是数据绑定?

数据绑定,就是一旦更新了data中的某个属性数据,所有界面上直接或间接使用了此属性的节点就会更新。




    
    Title


直接使用:{{firstname}}

间接使用:{{fullname}}

这个如何实现的呢?答案是数据劫持。

什么是数据劫持?

先来回顾一下之前的数据代理:
https://www.jianshu.com/p/3442527d543a
数据代理是通过对Vue对象增加get和set方法,可以监听data里面的信息,从而达到代理目的,那data的数据改变后,如何让视图也更新呢?




    
    Title


{{name}}

我们可以通过defineProperty()来监视vm.data中所有属性(任意层次)数据的变化,一旦变化就去更新界面,这就是数据劫持,简单吧~~
也就是说,数据代理是在vm对象中增加get和set,而数据劫持要在vm.data对象中增加get和set,二者对目的不一样哈,可以对比着来看。

实现数据劫持

我们先把前面的数据代理和模板解析两部分整理出来合成一处:

function Vue(options){
    this.$options = options
    var data = this._data = this.$options.data
    //=====数据代理=====//
    Object.keys(data).forEach(key=>{
        this._proxy(key)
    })
    //=====数据劫持=====//
    


    //=====模板解析=====//
    this.$compile = new Compile(options.el,this) 
}
Vue.prototype = {
    _proxy(key){
        Object.defineProperty(this,key,{
            configurable:false,
            enumerable:true,
            get(){
                return this._data[key]
            },
            set(newVal){
                this._data[key] = newVal
            }
        })
    }
}

function Compile(el,vm){
    this.$vm = vm
    this.$el = document.querySelector(el)
    console.log(this.$el)
    if(this.$el){
        this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理

        this.init() //开始处理

        this.$el.appendChild(this.$fragment) //塞回原位
    }
}
Compile.prototype = {
    //将#app里面的节点都转到文档碎片中
    node2Fragment(el){
        var fragment = document.createDocumentFragment()
        var child = null
        while(child = el.firstChild) {
            fragment.appendChild(child)
        }
        return fragment
    },
    //处理碎片中的信息
    init(){
        this.compileElement(this.$fragment)
    },
    //正则匹配
    compileElement(el){
        var childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node=>{
            var text = node.textContent // 获取文本信息
            var reg = /\{\{(.*)\}\}/
            //这里增加指令的处理
            if(node.nodeType === 1){
                //如果是元素节点
                this.compile(node)
            } else if(node.nodeType === 3 && reg.test(text)) {

                this.compileText(node,RegExp.$1)
            } else if(node.childNodes && node.childNodes.length) {
                //递归
                this.compileElement(node)
            }
        })
    },
    //处理元素节点
    compile(node){
        var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
        [].slice.call(nodeAttrs).forEach(attr=>{
            var attrName = attr.name;//获取属性名
            if(attrName.indexOf('v-') === 0){//判断是否是指令
                var exp = attr.value; //获取属性值,也就是触发的方法名
                var dir = attrName.substring(2) // 截取字符串,得到on:click
                if(dir.indexOf('on') === 0){ //判断事件指令
                    var eventType = dir.split(':')[1]; //获取事件类型
                    var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
                    if(eventType && fn) {
                        node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
                    }



                } else if(dir.indexOf('bind') === 0) { // 一般指令
                    var dirType = dir.split(':')[1] // 获取指令类型class
                    if(dirType === 'class') {
                        var oldClassName = node.className; //原来的class类
                        var newClassName = '' //动态class类
                        var classObj = eval('(' + exp + ')'); //解析为object对象

                        for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
                            if(classObj[key]) {
                                newClassName += ' ' + key
                            }
                        }
                        node.className = oldClassName + newClassName // 设置className
                    }

                }
                node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
            }
        })

    },
//用vm.data信息,替换大括号的name
    compileText(node,exp){

        node.textContent = this.getVMVal(exp)
    },
//处理层级问题
    getVMVal(exp){ // a.b.c
        var val = this.$vm._data
        var arr = exp.split('.') //["a", "b", "c"]
        console.log(exp)
        arr.forEach(k=>{
            //debugger
            val = val[k] // 层级递进
        })
        return val
    }
}

剩下的数据劫持部分,我们先脱离以上的代码单独实现,然后再拼回去,这样可以避免代码过多造成的干扰。
其实我们要实现的无非是这样的东西:

//示例对象
var vm = {
    data: {
        name: '简单数据',
        a:{
            b:{
                c:'嵌套数据'
            }
        }
    }
}
//数据劫持
observe(vm)
console.log(vm)

function observe(data){
    //对data进行defineProperty处理,嵌套的数据要用递归
    if(!data || typeof data!=='object'){//递归的退出条件
        return;
    }

    Object.keys(data).forEach(key=>{
        defineReactive(data,key,data[key])//对data的每一个key进行定义,如果是嵌套对象,放到defineReactive()函数内部处理
    })

}

function defineReactive(data,key,val){

    observe(val)//处理嵌套对象:执行递归,确保所有层次的key都可以被定义
    
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:false,
        get(){
            return val
        },
        set(newVal){
            val = newVal //修改为最新值
        }
    })
}

打印一下结果:
demo.png

可以看出,在vm.data的所有层次的属性都增加了get和set函数监听,我们把上述代码先合进去,起个名字叫myvue.js吧:

function Vue(options){
    this.$options = options
    var data = this._data = this.$options.data
    //数据代理
    Object.keys(data).forEach(key=>{
        this._proxy(key)
    })

    //数据劫持
    observe(data)


    //模板解析
    this.$compile = new Compile(options.el,this) //模板解析
}
Vue.prototype = {
    _proxy(key){
        Object.defineProperty(this,key,{
            configurable:false,
            enumerable:true,
            get(){
                return this._data[key]
            },
            set(newVal){
                this._data[key] = newVal
            }
        })
    }
}

function Compile(el,vm){
    this.$vm = vm
    this.$el = document.querySelector(el)
    console.log(this.$el)
    if(this.$el){
        this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理

        this.init() //开始处理

        this.$el.appendChild(this.$fragment) //塞回原位
    }
}
Compile.prototype = {
    //将#app里面的节点都转到文档碎片中
    node2Fragment(el){
        var fragment = document.createDocumentFragment()
        var child = null
        while(child = el.firstChild) {
            fragment.appendChild(child)
        }
        return fragment
    },
    //处理碎片中的信息
    init(){
        this.compileElement(this.$fragment)
    },
    //正则匹配
    compileElement(el){
        var childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node=>{
            var text = node.textContent // 获取文本信息
            var reg = /\{\{(.*)\}\}/
            //这里增加指令的处理
            if(node.nodeType === 1){
                //如果是元素节点
                this.compile(node)
            } else if(node.nodeType === 3 && reg.test(text)) {

                this.compileText(node,RegExp.$1)
            } else if(node.childNodes && node.childNodes.length) {
                //递归
                this.compileElement(node)
            }
        })
    },
    //处理元素节点
    compile(node){
        var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
        [].slice.call(nodeAttrs).forEach(attr=>{
            var attrName = attr.name;//获取属性名
            if(attrName.indexOf('v-') === 0){//判断是否是指令
                var exp = attr.value; //获取属性值,也就是触发的方法名
                var dir = attrName.substring(2) // 截取字符串,得到on:click
                if(dir.indexOf('on') === 0){ //判断事件指令
                    var eventType = dir.split(':')[1]; //获取事件类型
                    var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
                    if(eventType && fn) {
                        node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
                    }



                } else if(dir.indexOf('bind') === 0) { // 一般指令
                    var dirType = dir.split(':')[1] // 获取指令类型class
                    if(dirType === 'class') {
                        var oldClassName = node.className; //原来的class类
                        var newClassName = '' //动态class类
                        var classObj = eval('(' + exp + ')'); //解析为object对象

                        for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
                            if(classObj[key]) {
                                newClassName += ' ' + key
                            }
                        }
                        node.className = oldClassName + newClassName // 设置className
                    }

                }
                node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
            }
        })

    },
//用vm.data信息,替换大括号的name
    compileText(node,exp){

        node.textContent = this.getVMVal(exp)
    },
//处理层级问题
    getVMVal(exp){ // a.b.c
        var val = this.$vm._data
        var arr = exp.split('.') //["a", "b", "c"]
        console.log(exp)
        arr.forEach(k=>{
            //debugger
            val = val[k] // 层级递进
        })
        return val
    }
}

//新增部分
function observe(data){
    //对data进行defineProperty处理,嵌套的数据要用递归
    if(!data || typeof data!=='object'){//递归的退出条件
        return;
    }

    Object.keys(data).forEach(key=>{
        defineReactive(data,key,data[key])//对data里面的每一个key进行定义
    })

}

function defineReactive(data,key,val){
    observe(val)//先执行递归,确保嵌套对象的key都可以被定义
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:false,
        get(){
            console.log('getData>>>',key)
            return val
        },
        set(newVal){
            console.log('setData',key)
            val = newVal //修改为最新值
        }
    })
}

把这个自定义的js引入测试一下:




    
    Title


{{name}}

输出结果:

demo(1).png

当点击按钮时,get和set已经执行到了,但页面并未发生变化。
下面的工作,就是完善get和set函数了,这俩哥们需要负责实现:
当数据发生了变化,要更新到视图
我们写的Compile类是处理视图的,主要是做了一些解析工作,为了研究方便,这里我们就只考虑大括号表达式。
以{{name}}为例,思路为:
1、{{name}}在页面视图初始化完成后,订阅更新视图的函数(this.compileText)
2、在data的set函数里面,触发更新视图函数。
因此,我们需要增加一个观察者对象进来:

function Vue(options){
    this.$options = options
    var data = this._data = this.$options.data
    //数据代理
    Object.keys(data).forEach(key=>{
        this._proxy(key)
    })

    //数据劫持
    observe(data)


    //模板解析
    this.$compile = new Compile(options.el,this) //模板解析
}
Vue.prototype = {
    _proxy(key){
        Object.defineProperty(this,key,{
            configurable:false,
            enumerable:true,
            get(){
                return this._data[key]
            },
            set(newVal){
                this._data[key] = newVal
            }
        })
    }
}

function Compile(el,vm){
    this.$vm = vm
    this.$el = document.querySelector(el)

    if(this.$el){
        this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理

        this.init() //开始处理

        this.$el.appendChild(this.$fragment) //塞回原位
    }
}
Compile.prototype = {
    //将#app里面的节点都转到文档碎片中
    node2Fragment(el){
        var fragment = document.createDocumentFragment()
        var child = null
        while(child = el.firstChild) {
            fragment.appendChild(child)
        }
        return fragment
    },
    //处理碎片中的信息
    init(){
        this.compileElement(this.$fragment)
    },
    //正则匹配
    compileElement(el){
        var childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node=>{
            var text = node.textContent // 获取文本信息
            var reg = /\{\{(.*)\}\}/
            //这里增加指令的处理
            if(node.nodeType === 1){
                //如果是元素节点
                this.compile(node)
            } else if(node.nodeType === 3 && reg.test(text)) {

                this.compileText(node,RegExp.$1) //更新视图动作


                //订阅更新视图函数
                dep.register(RegExp.$1,()=>{
                    //如果data发生了变化,需要再次调用this.compileText这个函数来更新视图
                    this.compileText(node,RegExp.$1)
                })

            } else if(node.childNodes && node.childNodes.length) {
                //递归
                this.compileElement(node)
            }
        })
    },
    //处理元素节点
    compile(node){
        var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
        [].slice.call(nodeAttrs).forEach(attr=>{
            var attrName = attr.name;//获取属性名
            if(attrName.indexOf('v-') === 0){//判断是否是指令
                var exp = attr.value; //获取属性值,也就是触发的方法名
                var dir = attrName.substring(2) // 截取字符串,得到on:click
                if(dir.indexOf('on') === 0){ //判断事件指令
                    var eventType = dir.split(':')[1]; //获取事件类型
                    var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
                    if(eventType && fn) {
                        node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
                    }



                } else if(dir.indexOf('bind') === 0) { // 一般指令
                    var dirType = dir.split(':')[1] // 获取指令类型class
                    if(dirType === 'class') {
                        var oldClassName = node.className; //原来的class类
                        var newClassName = '' //动态class类
                        var classObj = eval('(' + exp + ')'); //解析为object对象

                        for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
                            if(classObj[key]) {
                                newClassName += ' ' + key
                            }
                        }
                        node.className = oldClassName + newClassName // 设置className
                    }

                }
                node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
            }
        })

    },
//用vm.data信息,替换大括号的name
    compileText(node,exp){
        console.log('最新值是:',this.getVMVal(exp))
        node.textContent = this.getVMVal(exp)
    },
//处理层级问题
    getVMVal(exp){ // a.b.c
        var val = this.$vm._data
        var arr = exp.split('.') //["a", "b", "c"]
        arr.forEach(k=>{
            //debugger
            val = val[k] // 层级递进
        })
        return val
    }
}

//新增部分
function observe(data){
    //对data进行defineProperty处理,嵌套的数据要用递归
    if(!data || typeof data!=='object'){//递归的退出条件
        return;
    }

    Object.keys(data).forEach(key=>{
        defineReactive(data,key,data[key])//对data里面的每一个key进行定义
    })

}

function defineReactive(data,key,val){
    observe(val)//先执行递归,确保嵌套对象的key都可以被定义
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:false,
        get(){
            console.log('get操作:',key,'===>',val)
            return val
        },
        set(newVal){
            console.log('set操作:',key,'===>',newVal)
            val = newVal //修改为最新值
            //触发更新视图函数
            dep.emit(key)
        }
    })
}
//引入观察订阅模式
var dep = new Dep()
function Dep(){
    this.subs = {}
}

Dep.prototype.register = function (key, callback) {

    this.subs[key] = callback
}
Dep.prototype.emit = function (key) {
    this.subs[key]()

}

执行结果:
test.png

这样更新视图就完成了。
不过,这只是最简单的一种情况哦,如果是嵌套对象就嗝屁了:




    
    Title


{{a.b}}

输出结果:
1599228802(1).png

为啥会这样呢?很简单,原因写在了代码注释里,自己看:

function Vue(options){
    this.$options = options
    var data = this._data = this.$options.data
    //数据代理
    Object.keys(data).forEach(key=>{
        this._proxy(key)
    })

    //数据劫持
    observe(data)


    //模板解析
    this.$compile = new Compile(options.el,this) //模板解析
}
Vue.prototype = {
    _proxy(key){
        Object.defineProperty(this,key,{
            configurable:false,
            enumerable:true,
            get(){
                return this._data[key]
            },
            set(newVal){
                this._data[key] = newVal
            }
        })
    }
}

function Compile(el,vm){
    this.$vm = vm
    this.$el = document.querySelector(el)

    if(this.$el){
        this.$fragment = this.node2Fragment(this.$el) //将节点转到fragment处理

        this.init() //开始处理

        this.$el.appendChild(this.$fragment) //塞回原位
    }
}
Compile.prototype = {
    //将#app里面的节点都转到文档碎片中
    node2Fragment(el){
        var fragment = document.createDocumentFragment()
        var child = null
        while(child = el.firstChild) {
            fragment.appendChild(child)
        }
        return fragment
    },
    //处理碎片中的信息
    init(){
        this.compileElement(this.$fragment)
    },
    //正则匹配
    compileElement(el){
        var childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node=>{
            var text = node.textContent // 获取文本信息
            var reg = /\{\{(.*)\}\}/
            //这里增加指令的处理
            if(node.nodeType === 1){
                //如果是元素节点
                this.compile(node)
            } else if(node.nodeType === 3 && reg.test(text)) {

                this.compileText(node,RegExp.$1) //更新视图动作


                //订阅更新视图函数
                dep.register(RegExp.$1,()=>{ //注册的key是a.b
                    //如果data发生了变化,需要再次调用this.compileText这个函数来更新视图
                    this.compileText(node,RegExp.$1)
                })

            } else if(node.childNodes && node.childNodes.length) {
                //递归
                this.compileElement(node)
            }
        })
    },
    //处理元素节点
    compile(node){
        var nodeAttrs = node.attributes; // 获取元素节点的所有属性,伪数组
        [].slice.call(nodeAttrs).forEach(attr=>{
            var attrName = attr.name;//获取属性名
            if(attrName.indexOf('v-') === 0){//判断是否是指令
                var exp = attr.value; //获取属性值,也就是触发的方法名
                var dir = attrName.substring(2) // 截取字符串,得到on:click
                if(dir.indexOf('on') === 0){ //判断事件指令
                    var eventType = dir.split(':')[1]; //获取事件类型
                    var fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];// 获取函数
                    if(eventType && fn) {
                        node.addEventListener(eventType,fn.bind(this.$vm),false) // 注意fn里面的this指向
                    }



                } else if(dir.indexOf('bind') === 0) { // 一般指令
                    var dirType = dir.split(':')[1] // 获取指令类型class
                    if(dirType === 'class') {
                        var oldClassName = node.className; //原来的class类
                        var newClassName = '' //动态class类
                        var classObj = eval('(' + exp + ')'); //解析为object对象

                        for(var key in classObj) { //遍历对象,如果value为true追加class名,false不追加
                            if(classObj[key]) {
                                newClassName += ' ' + key
                            }
                        }
                        node.className = oldClassName + newClassName // 设置className
                    }

                }
                node.removeAttribute(attrName) // 从chrome控制台的Elements看文档结构,发现没有v-这样的属性(指令),所以需要处理完去掉
            }
        })

    },
//用vm.data信息,替换大括号的name
    compileText(node,exp){
        console.log('最新值是:',this.getVMVal(exp))
        node.textContent = this.getVMVal(exp)
    },
//处理层级问题
    getVMVal(exp){ // a.b.c
        var val = this.$vm._data
        var arr = exp.split('.') //["a", "b", "c"]
        arr.forEach(k=>{
            //debugger
            val = val[k] // 层级递进
        })
        return val
    }
}

//新增部分
function observe(data){
    //对data进行defineProperty处理,嵌套的数据要用递归
    if(!data || typeof data!=='object'){//递归的退出条件
        return;
    }

    Object.keys(data).forEach(key=>{
        defineReactive(data,key,data[key])//对data里面的每一个key进行定义
    })

}

function defineReactive(data,key,val){
    observe(val)//先执行递归,确保嵌套对象的key都可以被定义
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:false,
        get(){
            console.log('get操作:',key,'===>',val)
            return val
        },
        set(newVal){
            console.log('set操作:',key,'===>',newVal)
            val = newVal //修改为最新值
            //触发更新视图函数
            dep.emit(key) // 这里的key是b,而不是a.b
        }
    })
}
//引入观察订阅模式
var dep = new Dep()
function Dep(){
    this.subs = {}
}

Dep.prototype.register = function (key, callback) {

    this.subs[key] = callback
}
Dep.prototype.emit = function (key) {
    console.log(key)
    this.subs[key]()

}

主要是嵌套对象注册的key,跟触发时的key不一致了,那怎么办呢?请看下回分解。

你可能感兴趣的:(vue源码分析(六):数据绑定与数据劫持)