zoukankan      html  css  js  c++  java
  • vue双向数据绑定原理

    1、什么是数据双向绑定?

    数据的双向绑定就是:当数据发生变化时,view同时发生变化;在单页spa中或表单操作中比较适用,常用的mvvm框架中,都存在数据的双向绑定,只是实现的原理不一样。

    2、mvvm的数据双向绑定方案对比

    Angular:事件触发

    原理:其是通过’验脏’实现双向绑定,这种‘验脏’是什么时候触发呢?如DOM事件,XHR事件[ajax异步]时,便会执行‘验脏’。还有一种非常简单的方法实现,那就是使用setTimeout()实现,进行定时检查。当然这样效率、性能都比较差。

    Vue:数据劫持

    原理:vue的数据双向绑定是通过依赖跟踪与属性劫持实现的。当相应的数据发生变更的时候,对应的视图也随之发生变更,从而实现最小的变化。其技术核心为ES5当中提出的,Object.defineProperty(),通过对数据属性的setter/getter进行劫持,从而实现数据变更时,视图变更。

    3、Vue双向数据绑定的实现

    下面的代码,与Vue源码比较更为简单,但原理一致。省去了属性深度监控,mutation observer过程等,着重分析数据双向绑定的过程。

    3.1、角色

    • 观察者
      这个角色很重要,它是用来观察数据对应的视图,每个数据对应的视图都有一个相应的观察者,当该数据发生变化时,对相应视图进行更新。这样也就实现了依赖跟踪。

    • 订阅器
      对每个数据实现一个订阅器,其维护一个观察着队列,当数据发生更新时,相应的订阅器接受到通知,使得相应的观察者进行视图更新。

    • 角色与数据之间的关系如下:

      图1 角色关系图

    如图,以单个属性为中心,其与订阅器为一对一的关系;与观察者为一对多的关系。

    • 下面看看两个角色的具体实现
      • 订阅器
    class Dep {
        constructor() {
            this.subList = []
        }
        // 增加订阅者
        addDep(watcher) {
            if (!watcher) {
                return
            }
            this.subList.push(watcher)
        }
        // 订阅通知
        notify() {
            this.subList.forEach((watcher) => {
                watcher.update()
            })
        }
    } 
    

    其中的数组用来保存对应的观察者watcher;addDep方法,用来增加订阅者,每个订阅者为一个watcher实例。notify方法,用来在数据发生变更时,接收通知,并触发对应的watcher更新视图。
    * 观察者

    class Watcher {
        constructor({name, node, nodeType, vm}) {
            Object.assign(this, {
                name,
                node,
                nodeType,
                vm
            })
        }
        update() {
            if (this.nodeType == 'text') {
                this.node.nodeValue = this.vm.data[this.name]
            }
            if (this.nodeType == 'input') {
                this.node.value = this.vm.data[this.name]
            }
        }
    }
    

    在其构造函数中,包含了数据属性名(name),DOM节点(node),节点类型(nodeType),对应的vm实例(vm);从而将数据对应属性与DOM节点建立依赖,从而实现依赖可追踪。另update方法,可实现对应节点的更新。

    3.2、劫持

    上面有说到劫持,到底是如何实现数据属性的劫持呢?

    • 劫持过程如下图所示:

      图2 数据劫持过程

    • 具体实现如下

    _defineReactive(obj, key, val) {
        depBuff[key] = new Dep()
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                return val
            },
            set(newValue) {
                if (newValue != val) {
                    val = newValue
                    depBuff[key].notify()
                }
            }
        })
    }
    

    如代码所示,通过使用defineProperty对数据的getter/setter进行劫持,实现变更的监测。当数据被赋值是,set方法被调用,当数据发生更新时,通知对应的订阅器,进行视图更新。

    3.3、依赖追踪

    在模版编译过程中,实现依赖追踪:

    // 使用fragment进行模版编译
    _compile(pNode, vm) {
        let fragment = document.createDocumentFragment()
        let child = null
        while(child = pNode.firstChild) {
            if (child.children) {
                child.appendChild(this._compile(child, vm))
            } 
            this._compileOneNode(child, fragment, vm)
            fragment.appendChild(child)
        }
        return fragment
    }
    
    // 遇到特殊命令,或者文本节点类型,则生成观察者,并将其放入订阅器中[可根据特殊命令的格式进行订制]
    _compileOneNode(child, fragment, vm) {
        // 为tag节点时,查找其属性节点,看看是否有v-model指令
        if (child.nodeType == 1) {
            let attrs = child.attributes
            for (let i = 0; i < attrs.length; i++) {
                let item = attrs[i]
                if (item.nodeName == 'v-model' && child.nodeName == 'INPUT') {
                    // 当input输入时,更新vm数据
                    child.addEventListener('input', function (e) {
                        vm.data[item.nodeValue] = e.target.value
                    })
                    // 生成对应模版的观察者
                    let watcher = new Watcher({
                        name: item.nodeValue,
                        node: child,
                        nodeType: 'input',
                        vm: vm
                    })
                    // 初始数据编译
                    watcher.update() 
                    // 将观察者放入订阅器
                    depBuff[item.nodeValue].addDep(watcher)
                    // 移除命令属性
                    child.removeAttribute(item.nodeName)
                }
            }
        }
         // 为文本节点时,判断是否有相应的特殊字符匹配;有则标记为文本节点
        if (child.nodeType == 3) {
            let reg = /{{(.*)}}/
            if (reg.test(child.nodeValue)) {
                let name = RegExp.$1
                name = name.trim()
                let watcher = new Watcher({
                    name,
                    node: child,
                    nodeType: 'text',
                    vm: vm
                })
                watcher.update()
                depBuff[name].addDep(watcher)
            }
        }
    }
    

    3.4、流程

    数据的遍历[劫持] -> 模版编译[依赖追踪]
    流程如下图所示:

    图3 流程图

    代码如下:

    class Vue {
        constructor({data, $el}) {
            Object.assign(this, {
                data,
                $el
            })
            // 遍历数据,进行属性劫持
            Object.keys(data).forEach((key) => {
                this._defineReactive(data, key, data[key])
            })
            // 进行模板编译,生成追踪依赖
            let $elElem = document.querySelector($el)
            $elElem.appendChild(this._compile($elElem, this))
        }
    }
    

    如代码所示,在vue的构造函数中,实现初始化流程:
    step1:vue实例化赋值;
    step2:遍历data,实现属性值的劫持设置;
    step3:模版编译,实现数据的依赖追踪。
    在step2中,借助ES5的Object.defineProperty()实现。
    在step3中,模版编译过程借助了documentFragment进行DOM碎片的管理;其编译的特殊命令(模式)匹配可根据具体业务进行订制。

    3.5、完整代码

    html代码

    <div id="demo">
        <div style="margin-bottom: 40px">
            <p>姓名:<input id="name" type="text" v-model="name" /></p>
            <p>学号:<input id="number" type="text" v-model="number" /></p>
            <p>勋章:<input id="metal" type="text" v-model="metal" /></p>
            <p>等级:<input id="level" type="text" v-model="level" /></p>
        </div>
        <div>
            <p>姓名:<span>{{name}}</span></p>
            <p>学号:<span>{{number}}</span></p>
            <p>勋章:<span>{{metal}}</span></p>
            <p>等级:<span>{{level}}</span></p>
            <p>签名:<span>{{name}}</span></p>
        </div>
        <button>点我提交</button>
    </div>
    

    js代码

    let depBuff = {}
    class Vue {
        constructor({data, $el}) {
            Object.assign(this, {
                data,
                $el
            })
            // 生成每个数据的订阅器,并在数据被set的时候,如有数据更新,则发布通知
            Object.keys(data).forEach((key) => {
                this._defineReactive(data, key, data[key])
            })
            // 进行模板编译,并对模版中的数据,增加观察着,并将观察者加入响应的订阅器当中
            let $elElem = document.querySelector($el)
            $elElem.appendChild(this._compile($elElem, this))
        }
        // 使用fragment进行模版编译
        _compile(pNode, vm) {
            let fragment = document.createDocumentFragment()
            let child = null
            while(child = pNode.firstChild) {
                if (child.children) {
                    child.appendChild(this._compile(child, vm))
                } 
                this._compileOneNode(child, fragment, vm)
                fragment.appendChild(child)
            }
            return fragment
        }
        // 遇到特殊命令,或者文本节点类型,则增加观察者,放入订阅器
        _compileOneNode(child, fragment, vm) {
            // 为tag节点时,查找其属性节点,看看是否有v-model指令
            if (child.nodeType == 1) {
                let attrs = child.attributes
                for (let i = 0; i < attrs.length; i++) {
                    let item = attrs[i]
                    if (item.nodeName == 'v-model' && child.nodeName == 'INPUT') {
                        // 当input输入时,更新vm数据
                        child.addEventListener('input', function (e) {
                            vm.data[item.nodeValue] = e.target.value
                        })
                        // 生成对应模版的观察者
                        let watcher = new Watcher({
                            name: item.nodeValue,
                            node: child,
                            nodeType: 'input',
                            vm: vm
                        })
                        watcher.update()
                        // 将观察者放入订阅器
                        depBuff[item.nodeValue].addDep(watcher)
                        child.removeAttribute(item.nodeName)
                    }
                }
            }
            if (child.nodeType == 3) {
                let reg = /{{(.*)}}/
                if (reg.test(child.nodeValue)) {
                    let name = RegExp.$1
                    name = name.trim()
                    let watcher = new Watcher({
                        name,
                        node: child,
                        nodeType: 'text',
                        vm: vm
                    })
                    watcher.update()
                    depBuff[name].addDep(watcher)
                }
            }
        }
        // 对每个数据生成对应的订阅器,并进行修改劫持
        _defineReactive(obj, key, val) {
            depBuff[key] = new Dep()
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return val
                },
                set(newValue) {
                    if (newValue != val) {
                        val = newValue
                        depBuff[key].notify()
                    }
                }
            })
        }
    }
    
    // 观察者:观察模版及数据的一一对应,一个数据存在多个观察者
    class Watcher {
        constructor({name, node, nodeType, vm}) {
            Object.assign(this, {
                name,
                node,
                nodeType,
                vm
            })
        }
        update() {
            if (this.nodeType == 'text') {
                this.node.nodeValue = this.vm.data[this.name]
            }
            if (this.nodeType == 'input') {
                this.node.value = this.vm.data[this.name]
            }
        }
    }
    
    // 发布/订阅  每个订阅者对应一个数据,一个数据存在多个观察者,所以一个订阅器,存在多个观察者,此处维护一个观察者列表
    class Dep {
        constructor() {
            this.subList = []
        }
        addDep(watcher) {
            if (!watcher) {
                return
            }
            this.subList.push(watcher)
        }
        notify() {
            this.subList.forEach((watcher) => {
                watcher.update()
            })
        }
    }
    
    let vm = new Vue({
        data: {
            name: '嘻哈',
            number: '1234',
            metal: '金辉',
            level: 'level1',
        },
        $el: '#demo'
    })
    
    • tips:在实际的Vue的视图更新中,会用到HTML5的Mutation Observer,其作用是可以通过监测DOM tree的变化,当该循环中,DOM tree的变更结束之后,才触发一次事件执行回调,从而减少浏览器的等待时间[异步]。例如我有100个段落要插入DOM当中,只有所有DOM插入结束之后,才会触发事件并执行相应回调。原本的mutation Events是同步执行的,也就是插入一个DOM,触发一次事件,执行回调,处理完成再处理下一个,会拖慢浏览器。详情链接:http://www.jianshu.com/p/b5c9e4c7b1e1
  • 相关阅读:
    POJ数据结构专辑(含部分题解)
    第K小数 uva 10041 Vito's Family poj 2388 Who's in the Middle
    POJ 1195 Mobile phones (二维树状树组)
    python 学习体会
    ACM竞赛常用STL(二)之STLalgorithm
    计算机科学中的树
    ctf古典密码从0到
    漏洞挖掘的艺术面向源码的静态漏洞挖掘
    漏洞挖掘的艺术面向二进制的静态漏洞挖掘
    实战演示 H5 性能分析
  • 原文地址:https://www.cnblogs.com/hity-tt/p/7453662.html
Copyright © 2011-2022 走看看