资讯专栏INFORMATION COLUMN

试着用Proxy 实现一个简单mvvm

fnngj / 2536人阅读

摘要:套数据,实现界面先把计算属性这个注释掉,后面进行实现计算属性然后在函数中增加一个编译函数,号表示是添加的函数添加一个编译函数上面我们添加了一个的构造函数。

Proxy、Reflect的简单概述
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
出自阮一峰老师的ECMAScript 6 入门,详细点击http://es6.ruanyifeng.com/#docs/proxy

例如:

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2
var proxy = new Proxy(target, handler);

这里有两个参数,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API

Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

同样也放上阮一峰老师的链接http://es6.ruanyifeng.com/#docs/reflect

初始化结构

看到这里,我就当大家有比较明白Proxy(代理)是做什么用的,然后下面我们看下要做最终的图骗。

看到上面的图片,首先我们新建一个index.html,然后里面的代码是这样子滴。很简单




    
    简单版mvvm


开发语言:{{language}}

组成部分:

  • {{makeUp.one}}
  • {{makeUp.two}}
  • {{makeUp.three}}

描述:

{{describe}}

计算属性:{{sum}}

看到上面的代码,大概跟vue长得差不多,下面去实现Mvvm这个构造函数

实现Mvvm这个构造函数

首先声明一个Mvvm函数,options当作参数传进来,options就是上面代码的配置,里面有eldatacomputed~~

function Mvvm(options = {}) {
    // 把options 赋值给this.$options
    this.$options = options
    // 把options.data赋值给this._data
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
    return this._vm
}

上面Mvvm函数很简单,就是把参数options 赋值给this.$options、把options.data赋值给this._data、然后调用初始化initVm函数,并用call改变this的指向,方便initVm函操作。然后返回一个this._vm,这个是在initVm函数生成的。

下面继续写initVm函数,

function initVm () {
    this._vm = new Proxy(this, {
        // 拦截get
        get: (target, key, receiver) => {
            return this[key] || this._data[key] || this._computed[key]
        },
        // 拦截set
        set: (target, key, value) => {
            return Reflect.set(this._data, key, value)
        }
    })
    return this._vm
}

这个init函数用到Proxy拦截了,this对象,生产Proxy实例的然后赋值给this._vm,最后返回this._vm

上面我们说了,要使得Proxy起作用,必须针对Proxy实例。

在代理里面,拦截了getsetget函数里面,返回this对象的对应的key的值,没有就去this._data对象里面取对应的key,再没有去this._computed对象里面去对应的key值。set函数就是直接返回修改this._data对应key

做好这些各种拦截工作。我们就可以直接从实力上访问到我们相对应的值了。(mvvm使我们第一块代码生成的实例)

mvvm.b // 2
mvvm.a // 1
mvvm.language // "Javascript"

如上图看控制台。可以设置值,可以获取值,但是这不是响应式的。

打开控制台看一下

可以详细的看到。只有_vm这个是proxy,我们需要的是,_data下面所有数据都是有拦截代理的;下面我们就去实现它。

实现所有数据代理拦截

我们首先在Mvvm里面加一个initObserve,如下

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
+   initObserve.call(this, data) // 初始化data的Observe
    return this._vm
}

initObserve这个函数主要是把,this._data都加上代理。如下

function initObserve(data) {
    this._data = observe(data) // 把所有observe都赋值到 this._data
}

// 分开这个主要是为了下面递归调用
function observe(data) {
    if (!data || typeof data !== "object") return data // 如果不是对象直接返回值
    return new Observe(data) // 对象调用Observe
}

下面主要实现Observe类

// Observe类
class Observe {
    constructor(data) {
        this.dep = new Dep() // 订阅类,后面会介绍
        for (let key in data) {
            data[key] = observe(data[key]) // 递归调用子对象
        }
        return this.proxy(data)
    }
    proxy(data) {
      let dep = this.dep
      return new Proxy(data, {
        get: (target, key, receiver) => {
          return Reflect.get(target, key, receiver)
        },
        set: (target, key, value) => {
          const result = Reflect.set(target, key, observe(value)) // 对于新添加的对象也要进行添加observe
          return result  
        }
      })
    }
  }

