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
posted @ 2017-08-30 16:12  蒲公英tt  阅读(928)  评论(0编辑  收藏  举报