一、实现原理

1. 从数据到视图的更新,是需要对数据进行监听劫持,这里我们设置一个监听器Observer来实现对所有数据的监听;

2. 设置一个订阅者Watcher,收到属性的变化通知并执行相应的函数,从而更新视图;

3. 设置一个解析器Compiler,解析视图DOM中所有节点的指令,并将模板中的数据进行初始化,然后初始化对应的订阅器。

二、Observer监听器

Observer是一个数据监听器,核心是Object.defineProperty(),对所有属性监听,利用递归来遍历所有的属性值,对其进行Object.defineProperty()操作。

// observer.js文件
function Observer(data) {    // 监听器
    this.walk(data)
}
Observer.prototype = {
    walk(data) {
        if( !data || typeof data !== 'object') {
            return false
        }

        Object.keys(data).forEach(key => { // 遍历操作
            this.defineProperty(data, key, data[key])
        })
    },
    defineProperty(data, key, val) { // set/get方法
        this.walk(val) // key 对应的又是 字典对象时
        
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: false,
            get() {
                return val
            },
            set(newVal) {
                if(val !== newVal) {
                    console.log('新值: '+ newVal);
                    val = newVal
                }
            }
        })
    }
}

// 测试数据
var zhangsan = {
    name: '张三',
    age: 18,
    score:{
        math: 100,
        english:90
    }
}

// 添加监听
new Observer(zhangsan)
zhangsan.age = 20
zhangsan.score.english = 99

确保都数据中的每个属性都转为getter/setter方法!

三、Dep订阅器

在最开始的结构中,通过Observer劫持并监听数据,当属性发生改变时,即通知调用到Watcher执行界面的更新。

这先建立一个简单的概念,Observer相当于作家,Watcher相当于读者(订阅者),现在是属于直接的关系。
但假如有新的Watcher订阅者时,怎么办呢?
最开始结构

这就还需要一个订阅器Dep,订阅器Dep是用来收集所有订阅者的。
Dep相当于是杂志社,Watcher作为订阅者,首先需要向杂志社订阅杂志,这样当有新的杂志(消息)产生时,Dep才会通知Watcher,如下图所示:
添加订阅器

// dep.js文件
function Dep(){    // 订阅器
    this.subs = []
}

Dep.prototype = {
    addSub(watcher){        // 添加订阅者
        this.subs.push(watcher)
    },
    
    notify(){    // 通知订阅者
        this.subs.forEach(watcher=>{
            watcher.run()
        })
    }
}

四、Dep订阅器如何使用

Dep主要的功能是添加新的订阅者,以及通知订阅者。
但什么时候添加订阅者,什么是通知订阅者呢?

- 当数据发生改变时,即set的时就要通知订阅者; [新杂志发布(数据更新set),这就是通知订阅者]
- 在获取数据时,即get时添加订阅者; [想要阅读杂志,这要先获取到杂志(获取数据get)]

杂志社和订阅者的示例,进行对比才知道为什么放在set或get方法中!
在获取的时(get)就需要加上条件限制,并不是所有获取的都是 添加订阅者操作。

// Observer.prototype中的defineProperty
defineProperty(data, key, val) { // set/get方法
    this.walk(val) // key 对应的又是 字典对象时
    
    // 订阅器
        var dep = new Dep()
    
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get() {
            if(条件){    // 符合某个条件说明是新的订阅者,才进行添加操作
                dep.addSub(watcher);
            }
            
            return val;
        },
        set(newVal) {
            if(val !== newVal) {
                console.log('新值: '+ newVal);
                val = newVal;
                
                // 通知订阅者
                dep.notify()
            }
        }
    })
}

五、Watcher订阅者

