面试题:

你所理解的MVVM响应式原理

简单版本:

MVVM即模型、视图、视图模型。模型指的是后端传递的数据。视图指的是所看到的页面。视图模型mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:一是将模型转化成视图,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将视图转化成模型,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。这两个方向都实现的,我们称之为数据的双向绑定。

如果是第一次看到这样的话,肯定一脸茫然。上面很多概念都没有解析清楚,怎么实现的数据绑定双向绑定
代码是最简洁直白的,接下来所要做的就是通过实现代码去理解MVVM响应式原理。
Vue响应式原理.png

MVVM Demo

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3 v-bind="person.child.name">{{msg}}</h3>
        <div v-text="msg"></div>
        <div v-html="htmlStr"></div>
        <input type="text" v-model="msg">
        <button v-on:click="ShowMsg">123</button>
    </div>
<script src="./CrazyVue.js"></script>
<script>
    let vm = new CrazyVue({
        el: '#app',
        data: {
            person: {
                name: '法外狂徒张三',
                age: 28,
                fav: "唱跳Rap",
                child: {
                    name: "sadsa"
                }
            },
            msg: '学习MVVM响应式原理',
            htmlStr: '<p>Vue真香阿</p>'
        },
        methods: {
            ShowMsg(){
                console.log(this.$data)
                this.person.name = '学习Vue'
            }
        }
    })
</script>
</body>
</html>
入口函数
class CrazyVue{
    constructor(options){
        this.$el = options.el
        this.$data = options.data
        this.$options = options
        if(this.$el){
            // 1.实现一个数据观察者
            new Observer(this.$data)
            // 2.实现一个指令解析器
            new Compile(this.$el,this)
            this.proxyData(this.$data)
        }
    }
    /*
        proxy:this.$data.person => this.person
    */
    proxyData(data){
        for(const key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                },
                set(newVal){
                    data[key] = newVal
                }
            })
        }
    }
}

在这里实现一个数据观察者和指令解析器以及对$data数据的代理,先分析指令解析器Compile做了些什么,
我们猜它对模版中含有v-标签以及{{}}这样的模版指令进行初始化模版渲染,在初始化的时候添加观察者以及数据改动触发的回调函数。

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)
    }   
    /*
        <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-text="msg"></div>
        <div v-html="htmlStr"></div>
        <input type="text" v-model="msg">
    */
    compile(fragment){
        // 1.获取子节点
        const childNodes = fragment.childNodes;
        [...childNodes].forEach(child => {
            if(this.isElementNode(child)){
                // 是元素节点
                // 编译元素节点
                // console.log("元素节点",child)
                this.compileElement(child)
            }else{
                // 文本节点
                // 编译文本节点
                // console.log("文本节点",child)
                this.compileText(child)
            }
            if(child.childNodes && child.childNodes.length){
                this.compile(child)
            }
        })
    }
    compileElement(node){
        // <div v-text="msg"></div>
        const attributes = node.attributes;
        [...attributes].forEach(attr=> {
            const {name,value} = attr
            if(this.isDirective(name)){
                // 是一个指令 v-text v-model v-html v-on:click
                const [,directive] = name.split('-') // text model html on:click
                const [dirName,eventName] = directive.split(":") // test model html on
                // 更新数据 数据驱动视图
                compileUtil[dirName](node,value,this.vm,eventName)

                // 删除有指令的标签上的属性
                node.removeAttribute('v-'+directive)
            }else if(this.isEventName(name)){
                let [,eventName] = name.split('@')
                compileUtil['on'](node,value,this.vm,eventName)
            }
        })
    }
    compileText(node){
        // <h3>{{msg}}</h3>
        const content = node.textContent
        if(/\{\{(.*?)\}\}/.test(content)){
            compileUtil['text'](node,content,this.vm)
        }
    }
    node2Fragment(el){
        // 创建文档碎片
        const f = document.createDocumentFragment()
        let firstChild
        while(firstChild = el.firstChild){
            f.appendChild(firstChild)
        }
        return f
    }
    isEventName(attrName){
        return attrName.startsWith("@")

    }
    isDirective(attrName){
        return attrName.startsWith("v-")
    }
    isElementNode(node){
        return node.nodeType === 1
    }
}

Compile实现功能

  • 获取DOM树,将DOM树写入文档碎片对象,减少页面的回流和重绘
  • 解析模版,对元素节点、文本节点进行不同的模版渲染,对于含有子节点的DOM进行递归
  • 追加子元素到根元素
