MVVM的实现原理

1.MVVM是什么?

响应式,双向数据绑定,即MVVM。是指数据层(Model)-视图层(View)-数据视图(ViewModel)的响应式框架。它包括:

1.修改View层,Model对应数据发生变化。

2.Model数据变化,不需要查找DOM,直接更新View。

2.MVVM的实现方式

(1)发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value)

(2)脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,在指定的事件触发时进入脏值检测。

(3)数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

3.思路

1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

4、mvvm入口函数,整合以上三者

如下图:

MVVM的实现原理_第1张图片

4.实现Observer

写个简单的observer,监听对象的每一个属性的变化

	const data = {
        a: 1,
        b: 2,
        c: {
            d: 4
        }
    }
    observer(data)
    data.c.d = 5
    // 观察者,遍历监听所有数据
    function observer(data) {
        if(!data || typeof data!=='object')return
        for (let key in data) {
            observeProperty(data, key, data[key])
        }
    }
    // 观察属性变化
    function observeProperty(data, key, val) {
        observer(val) // 递归监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get() {
                console.log('you get it')
                return val
            },
            set(newval) {
                console.log('new:' + key, newval)
                console.log('old:' + key, 1)
                val = newval
            }
        })
    }

监听到变化之后,需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ...
// 观察属性变化
    function observeProperty(data, key, val) {
        const dep = new Dep()
        observer(val) // 递归监听子属性
        Object.defineProperty(data, key, {
            // ...
            set(newval) {
                if(val === newval)return
                console.log('new:' + key, newval)
                console.log('old:' + key, 1)
                val = newval
                dep.notify() // 通知所有订阅者
            }
        })
    }
// 订阅器类
    class Dep{
        constructor(){
            this.subs = [] // 存储所有订阅者,也就是Watcher
        }
        addSub(sub){ // 新增订阅者
            this.subs.push(sub)
        }
        notify(){ // 通知所有订阅者,更新数据
            this.subs.forEach(sub=>{
                sub.update()
            })
        }
    }

通过思路图可知,这里的订阅者是指Watcher。

// Observer.js
Object.defineProperty(data, key, {
    // ...
    get() {
        console.log('you get it')
        // 当Dep.target不为空时,添加当前watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val
    }
})

