Vue的MVVM响应式原理(双向数据绑定)源码解析(附面试题)

MVVM

MVVM是Model-View-ViewModel的简写,将视图 UI 和业务逻辑分开
ViewMode:也就是mvc 的控制器,取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑

源码

MVue.js

import {Observer, Watcher} from './Observer'

//编译类
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)
    }

    //编译
    compile(fragment){
        //1.获取子节点
        const childNodes = fragment.childNodes;
        [...childNodes].forEach( child => {
            // console.log(item)
            if(this.isElementNode(child)){
                //元素节点
                //编译元素节点
                // console.log('元素节点'+child.tagName)
                this.compileElement(child)
            }else{
                //文本节点
                //编译文本节点
                // console.log('文本节点'+child)
                this.compileText(child)
            }

            if(child.childNodes && child.childNodes.length){
                this.compile(child)
            }
        })
    }

    //编译元素节点
    compileElement(node){
        //取得属性
        const attrs = node.attributes
        //通过转换成数组来遍历属性
        let arr = [...attrs]
        arr.forEach(attr => {
            const {name, value} = attr
            //进行v-开头判定 true就是指令
            if(this.isDirective(name)){
                //v-text->text
                const [,dirctive] = name.split('-')
                //对on:click继续->click  而如果为text 就放在第一个参数
                //v-bind:src
                const [dirName,eventName] = dirctive.split(':')
                //对不同类型的属性进行赋值到节点上  更新数据  数据驱动视图
                compileUtil[dirName](node,value,this.vm,eventName)
                //删除有指令的标签的属性
                node.removeAttribute('v-'+ dirctive)
                
            }
            //@click='handle'
            else if(this.isEventName(name)){
                let [,eventName] = name.split('@')
                compileUtil['on'](node, value, this.vm, eventName)
            }
            //:class='red'
            else if(this.isEllipsisName(name)){
                let [, propsName] = name.split(':')
                compileUtil['bind'](node, value, this.vm, propsName)
            }
        });
    }

    //编译文本节点
    compileText(node){
        //获得文本
        const content = node.textContent
        //通过正则只取{{}}的文本
        const expr = /\{\{(.+?)\}\}/
        if(expr.test(content)){
            compileUtil['text'](node,content,this.vm)
        }
    }

    //@开头
    isEventName(attrName){
        return attrName.startsWith('@')
    }

    //:开头
    isEllipsisName(attrName){
        return attrName.startsWith(':')
    }

    //判断是否是以v-开头
    isDirective(attrName){
        return attrName.startsWith('v-')
    }

    //创建文档碎片对象
    node2Fragment(el){
        const fragment = document.createDocumentFragment()
        let firstChild;
        while(firstChild = el.firstChild){
            fragment.appendChild(firstChild)
        }
        return fragment
    }

    //判断是不是节点对象
    isElementNode(node){
        return node.nodeType === 1 ; 
    }
}

//编译工具类
export const compileUtil = {
    //node 节点  expr 属性指向的数据名  vm vue实例对象用于去的data数据  event  事件属性
    //解决 person.name类型
    getVal(expr,vm){
        //[person, name] data 之前的值  curr 当前值
        //执行过程  先将curr取到 person 将person对象付给data  再将curr取到name  就获得person.name
        return expr.split('.').reduce((data,currentVal) => {
            // console.log(currentVal)
            return data[currentVal]
        },vm.$options.data)
    },
    setVal(expr,vm,inputVal){
        return expr.split('.').reduce((data,currentVal) => {
            // console.log(currentVal)
            data[currentVal] = inputVal
        },vm.$options.data)
    },
    getContentVal(expr,vm){
        let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {                
            return this.getVal(args[1],vm)
        })
        return value
    },
    //处理文本
    text(node, expr, vm){
        let value
        //处理{{}}文本
        if(expr.indexOf('{{')!== -1){
            //取得{{}}里的内容 并调getval取得data的数据
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {                
                new Watcher(vm, args[1], () => {
                    this.updater.textUpdater(node,this.getContentVal(expr,vm))
                })
                return this.getVal(args[1],vm)
            })
        }
        //处理v-text
        else{
            value = this.getVal(expr, vm)
        }
        //数据渲染到视图
        this.updater.textUpdater(node,value)
    },
    html(node, expr, vm){
        let value = this.getVal(expr,vm)
        //绑定watcher 对数据监听 更改数据时,绑定对应更新的函数
        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)
    },
    //event 响应事件函数
    on(node, expr, vm, event){
        let fn = vm.$options.methods && vm.$options.methods[expr]
        node.addEventListener(event,fn.bind(vm),false)
    },
    //props 属性src等
    bind(node, expr, vm, props){
        const value = this.getVal(expr,vm)
        this.updater.bindUpdater(node,props,value)
    },
    //更新函数类
    updater:{
        textUpdater(node,value){
            node.textContent = value
        },
        htmlUpdater(node,value){
            node.innerHTML = value
        },
        modelUpdater(node,value){
            node.value = value
        },
        bindUpdater(node,props,value){
            node.setAttribute(props,value)
        }
    }
}