这样子,通过我们层层递归添加proxy,把我们的_data对象都添加一遍,再看一下控制台

很不错,_data也有proxy了,很王祖蓝式的完美。

看到我们的html的界面,都是没有数据的,上面我们把数据都准备好了,下面我们就开始把数据结合到html的界面上。

套数据,实现hmtl界面

先把计算属性这个html注释掉,后面进行实现

然后在Mvvm函数中增加一个编译函数,➕号表示是添加的函数

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
+   new Compile(this.$options.el, vm) // 添加一个编译函数
    return this._vm
}

上面我们添加了一个Compile的构造函数。把配置的el作为参数传机进来,把生成proxy的实例vm也传进去,这样子我们就可以拿到vm下面的数据,下面我们就去实现它。顺序读注释就可以了,很好理解

// 编译类
class Compile {
    constructor (el, vm) {
        this.vm = vm // 把传进来的vm 存起来,因为这个vm.a = 1 没毛病
        let element = document.querySelector(el) // 拿到 app 节点
        let fragment = document.createDocumentFragment() // 创建fragment代码片段
        fragment.append(element) // 把app节点 添加到 创建fragment代码片段中
        this.replace(fragment) // 套数据函数
        document.body.appendChild(fragment) // 最后添加到body中
    }
    replace(frag) {
        let vm = this.vm // 拿到之前存起来的vm
        // 循环frag.childNodes
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent // 拿到文本 例如:"开发语言:{{language}}"
            let reg = /{{(.*?)}}/g // 定义匹配正则
            if (node.nodeType === 3 && reg.test(txt)) {
            
                replaceTxt()
                
                function replaceTxt() {
                    // 如果匹配到的话,就替换文本
                    node.textContent = txt.replace(reg, (matched, placeholder) => {
                        return placeholder.split(".").reduce((obj, key) => {
                            return obj[key] // 例如:去vm.makeUp.one对象拿到值
                        }, vm)
                    })
                }
            }
            // 如果还有字节点,并且长度不为0 
            if (node.childNodes && node.childNodes.length) {
                // 直接递归匹配替换
                this.replace(node)
            }
        })
    }
}

上面的编译函数,总之就是一句话,千方百计的把{{xxx}}的占位符通过正则替换成真实的数据。

然后刷新浏览器,铛铛档铛铛档,就出现我们要的数据了。

很好很好,但是我们现在的数据并不是改变了 就发生变化了。还需要订阅发布和watcher来配合,才能做好改变数据就发生变化了。下面我们先实现订阅发布。

实现订阅发布

订阅发布其实是一种常见的程序设计模式,简单直白来说就是:

把函数push到一个数组里面,然后循环数据调用函数。

例如:举个很直白的例子

let arr = [] 
let a = () => {console.log("a")}

arr.push(a) // 订阅a函数
arr.push(a) // 又订阅a函数
arr.push(a) // 双订阅a函数

arr.forEach(fn => fn()) // 发布所有

// 此时会打印三个a

很简单吧。下面我们去实现我们的代码

// 订阅类
class Dep {
    constructor() {
        this.subs = [] // 定义数组
    }
    // 订阅函数
    addSub(sub) {
        this.subs.push(sub)
    }
    // 发布函数
    notify() {
        this.subs.filter(item => typeof item !== "string").forEach(sub => sub.update())
    }
}

订阅发布是写好了,但是在什么时候订阅,什么时候发布??这时候,我们是在数据获取的时候订阅watcher,然后在数据设置的时候发布watcher,在上面的Observe类里面里面,看➕号的代码。 .

... //省略代码
...
proxy(data) {
    let dep = this.dep
    return new Proxy(data, {
        // 拦截get
        get: (target, prop, receiver) => {
+           if (Dep.target) {
                // 如果之前是push过的,就不用重复push了
                if (!dep.subs.includes(Dep.exp)) {
                    dep.addSub(Dep.exp) // 把Dep.exp。push到sub数组里面,订阅
                    dep.addSub(Dep.target) // 把Dep.target。push到sub数组里面,订阅
                }
+           }
            return Reflect.get(target, prop, receiver)
        },
        // 拦截set
        set: (target, prop, value) => {
            const result = Reflect.set(target, prop, observe(value))
+           dep.notify() // 发布
            return result  
        }
    })
}