const compileUtil = {
    getVal(expr,vm){
        // {person,name}
        return expr.split('.').reduce((data,currentVal)=>{
            return data[currentVal]
        },vm.$data);
    },
    getContentVal(expr,vm){
        expr.replace(/\{\{(.*?)\}\}/g,(...args)=> {
            return this.getVal(args[1],vm)
        })
    },
    setVal(expr,vm,inputVal){
        return expr.split('.').reduce((data,currentVal)=>{
            data[currentVal] = inputVal
        },vm.$data);
    },
    text(node,expr,vm){ // expr:msg
        let value 
        if(expr.indexOf('{{') !== -1){
            // {{person.name}}--{{person.age}}
            value = expr.replace(/\{\{(.*?)\}\}/g,(...args)=> {

                // 绑定观察者,将来数据发生变化,触发这里的回调函数
                new Watcher(vm,expr,()=>{
                    this.updater.textUpdater(node,this.getContentVal(expr,vm))
                })
                return this.getVal(args[1],vm)
            })
        }else{
            const value = this.getVal(expr,vm)
        }
        this.updater.textUpdater(node,value)
    },
    html(node,expr,vm){
        const value = this.getVal(expr,vm)
        new Watcher(vm,expr,(newVal)=>{
            this.updater.htmlUpdater(node,newVal)
        })
        this.updater.htmlUpdater(node,value)
    },
    model(node,expr,vm){
        const value = this.getVal(expr,vm)
        // 绑定更新函数 数据 => 视图
        new Watcher(vm,expr,(newVal)=>{
            this.updater.modelUpdater(node,newVal)
        })
        // 视图 => 数据 => 视图
        node.addEventListener('input',(e)=>{
            // 设置值
            this.setVal(expr,vm,e.target.value)
        
        })
        this.updater.modelUpdater(node,value)
    },
    on(node,expr,vm,eventName){
        let fn = vm.$options.methods && vm.$options.methods[expr]
        node.addEventListener(eventName,fn.bind(vm),false)
    },
    bind(node,expr,vm){
        const value = this.getVal(expr,vm)
        console.log(expr)
        if(/(.+?).(.+?)/.test(expr)){
            keys = []
            expr.split('.').reduce((data,currentVal)=>{
                keys.push(currentVal)
            });
            console.log(keys.slice(-1))
            expr = keys.slice(-1)
        }
        this.updater.bindUpdater(node,expr,value)
    },
    updater:{
        textUpdater(node,value){
            node.textContent = value
        },
        htmlUpdater(node,value){
            node.innerHTML = value
        },
        modelUpdater(node,value){
            node.value = value
        },
        bindUpdater(node,expr,value){
            node.setAttribute(expr,value); 
        }
    }
}

针对不同的指令调用指定的函数进行渲染模版,在初始化的时候添加观察者,将来数据发生变化的时候,触发这里的回调函数

Observer
class Observer {
    constructor(data){
        this.data = data 
    }
    observer(data){
        /*
        {
            person: {
                name: '张三',
                fav: {
                    dance: {
                        a: '小苹果'
                    }
                }
            }
        }
        */
        if(data && typeof data === "object"){
            Object.keys(data).forEach(key => {
                this.defineReactive(data,key,data[key])
            })
        }
    }
    defineReactive(obj,key,value){
        // 递归遍历
        this.observer(value)
        const dep = new Dep()
        // 劫持并监听所有的属性
        Object.defineProperty(obj,key,{
            enumerable: true,
            configurable: true,
            get(){
                // 初始化
                // 订阅数据变化时,往Dep中添加观察者
                dep.addSub(Dep.target)
                return value
            },
            set:(newVal)=>{
                this.observer(newVal)
                if(newVal !== value){
                    value = newVal
                }
                // 告诉dep通知变化
                dep.notify()
            }
        })
    }
}
Dep
class Dep{
    constructor(){
        this.subs = []
    }
    // 收集观察者
    addSub(watcher){
        this.subs.push(watcher)
    }
    // 通知观察者去更新
    notify(){
        this.subs.forEach(w=>w.update)
    }
}
Watcher
class Watcher{
    constructor(vm,expr,cb){
        this.vm = vm
        this.expr = expr
        this.cb = cb
        // 先把旧值保持起来
        this.oldVal = this.getOldVal()
    }
    getOldVal(vm,expr){
        Dep.target = this
        const oldVal = compileUtil.getVal(this.expr,this.vm)
        Dep.target = null
        return oldVal
    }
    update(){
        const newVal = compileUtil.getVal(this.expr,this.vm)
        if(newVal !== this.oldVal){
            this.cb(newVal)
        }
    }
}

Observer判断传进来的data数据是否是对象类型,对对象的每个key进行数据劫持监听所有的属性,对于嵌套的对象进行递归遍历,创建了Dep去存储所有的观察者,当初始化的时候向Dep当中添加观察者,在改变属性的时候对新的属性创建观察者并告诉dep通知变化

阐述一下你所理解的MVVM响应式原理

vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时,发布消息给依赖收集器,去通知观察者,做出对应的回调函数,去更新视图
MVVM作为绑定的入口,整合Observer,Compile,Watcher三者,通过Observer监听model数据变化,通过Compile解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通信桥梁,达到数据变化->视图更新,视图交换变化->数据model变更的双向数据绑定

写在最后

问渠那得清如许
唯有源头活水来


闲鱼
0 声望1 粉丝

潇洒自在行路人)逃