Watcher订阅者,在开始的时就要是添加到Dep订阅器中(杂志社当有新的杂志产生时,才知道通知谁。

其实也就是在Observer监听器的get方法中执行时,添加Watcher订阅者操作。但get方法会被调用多次,这就可以在Dep订阅器中添加一个Dep.target标识是否为新订阅者,添加成功后再将其去掉。
记住: 只有是新的订阅者,才是添加操作,否则不添加操作!

// watcher.js文件
// vm Vue的实例
// exp data中的key
// cb 回调函数
function Watcher(vm, exp, cb){
    this.vm = vm
    this.exp = exp
    this.cb = cb
    
    // 获取key对应的值,同时将watcher添加到Dep的队列中
    this.value = this.get() 
}

Watcher.prototype = {
    get(){    // 获取数据时,添加订阅者
        // 添加一个标识的意思
        Dep.target = this
        
        // 获取值,即触发get方法 [添加订阅者]
        var val = this.vm[this.exp]
        
        // 已经添加完成
        Dep.target = null
        
        return val
    },
    update(){    // 更新界面
        this.run()
    },
    run(){
        var val = this.vm[this.exp]
        
        if(val != this.value){    // 新值和旧值判断
            var oldValue = this.value
            this.value = val
            this.cb.call(this.vm, val, oldValue) // 回调函数
        }
    }
}

Observer监听器中,条件就有了,添加上即可:

// observer.js文件的 Observer.prototype
defineProperty(data, key, val) { 
    this.walk(val) // key 对应的又是 字典对象时
    
    // 订阅器
    var dep = new Dep()
    
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get() {    // Dep.target存储的就是Watcher实例
            if(Dep.target){    // 判断是否要添加(新订阅者) 
                dep.addSub(Dep.target)
            }
            
            return val;
        },
        set(newVal) {
            if(val !== newVal) {
                console.log('新值: '+ newVal)
                val = newVal
                
                // 通知订阅者
                dep.notify()
            }
        }
    })
}

基本流程操作,参考下图:
添加订阅者的流程

六、效果

从上图中可以看到,现阶段只是实现一个简单的从数据到视图的更新,后面我们再完善解析器Compiler(备注: 此时效果还是有问题的)。

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <body>
        <div id="app">
            <h3>名字: {{name}}</h3>
            <h3>技能: {{skill}}</h3>
        </div>

        <div>
            请输入你的名字:<input type="text" class="text" value="" />
            <input type="button" class="bt" value="确定" />
        </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 type="text/javascript">
            function Vue(options) {
                this.el = options.el // 元素
                this.data = options.data // 数据
                this.watcher = {} // 属性、数据、元素 的关联

                Object.keys(this.data).forEach(key => { // 数据代理
                    this.proxyKeys(key);
                })

                // 监听器
                new Observer(this.data);

                // 解析DOM
                this.compile()
            }

            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
                        }
                    })
                },

                compile() { // 解析DOM即更新数据
                    // 获取到对象绑定的元素
                    var ele = document.querySelector(this.el);

                    // 所有子元素
                    var childEls = ele.childNodes;

                    // 创建fragment
                    var fragment = document.createDocumentFragment();

                    // 获取到第一个子元素
                    var child = ele.firstChild;
                    while(child) {
                        // 将Dom元素移入fragment中
                        // appendChild: 如果被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除,然后再插入到新的位置
                        fragment.appendChild(child)
                        // 再获取第一个(其实就是下一个下一个的操作)
                        child = ele.firstChild
                    }

                    // 遍历所有子元素
                    [].slice.call(fragment.childNodes).forEach(el => {
                        var reg = /\{\{(.*)\}\}/;
                        var text = el.textContent;
                        if(reg.test(text)) {
                            var key = reg.exec(text)[1];
                            el.textContent = this.data[key];

                            // 新的订阅者
                            new Watcher(this, key, value=>{
                                    ele.innerHTML = value;
                                })
                        }
                    })

                    // 添加
                    ele.appendChild(fragment)
                }
            }

            // 创建Vue对象
            var myVue = new Vue({
                el: '#app',
                data: {
                    name: '阿童木',
                    skill: 'web前端开发',
                }
            })

            // 修改名字
            document.querySelector('.bt').onclick = function() {
                myVue.data.name = document.querySelector('.text').value
            }
        </script>
    </body>

</html>

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

楼主残忍的关闭了评论