上面代码说到,watcher是什么鬼?然后发布里面的sub.update()又是什么鬼??

带着一堆疑问我们来到了watcher

实现watcher

看详细注释

// Watcher类
class Watcher {
    constructor (vm, exp, fn) {
        this.fn = fn // 传进来的fn
        this.vm = vm // 传进来的vm
        this.exp = exp // 传进来的匹配到exp 例如:"language","makeUp.one"
        Dep.exp = exp // 给Dep类挂载一个exp
        Dep.target = this // 给Dep类挂载一个watcher对象,跟新的时候就用到了
        let arr = exp.split(".")
        let val = vm
        arr.forEach(key => {
            val = val[key] // 获取值,这时候会粗发vm.proxy的get()函数,get()里面就添加addSub订阅函数
        })
        Dep.target = null // 添加了订阅之后,把Dep.target清空
    }
    update() {
        // 设置值会触发vm.proxy.set函数,然后调用发布的notify,
        // 最后调用update,update里面继续调用this.fn(val)
        let exp = this.exp
        let arr = exp.split(".")
        let val = this.vm
        arr.forEach(key => {
            val = val[key]
        })
        this.fn(val)
    }
}

Watcher类就是我们要订阅的watcher,里面有回调函数fn,有update函数调用fn,

我们都弄好了。但是在哪里添加watcher呢??如下代码

在Compile里面

...
...
function replaceTxt() {
    node.textContent = txt.replace(reg, (matched, placeholder) => {
+       new Watcher(vm, placeholder, replaceTxt);   // 监听变化,进行匹配替换内容
        return placeholder.split(".").reduce((val, key) => {
            return val[key]
        }, vm)
    })
}

添加好有所的东西了,我们看一下控制台。修改发现果然起作用了。

然后我们回顾一下所有的流程,然后看见古老(我也是别的地方弄来的)的一张图。

帮助理解嘛

响应式的数据我们都已经完成了,下面我们完成一下双向绑定。

实现双向绑定

看到我们html里面有个v-module绑定了一个language,然后在Compile类里面的replace函数,我们加上

replace(frag) {
    let vm = this.vm
    Array.from(frag.childNodes).forEach(node => {
        let txt = node.textContent
        let reg = /{{(.*?)}}/g
        // 判断nodeType
+       if (node.nodeType === 1) {
            const nodeAttr = node.attributes // 属性集合
            Array.from(nodeAttr).forEach(item => {
                let name = item.name // 属性名
                let exp = item.value // 属性值
                // 如果属性有 v-
                if (name.includes("v-")){
                    node.value = vm[exp]
                    node.addEventListener("input", e => {
                        // 相当于给this.language赋了一个新值
                        // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新操作
                        vm[exp] = e.target.value
                    })
                }
            });
+       }
        ...
        ...
    }
  }

上面的方法就是,让我们的input节点绑定一个input事件,然后当input事件触发的时候,改变我们的值,而值的改变会调用setset中又会调用notifynotify中调用watcherupdate方法实现了更新操作。

然后我们看一下,界面

双向数据绑定我们基本完成了,别忘了,我们上面还有个注释掉的计算属性。

计算属性

先把

计算属性:{{sum}}

注释去掉,以为上面一开始initVm函数里面,我们加了这个代码return this[key] || this._data[key] || this._computed[key],到这里大家都明白了,只需要把this._computed也加一个watcher就好了。

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
    initObserve.call(this, data)
+   initComputed.call(this) // 添加计算函数,改变this指向
    new Compile(this.$options.el, vm)
    return this._vm
}


function initComputed() {
    let vm = this
    let computed = this.$options.computed // 拿到配置的computed
    vm._computed = {}
    if (!computed) return // 没有计算直接返回
    Object.keys(computed).forEach(key => {
        // 相当于把sum里的this指向到this._vm,然后就可以拿到this.a、this、b
        this._computed[key] = computed[key].call(this._vm)
        // 添加新的Watcher
        new Watcher(this._vm, key, val => {
            // 每次设置的时候都会计算
            this._computed[key] = computed[key].call(this._vm)
        })
    })
}

