Vue 双向数据绑定之原理及实现 1
Vue 双向数据绑定之原理及实现 2
Vue 双向数据绑定之原理及实现 3

一、Compiler解析器

解析器的作用一方面是解析出视图中相关的指令,将数据填充到视图中,另一方面也是添加新的订阅者,在数据发生更新时,能同步更新到视图中。
有了解析器以后,整个模型就算是完整了,参考下图:
双向数据绑定完整模型

Vue中的模板指令非常多,而且也做了很多的兼容,所以我们这只做例如v-model, {{}}, v-on
基本思路:

1、把真实DOM元素转换为文档片段;
2、遍历文档片段中所有的节点,解析出双括号指令和v-xxx的指令;
3、根据不同的指令,添加不同的操作:
    v-model: 初始化数据,添加订阅器,同时添加input事件;
    v-text: 初始化数据,添加订阅器;
    v-on: 为当前元素节点添加对应的事件和回调;


定义Compiler解析器类:

function Compiler(vm, el){
    this.vm = vm        // Vue对象
    this.el = document.querySelector(el)        // 挂载点
    this.fragment = null        // 文档片段
    this.init()    // 初始化
}

初始化操作:

Compiler.prototype = {
    init(){
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el) // 文档片段
            this.compileElement(this.fragment)  // 解析
            this.el.appendChild(this.fragment)  // 将文档片段添加到视图中
        } else {
            console.log('Dom元素不存在');
        }
    },
}

文档片段处理:

Compiler.prototype = {
    ...
    nodeToFragment(el){    // 文档片段处理
        var fragment = document.createDocumentFragment()
        var child = el.firstChild
        while (child) {
            fragment.appendChild(child)
            child = el.firstChild
        }
        return fragment
    },
}

解析操作,对应v-modelv-on{{}}是不有不同操作的,所以要进行判断处理。
v-textv-on是元素属性中的,而{{}}是在元素中的`innerHTML。

Compiler.prototype = {
    ...
    compileElement(fragment){    // 解析元素节点
        var childNodes = fragment.childNodes;
    
        [].slice.call(childNodes).forEach(node=>{
            var reg = /\{\{(.*)\}\}/
            var text = node.textContent
            
            if(this.isElementNode(node))    {    // 元素节点
                this.compileAttr(node)
            } else if (this.isTextNode(node) && reg.test(text)) {    // 文本节点
                this.compileText(node, reg.exec(text)[1])
            }
    
            // 继续递归遍历当前节点的子节点
            if (node.childNodes && node.childNodes.length) {
                this.compileElement(node);
            }
        });
    },
    isElementNode(node){    // 元素节点
        return node.nodeType === 1
    },
    isTextNode(node){    // 文本节点
        return node.nodeType === 3
    },
}

先处理{{}},只需要先将数据更新到界面中,并创建新的订阅者(会添加到订阅器中):

Compiler.prototype = {
    ...
    compileText(node, exp){    // {{}}的处理
        var val = this.vm[exp]    // 获取属性值
        node.textContent = val    // 更新到页面中
        new Watcher(this.vm, exp, value=>{ // 新的订阅者(绑定更新函数即可)
            node.textContent = value
        })
    },
}

对于元素中的属性v-modelv-on绑定处理的,又需要判断出是那种,v-on这只需要绑定上对应的事件即可;而v-model会不一样,所有需要区分处理:

Compiler.prototype = {
    ...
    compileAttr(node){    // v-xxx的处理
            // 获取当前节点上所有的属性节点
            var nodeAttrs = node.attributes;
            var self = this;
            
            // 遍历属性,找到是否有v-xxx
            [].slice.call(nodeAttrs).forEach(attr=>{
                var attr_name = attr.name
                
                var reg = /v\-/;
                if(reg.test(attr_name)){        // v-xxx指令
                    var exp = attr.value        // 对应的事件
                    let dir = attr_name.substring(2)        // 字符串切分 v-   xxx
                    
                    reg = /on\:/;
                    if(reg.test(dir)){    // v-on事件绑定
                        self.compileEvent(node, self.vm, exp, dir)
                    } else{    // v-model双向数据绑定    [备注: 我们只做几个指令操作]
                        self.compileModel(node, self.vm, exp, dir)
                    }
                    
                    
                    // 操作完成后,删除对应的属性
                    node.removeAttribute(attr_name)
                }
            })
    },
}

v-on事件绑定的处理,根据属性值,获取事件句柄,绑定上即可:

Compiler.prototype = {
    ...
    compileEvent(node, vm, exp, dir){    // v-on的处理
            // 例如   v-on:click='xxxx'
            // 获取事件类型
            var eventType = dir.split(':')[1]
            // 根据事件名,获取函数句柄
            var callback = vm.methods && vm.methods[exp]
            if(eventType && callback){    // 有事件名且定义有对应事件处理
                node.addEventListener(eventType, callback.bind(vm), false)
            } else {
                console.log('请在methods中添加对应的方法: ' + exp)
            }
    },
}

v-model双向数据绑定,数据先要更新到界面中,接着是创建新的订阅者(会添加到订阅器中),最后还要对输入事件的处理:

Compiler.prototype = {
    ...
    compileModel(node, vm, exp, dir){    // v-model的处理,为model绑定input事件
            var val = this.vm[exp]    // 获取属性值
            node.value = val     // 更新到页面中
            new Watcher(this.vm, exp, value=>{ // 新的订阅者(绑定更新函数即可)
                node.value = value
            })
            
            node.addEventListener('input', e => {
            let newVal = e.target.value
            if (val == newVal) {
                return false
            }
            this.vm[exp] = newVal
            val = newVal
        }, false)
    },
}

二、Vue类的创建

添加上监听器,并对DOM进行指令解析,根据不同指令添加不同的操作(例如事件处理、订阅服务...)

function Vue(options) {
    this.el = options.el
    this.data = options.data
    this.methods = options.methods

    Object.keys(this.data).forEach(key => { // 数据代理
        this.proxyKeys(key)
    })
    new Observer(this.data) // 监听器    
    new Compiler(this, this.el) // DOM解析
}

Vue.prototype = {
    proxyKeys(key) {
        var self = this
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get() {
                return self.data[key]
            },
            set(newVal) {
                self.data[key] = newVal
            }
        })
    },
}

三、测试

代码参考: https://github.com/iphone3/VueDataBinging

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        
    </head>
    
    <body>
        <div id="app">
            <h1 v-on:click="onAlert">Vue双向数据绑定之原理及实现</h1>
            <h3>名字: {{name}}</h3>
            
            <div>
                请输入你的名字:<input type="text" class="text" v-model='name' />
            </div>
        </div>
        
        <script src="dep.js" type="text/javascript" charset="utf-8"></script>
        <script src="watcher.js" type="text/javascript" charset="utf-8"></script>
        <script src="observer.js" type="text/javascript" charset="utf-8"></script>
        <script src="compiler.js" type="text/javascript" charset="utf-8"></script>
        <script src="Vue.js" type="text/javascript" charset="utf-8"></script>
        
        <script type="text/javascript">
            var app = new Vue({
                el:'#app',
                data: {
                    name: 'atom'
                }, 
                methods: {
                    onAlert: function(){
                        alert('恭喜你完成了学习!!!')
                    }
                }
            })
        </script>
    </body>
</html>

本文由 zyz 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论