vue采用数据劫持结合发布者订阅者模式的方式,通过es5中Object.defineproperty()来劫持各个属性的setter、getter,在数据变动时发布消息给依赖收集器,去通知观察者,触发响应回调,去更新视图。

将以上的描述用以下的图来展示:

         

 

实现分析,具体步骤:

第一步:需要Observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,给这个对象的某个值赋值,就会出发setter,那么就能监听到了数据变化

第二步:Complie解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步:watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:

  1.在自身实例化时往属性订阅器(Dep)里面添加自己

  2.自身必须有一个update()方法

  3.待属性变动Dep.notice()通知时,能调用自身的Update()方法,并触发compile中绑定的回调

第四步:MVVM作为数据绑定的入口,整合Observer、Compile、Watcher三者,通过Observer来监听自己的model数据变化,通过Complie来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的桥梁通信,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果

 
Complie 和 Watcher
  在哪里绑定关联两者: 在编译v-html等时创建Watcher,添加Watcher
Observer 和 Dep
  在哪里关联两者:在Observer获取数据的时候将Dep和Observer关联;
  在Observer数据更改时,告诉Dep通知变化

1.Compile

class Compile{
    constructor(el,vm){
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1. 获取文档碎片对象,放入内存中会减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el)
        // 2. 编译模板
        this.compile(fragment)
        // 3. 追加子元素到根元素
        this.el.appendChild(fragment)
    }
    // 判断是不是元素节点,元素节点nodeType为1
    isElementNode(node){
        return node.nodeType === 1;
    }
    // 获取文档碎片对象
    node2Fragment(el){
        // 创建文档碎片对象
        const f = document.createDocumentFragment()
        let firstChild;
        while(firstChild = el.firstChild){
            f.appendChild(firstChild);
        }
        return f
    }
    // 编译模板:渲染页面元素
    compile(fragment){
        const childNodes = fragment.childNodes;
        [...childNodes].forEach( child =>{
            if(this.isElementNode(child)){
                // 编译元素节点
                this.compileElement(child);
            }else{
                // 编译文本节点
                this.compileText(child);
            }
            // 如果节点有孩子节点则再次编译
            if(child.childNodes && child.childNodes.length){
                this.compile(child);
            }
        })
    }
    // 编译元素节点
    compileElement(node){
        const attributes = node.attributes;
        [...attributes].forEach(attr => {
            const {name, value} = attr;
            if(this.isDirective(name)){
                const [,directive] = name.split('-');
                const [dirName, eventName] = directive.split(':');
                // 更新视图
                compileUtil[dirName](node, value, this.vm, eventName)
                // 删除有指令的标签上的属性
                node.removeAttribute('v-'+ directive);
                if(directive === 'model'){
                    node.setAttribute('value',compileUtil.getVal(value, this.vm));
                }
            }else if(this.isEventName(name)){
                // 处理@click事件
                let [,eventName] = name.split('@')
                compileUtil['on'](node, value, this.vm, eventName)
                // 删除有指令的标签上的@事件
                node.removeAttribute('@'+ eventName);
            }else if(this.isAttr(name)){
                // 处理:属性
                let [,attrName] = name.split(':')
                compileUtil['bind'](node, value, this.vm, attrName)
                // 删除有指令的标签上的:属性
                node.removeAttribute(':'+ attrName);
            }
        });

    }
    // 编译文本节点
    compileText(node){
        const content = node.textContent;
        if(/\{\{(.+?)\}\}/.test(content)){
            compileUtil['text'](node,content,this.vm)
        }
    }
    // 判断是不是v-开头指令
    isDirective(attrName){
        return attrName.startsWith('v-');
    }
    // 判断是不是事件绑定简写@
    isEventName(attrName){
        return attrName.startsWith('@');
    }
    // 判断是不是属性绑定:
    isAttr(attrName){
        return attrName.startsWith(':');
    }
}

 

2.Compile工具集和Watcher

// 编译指令的方法集
const compileUtil = {
    // 获取value的值
    getVal(exp,vm){
        return exp.split('.').reduce((data,currentVal) => {
            return data[currentVal]
        },vm.$data)
    },
    getContentVal(exp,vm){
        return  exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(args[1],vm)
        })
    },
    setVal(exp,vm,inputVal){
        return exp.split('.').reduce((data,currentVal) => {
            data[currentVal] = inputVal
        },vm.$data)
    },
    // 处理v-text 和插值表达式{{}}
    text(node,exp,vm){
        let value;
        // 对v-text和插值表达式做区分处理
        if(exp.indexOf('{{') !== -1){
            // ????????????
            value = exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
                // 绑定观察者,将来数据发生变化 触发这里的回调 进行更新
                new Watcher(vm,args[1], () =>{
                    this.updater.textUpdate(node,this.getContentVal(exp,vm))
                })
                return this.getVal(args[1],vm)
            })
        }else{
            value = this.getVal(exp, vm)
        }
        this.updater.textUpdate(node,value)
    },
    // 处理v-html
    html(node,exp,vm){
        const value = this.getVal(exp, vm)
        // 添加数据观察者更新数据
        //1. Complie和updater关联,添加数据观察者
        new Watcher(vm,exp,(newVal) =>{
            this.updater.htmlUpdate(node,newVal)
        })
        // 初始话更新视图
        this.updater.htmlUpdate(node,value)
    },
    // 处理v-model 文本框
    model(node,exp,vm){
        const value = this.getVal(exp, vm)
        // 绑定更新函数,数据=》视图
        new Watcher(vm,exp,(newVal) =>{
            this.updater.modelUpdate(node,newVal)
        })
        // 视图 =》数据=》视图
        node.addEventListener('input', (e) =>{
            // 设置值
            this.setVal(exp,vm,e.target.value)
        })
        this.updater.modelUpdate(node,value)
    },
    // 处理事件绑定:v-bind和简写@
    on(node,exp,vm,eventName){
        let fn = vm.$options.methods && vm.$options.methods[exp]
        node.addEventListener(eventName,fn.bind(vm),false)
    },
    // 处理事件绑定: v-bind和简写:
    bind(node,exp,vm,attrName){
        const value = this.getVal(exp, vm)
        this.updater.attrUpdate(node,attrName,value)
    },
    // 更新视图(没有抽离出去)
    updater:{
        // text视图更新,设置text
        textUpdate(node,value){
            node.textContent = value
        },
        // html视图更新,设置innerHTML
        htmlUpdate(node,value){
            node.innerHTML = value
        },
        // model视图更新,设置文本框的value值
        modelUpdate(node,value){
            node.value = value
        },
        // 属性值视图更新,设置元素的属性值
        attrUpdate(node,attrName,value){
            node.setAttribute(attrName,value)
        }
    }
}

 