// Watcher.js
Watcher.prototype = {
    get(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。

5.实现Compile

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

MVVM的实现原理_第2张图片

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。

class Compile {
        constructor(el, vm) {
            this.$vm = vm
            this.$el = this.isElementNode(el) ? el : document.querySelector(el)
            if (this.$el) {
                // 把模板内容放入内存(片段)
                this.$fragment = this.node2Fragment(this.$el)
                // 解析模板
                this.init()
                // 把内存的结果返回页面
                this.$el.appendChild(this.$fragment)
            }
        }
        init() {
            this.compile(this.$fragment)
        }
        node2Fragment(el) { // 将节点解析成片段
            const fragment = document.createDocumentFragment()
            while (child = el.firstChild) {
                // fragment有个特点,它每appendChild一个节点,该节点在dom上被删除
                fragment.appendChild(child)
            }
            return fragment
        }
        compile(el) {
            const childNodes = el.childNodes;
            [].slice.call(childNodes).forEach((node) => {
                const text = node.textContent
                const reg = /\{\{(.*)\}\}/
                if (this.isElementNode(node)) { //属性节点
                    this.compileElement(node)
                } else if (this.isTextNode(node) && reg.test(text)) { //文本节点
                    this.compileText(node, RegExp.$1)
                }
                // 递归遍历编译子节点
                if (node.childNodes && node.childNodes.length) {
                    this.compile(node)
                }
            })
        }
        compileElement(node) {
            const nodeAttrs = node.attributes;
            [].slice.call(nodeAttrs).forEach(attr => {
                // 规定:指令以 v-xxx 命名
                // 如  中指令为 v-text
                const attrName = attr.name // v-text
                if (this.isDerective(attrName)) {
                    const exp = attr.value // content1
                    const dir = attrName.substring(2) // text
                    if (this.isEventDirective(dir)) {
                        // 事件指令, 如 v-on:click
                        compileUtil.eventHandler(node, this.$vm, exp, dir)
                    } else {
                        // 普通指令
                        compileUtil[dir] && compileUtil[dir](node, this.$vm, exp)
                    }
                }
            })
        }
        compileText(node, exp) {
            compileUtil.text(node, this.vm, exp)
        }
        isElementNode(node) {
            return node.nodeType === 1
        }
        isTextNode(node) {
            return node.nodeType === 3
        }
        isDerective(attrName) {
            return attrName.indexOf('v-') >= 0
        }
        isEventDirective(attrName) {
            return attrName.indexOf('on') >= 0
        }
    }

compile方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定。本文举text,model和事件注册三个例子来说明,更新函数代码如下:

const compileUtil = {
        text(node, vm, exp) {
            this.bind(node, vm, exp, 'text')
        },
        model(node, vm, exp) {
            this.bind(node, vm, exp, 'model')
            node.addEventListener('input',e=>{
                // 视图的变化更新到vm实例
                vm.$data[exp]= e.target.value
            })
        },
        bind(node, vm, exp, dir) {
            const updaterFn = updater[dir + 'Updater']
            // 初始化视图
            updaterFn && updaterFn(node, vm[exp])
            // 实例化订阅者,此操作会在对应的属性消息订阅器中添加该订阅者watcher
            new Watcher(vm, exp, (value, oldValue) => {
                updaterFn && updaterFn(node, value, oldValue)
            })
        },
        // 注册事件
        eventHandler(node,vm,exp,dir){
            // dir  on:click
            const eventType = dir.split(':')[1]
            const fn = vm.$options.methods && vm.$options.methods[exp]
            if(eventType&&fn){
                node.addEventListener(eventType,fn.bind(vm))
            }
        }
    }
    // 更新函数
    const updater = {
        textUpdater(node, value) {
            node.textContent = typeof value === 'undefined' ? '' : value
        },
        modelUpdater(node, value) {
            node.value = typeof value === 'undefined' ? '' : value
        }
    }

至此,视图和数据已经绑定成功,页面的初始化渲染由vm实例的属性初始化完成,并对节点上的事件进行了注册,接下了就是Watcher的事了。它将接受数据的变化,并重新更新视图。

其中,双向绑定由v-model实现,初始化时调用modelUpdater对input中的value初始化赋值,然后添加input事件监听,当视图层的数据变化后,更新vm实例上对应属性的值。

6.实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

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

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

3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

class Watcher {
        constructor(vm, exp, cb) {
            this.vm = vm
            this.exp = exp
            this.cb = cb
            this.value = this.get() // 缓存当前值(旧值)
        }
        get() {
            Dep.target = this // 将当前订阅者指向自己
            const value = this.vm[this.exp] // 触发getter,添加自己到属性订阅器中
            Dep.target = null // 添加完毕,重置
            return value
        }
        update() {
            this.run()
        }
        run() {
            const value = this.get() // 获取新值
            const oldValue = this.value
            if (value !== oldValue) {
                this.value = value
                this.cb.call(this.vm, value, oldValue) // 执行Compile中绑定的回调,更新视图
            }
        }
    }

// 这里再次列出Observer和Dep,方便理解
    Object.defineProperty(data, key, {
        get() {
            Dep.target && dep.addSub(Dep.target)
            return val
        }
    })
    Dep.prototype = {
        notify() {
            this.sbus.forEach(sub => sub.update())
        }
    }

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

7.实现MVVM

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

class MVVM {
        constructor(options) {
            this.$options = options
            this.$el = options.el
            this.$data = options.data
            // 属性代理,实现 vm.xxx -> vm._data.xxx
            for (let key in this.$data) {
                this.$proxy(key)
            }
            observer(this.$data)
            this.$compile = new Compile(this.$el || document.body, this)
        }
        $proxy(key) {
            Object.defineProperty(this, key, {
                configurable: false,
                enumerable: true,
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    this.$data[key] = newVal
                }
            })
        }
    }

参考文章:

Vue实现双向数据绑定

手写MVVM

Object的defineProperty

你可能感兴趣的:(vue)