上面的initComputed 就是添加一个watcher,大致流程:

this._vm改变 ---> vm.set() ---> notify() -->update()-->更新界面

最后看看图片

一切似乎没什么毛病~~~~

添加mounted钩子

添加mounted也很简单

// 写法和Vue一样
let mvvm = new Mvvm({
    el: "#app",
    data: {
        ...
        ...
    },
    computed: {
        ...
        ...
    },
    mounted() {
        console.log("i am mounted", this.a)
    }
})

在new Mvvm里面添加mounted,
然后到function Mvvm里面加上

function Mvvm(options = {}) {
    this.$options = options
    let data = this._data = this.$options.data
    let vm = initVm.call(this)
    initObserve.call(this, data)
    initComputed.call(this)
    new Compile(this.$options.el, vm)
+   mounted.call(this._vm) // 加上mounted,改变指向
    return this._vm
}

// 运行mounted
+ function mounted() {
    let mounted = this.$options.mounted
    mounted && mounted.call(this)
+ }

执行之后会打印出

i am mounted 1

完结~~~~撒花

ps:编译里面的,参考到这个大神的操作。@chenhongdong,谢谢大佬

最后附上,源代码地址,直接下载运行就可以啦。

源码地址:https://github.com/naihe138/proxy-mvvm

预览地址:http://gitblog.naice.me/proxy-mvvm/index.html

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95880.html

相关文章

  • 说说PHP框架的MVC架构

    摘要:前言在说架构之前,先说说框架吧。在架构中就是这个转接头。当一个新框架诞生后,关注点从学习这个框架,慢慢变成了这个框架是如何设计的,解决什么样的问题。前几年使用过各种框架,小到,大到。 前言 在说 MVC 架构之前,先说说PHP框架吧。很多很多学完PHP语言的人,面对的就是PHP各种各样的框架。什么TP啊、Yii啊、CI啊,还有很流行的laravel啊等等。 他们的大部分都会说自己是基于...

    waltr 评论0 收藏0
  • 使用 Proxy 实现简单MVVM 模型

    摘要:绑定实现的历史绑定的基础是事件。但脏检查机制随之带来的就是性能问题。是谷歌对于简化双向绑定机制的尝试,在中引入。挣扎了一段时间后谷歌团队宣布收回的提议,并在中完全删除了实现。自然全军覆没其他各大浏览器实现的时间也较晚。 绑定实现的历史 绑定的基础是 propertyChange 事件。如何得知 viewModel 成员值的改变一直是开发 MVVM 框架的首要问题。主流框架的处理有一下三...

    BetaRabbit 评论0 收藏0
  • 使用 Proxy 实现简单MVVM 模型

    摘要:绑定实现的历史绑定的基础是事件。但脏检查机制随之带来的就是性能问题。是谷歌对于简化双向绑定机制的尝试,在中引入。挣扎了一段时间后谷歌团队宣布收回的提议,并在中完全删除了实现。自然全军覆没其他各大浏览器实现的时间也较晚。 绑定实现的历史 绑定的基础是 propertyChange 事件。如何得知 viewModel 成员值的改变一直是开发 MVVM 框架的首要问题。主流框架的处理有一下三...

    MarvinZhang 评论0 收藏0
  • Windows上利用Python自动切换代理IP的终极方案!

    摘要:在中,格式是,所以需要把格式统一为注册表的标准。注册表的二进制值及关键信息如下开关长度地址是否跳过本地代理地址通过在中导入文件的方式执行并立即生效。本代码可以根据需要自动设置代理。 声明下:不同于网络上千百篇方法,下文是经过各种严格测试都通过的,同时也是一个实验的过程,排除了各种不靠谱的方法。有需要的可以评论来讨论,想要源码和相关参考文献或笔记的,也可以找我。 思路及启发 先说一下我这...

    jeyhan 评论0 收藏0

发表评论

0条评论

fnngj

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<