3.Observer

class Observer{
    constructor(data){
        this.observer(data)
    }
    observer(data){
        if(data && typeof data === 'object'){
            Object.keys(data).forEach( key => {
                this.defineReactive(data,key,data[key])
            })
        }
    }
    defineReactive(obj,key,value){
        // 递归遍历
        this.observer(value);
        // 实例化Dep
        const dep = new Dep()
        // 劫持并监听所有的属性
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:false,
            // 初始化时(编译之前)
            get(){
                // 订阅数据变化时,往dep中添加观察者
                // 获取数据的时候,将Dep 和 Observer关联
                Dep.target && dep.addSub(Dep.target);
                return value
            },
            set:(newVal) => {
                this.observer(newVal)
                if(newVal !== value){
                    value = newVal
                }
                // 告诉Dep通知变化
                dep.notify()
            }
        })
    }
}

4.Dep

class Dep{
    constructor(){
        this.subs = []
    }
    // 添加观察者
    addSub(watcher){
        this.subs.push(watcher)
    }
    // 通知观察者去更新视图
    notify(){
        console.log('通知观察者',this.subs)
        this.subs.forEach( w => {
            // console.log(w.update)
            w.update()
        })
    }
}

5.Watcher

class Watcher{
    constructor(vm,exp,cb){
        this.vm = vm;
        this.exp = exp;
        this.cb = cb;
        // 先把旧值保存起来
        this.oldVal = this.getOldVal()
    }
    getOldVal(){
        // 在拿到旧数据之前将Watcher和Dep关联起来
        Dep.target = this
        const oldVal = compileUtil.getVal(this.exp,this.vm)
        // 在拿到旧数据之后将观察者和dep关联取消,避免生成无数的watcher
        Dep.target = null
        return oldVal
    }
    update(){
        const newVal = compileUtil.getVal(this.exp,this.vm)
        if(newVal !== this.oldVal){
            this.cb(newVal)
        }
    }
}

 

6.Mvue

class MVue{
    constructor(options){
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if(this.$el){
            // 实现数据观察者
            new Observer(this.$data)
            // 实现指令解析器
            new Compile(this.$el, this)
            // 代理
            this.proxyData(this.$data)
        }
    }
    proxyData(data){
        for(const key in data){
            Object.defineProperty(this, key,{
                get(){
                    return data[key]
                },
                set(){
                    data[key] = newVal
                }
            })
        }
    }
}

 

7.index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./Mvue.js"></script>
    <script src="./Observer.js"></script>
</head>
<body>
    <div id="app">
        <h2>{{person.name}}</h2>
        <h2>{{person.name}} -- {{person.age}}</h2>
        <ul>
            <li>{{msg}}</li>
            <li v-text='person.name'></li>
            <li v-text='msg'></li>
            <li v-html='htmlStr'>2</li>
            <li v-html='person.htmlStr'></li>
        </ul>
        <img v-bind:src="img" alt=" ">
        <img :src="img" alt=" ">
        <div>
            <a :href="aLink">百度</a>
            <a v-bind:href="aLink">百度</a>
        </div>
        <input v-model='msg' @click='clickFun' />
        <button v-on:click='clickFun'>点击事件</button>
    </div>
    <script>
        let vm new MVue({
            el: '#app',
            data:{
                    person:{
                        name: 'hzz',
                        age: 18,
                        fav: 'code',
                        htmlStr: '<h2>这是个html片段person</h2>',
                    },
                    msg: '这是消息',
                    htmlStr: '<h2>这是个html片段</h2>',
                    img:'https://dss3.baidu.com/-rVXeDTa2gU2pMbgoY3K/it/u=3139850293,1719705775&fm=202&src=608&crossm&mola=new&crop=v1',
                    aLink:'https://www.baidu.com'
            },
            methods:{
                clickFun(){
                    console.log(this)
                }
            }
        })
    </script>
    
</body>
</html>

 

页面应用展示组成

  1.Mvue.js文件包含(编译指令方法集、complie、Mvue)

  2.observer.js文件中包含 (Watcher、Dep)

  3.index.html

posted on 2021-07-08 15:28  薇依  阅读(137)  评论(0编辑  收藏  举报