//Vue类
export class MVue{
     constructor(options){
         this.$el = options.el
         this.$data = options.$data
         this.$options = options
         if(this.$el){
             //1.实现数据观察者
             new Observer(this.$options.data)
             //2.实现指令解析器
             new Compile(this.$el,this)
             this.proxyData(this.$options.data)
         }
     }

     proxyData(data){
         for(const key in data){
             Object.defineProperty(this, key, {
                get(){
                    return data[key]
                 },
                 set(newVal){
                     data[key] = newVal
                 }
             })
         }
     }
 }

// export default MVue

Observer.js

import {compileUtil} from './MVue'

//数据观察者类
export class Observer{
    constructor(data){
        this.observer(data)
    }

    observer(data){
        if(data && typeof data === 'object'){
            //遍历data中的属性
            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:false,
            get(){
                //订阅数据发生变化时,往dep中添加观察者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            //当我修改数据时,会进到set  然后进行监听 然后进行值的更新
            set: (newValue) => {
                //对于新值进行监听
                this.observer(newValue)
                if(newValue !== value){
                    value = newValue
                }
                //告诉dep变化
                //进行通知
                dep.notify()
            },
        })
    }
        
}

//数据依赖器
class Dep{
    constructor(){
        this.subs = [] 
    }
    //收集观察者
    addSub(watcher){
        this.subs.push(watcher)
    }
    //通知观察者更新
    notify(){
        //拿到观察者 然后更新
        this.subs.forEach(w => {
            w.update()
        })
    }
}


//观察数据
export class Watcher{

    constructor(vm,expr,cb){
        this.vm = vm
        this.expr = expr
        this.cb = cb
        //先保存旧值
        this.oldVla = this.getOldVal() 
    }

    getOldVal(){
        //未初始化  挂载在dep
        Dep.target = this
        let oldVal = compileUtil.getVal(this.expr, this.vm)
        Dep.target = null
        return oldVal
    }

    //在watcher中拿到新值  callback
    update(){
        let newVal = compileUtil.getVal(this.expr, this.vm)
        if(newVal !== this.oldVla){
            this.cb(newVal)
        }
    }
}

// module.exports = {
//     Observer, 
//     Watcher
// }

index.js

import './index.less';
import {MVue} from './MVue'

const vm = new MVue({
    el:'#app',
    data:{
        person:{
            name:'xa',
            age:18, 
        },
        msg:'MVVM',
        color:'red'
    },
    methods:{
        handel(){
            this.msg= '学习MVVM'
        }
    }
})

index.html

<!DOCTYPE html>
<html lang="en">

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

<body>
	<div id="app">
		<h2>{{person.name}}</h2>
		<h3 v-html='person.age'></h3>
		<h3 v-html='msg'></h3>
		<div v-text='msg'></div>
		<div v-text='person.name'></div>
		<input type="text" v-model='msg'>
		<button v-on:click='handel'>2222</button>
		<button @click='handel'>@@@</button>
		<ul>
			<li v-bind:class='color'>{{msg}}</li>
			<li :class='color'>{{msg}}</li>
			<li>{{msg}}</li>
		</ul>
	</div>
</body>
</html>

上面文件通过webpack进行打包

面试题

阐述一下你所理解vue的MVVM响应式原理
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果

在这里插入图片描述

视频讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值