vue2框架简易版响应式设计(观察者模式)

对于vue.js中的属性值我们要格外关注:

$attrs 获取当前传递的参数

$listeners 获取当前组件的自定义事件

$children 获取当前组件所有子组件

$parent 获取当前组件所有父组件

$options 获取当前vue实例参数信息

$refs 获取ref所有的引用节点

设计原则:单一职责,一个类或一个函数,只做一件事。

数据定义后页面更新

页面数据切换操作data数据

添加数据劫持方法

// Vue.js 
//Observer专门用于数据劫持
class Observer {
    data;
    constructor(data) {
        this.data = data
        this.walk()
    }

    defineProperty(data, key, value) {
        Object.defineProperty(data, key, {
            get() {
                console.log(`使用了${key}这个属性`);
                return value
            },
            set(val) {
                console.log(`修改了${key}属性`, val);
                value = val
            }
        })
    }
    walk() {
        Object.keys(this.data).forEach(el => {
            this.defineProperty(user, el, this.data[el])
        })
    }
}

const user = {
    username:'xiaowang',
    age:11
}

new Observer(user)
console.log(user.username);

对于data来说,我们自己数据劫持存了一份在$data,并且还将$data中每个数据都再次存在了vue实例上,创建Vue类。

class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data()
        this.$el = options.el
        //$data上的所有数据都要数据劫持
        new Observer(this.$data)
        //$data存放所有的数据
        //会将$data的数据挂并挂载到this身上
        this.proxy()
    }
    proxy() {
        Object.keys(this.$data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key]
                },
                set(val) {
                    this.$data[key] = val
                }
            })
        })
    }
}

进行模版渲染,这里仅仅渲染一层,如果多层可以执行递归。

//模版渲染
class Compile{
    constructor(el,data){
        this.$el = document.querySelector(el)
        this.$data = data
        this.compiler()
    }
    compiler(){
        [...this.$el.children].forEach(item=>{
            if(/\{\{([a-zA-z0-9]+)\}\}/.test(item.innerHTML)){
                const key =RegExp.$1.trim()
                item.innerHTML=this.$data[key]
            }
        })
    }
}

观察者模式

观察者模式是一种设计模式,就是一种代码规范。有两个非常重要的元素:发布者和订阅者。一个发布者可能对应多个订阅者。

对应到我们的代码,使用{{}}的标签,意味着是我们需要标记的标签,作为订阅者,在项目中提供一个发布者,一旦数据发生变化,发布者通知订阅者更新页面。

//订阅者
class Watcher {
    constructor(callback) {
        Dep.target = this
        this.callback = callback
        this.update()
        Dep.target = null
    }
    update() {
        //这一步并不是直接修改,而是更新虚拟dom,这里只是简化了
        this.callback()
    }
}

//发布者
class Dep {
    constructor() {
        this.subs = []
    }
    notify() {
        this.subs.forEach(item => {
            item.update()
        })
    }
}

于此同时,在set get中需要收集wacher以及通知watcher更新数据,那么也要修改,这里有个难点,就是在数据劫持的时候怎么获取对应的watcher,这里我们采用在watcher生成实例时,将watcher挂载到发布者身上,在获取属性的劫持过程中,使用target属性,那么就生成一个watcher,并将他进行收集,在修改属性的时候,可以使用notify的方法,通知watcher使用update方法更新对应的render函数进行渲染。

Object.defineProperty(data, key, {
            get() {
                if (Dep.target) { //只收集编译时的watcher
                    //依赖收集,使用这个属性就生成一个watcher
                    dep.subs.push(Dep.target)
                }
                return value
            },
            set(val) {
                //一旦数据更新,Dep通知watcher更新
                value = val
                dep.notify()
            }
        })

此刻,我们简易的响应式就算完成了。

这里的执行顺序比较绕,先调用数据劫持,然后调用compile,每个有{{}}的标签生成watcher,并传入render函数,在watcher的constructor中挂到发布者dep的身上,执行update,render函数得以执行,然后会触发数据劫持get方法,dep的subs进行收集watcher,同时update结束后,清除挂载在dep上的watcher。修改数据时,使用notify通知watcher然后调用update方法,触发render方法,同时继续触发get方法获取数据,但是这个时候,就不会再继续获取wacher,因为整个watcher的生成只有一次,就是在第一次编译的时候。

数据嵌套模式:

如果涉及到多层数据嵌套,我们需要递归对象属性数据,在render方法中我们要对数据进行处理,获取到对应的值。

思路:

1.渲染函数需要渲染对应的值

2.对象数据内部的数据进行响应式处理

//模版渲染
class Compile {
    constructor(el, data) {
        this.$el = document.querySelector(el)
        this.$data = data
        this.compiler()
    }
    compiler() {
        [...this.$el.children].forEach(item => {
            //修改正则表达式匹配任何内容
            if (/\{\{(\S+)\}\}/.test(item.innerHTML)) {
                const key = RegExp.$1.trim()
                //根据.获取层级内容arr
                let arr = key.split('.')
                //实际上vue底层不是直接innerHTML
                const render = () => {
                    let value = this.$data
                    arr.forEach((el, index) => {
                        if (arr.length == 1) {
                            value = this.$data[el]
                        } else {
                            //大于一层的要调递归方法一层一层取数据
                            value = this.getData(value, el)
                        }
                    })
                    item.innerHTML = value
                }
                new Watcher(render)
            }
        })
    }
    getData(obj, key) {
        return obj[key]
    }
}
defineProperty(data, key, value) {
        const dep = new Dep()
        //如果属性值为对象,那么对象属性进行遍历做响应式
        if (typeof data[key] == 'object') {
            Object.keys(data[key]).forEach(el => {
                this.defineProperty(data[key], el, data[key][el])
            })
        }

        Object.defineProperty(data, key, {
            get() {
                if (Dep.target) { //只收集编译时的watcher
                    //依赖收集,使用这个属性就生成一个watcher
                    dep.subs.push(Dep.target)
                }
                return value
            },
            set(val) {
                //一旦数据更新,Dep通知watcher更新
                value = val
                dep.notify()
            }
        })
    }

附完整代码:

// Observer专门用于数据劫持
class Observer {
    data;
    constructor(data) {
        this.data = data
        this.walk()
    }

    defineProperty(data, key, value) {
        const dep = new Dep()
        //如果属性值为对象,那么对象属性进行遍历做响应式
        if (typeof data[key] == 'object') {
            Object.keys(data[key]).forEach(el => {
                this.defineProperty(data[key], el, data[key][el])
            })
        }

        Object.defineProperty(data, key, {
            get() {
                if (Dep.target) { //只收集编译时的watcher
                    //依赖收集,使用这个属性就生成一个watcher
                    dep.subs.push(Dep.target)
                }
                return value
            },
            set(val) {
                //一旦数据更新,Dep通知watcher更新
                value = val
                dep.notify()
            }
        })
    }
    walk() {
        Object.keys(this.data).forEach(el => {
            this.defineProperty(this.data, el, this.data[el])
        })
    }
}

class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data()
        this.$el = options.el
        //$data上的所有数据都要数据劫持
        new Observer(this.$data)
        //$data存放所有的数据
        //会将$data的数据挂并挂载到this身上
        this.proxy(this.$data)
        new Compile(this.$el, this.$data)
        console.log(this);

    }
    proxy(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(val) {
                    data[key] = val
                }
            })
        })
    }
}


//模版渲染
class Compile {
    constructor(el, data) {
        this.$el = document.querySelector(el)
        this.$data = data
        this.compiler()
    }
    compiler() {
        [...this.$el.children].forEach(item => {
            //修改正则表达式匹配任何内容
            if (/\{\{(\S+)\}\}/.test(item.innerHTML)) {
                const key = RegExp.$1.trim()
                //获取层级内容arr
                let arr = key.split('.')
                const render = () => {
                    let value = this.$data
                    arr.forEach((el, index) => {
                        if (arr.length == 1) {
                            value = this.$data[el]
                        } else {
                            //大于一层的要循环一层一层取数据
                            value = this.getData(value, el)
                        }
                    })
                    //实际上vue底层不是直接innerHTML
                    item.innerHTML = value
                }
                new Watcher(render)
            }
        })
    }
    getData(obj, key) {
        return obj[key]
    }
}
//订阅者
class Watcher {
    constructor(callback) {
        Dep.target = this
        this.callback = callback
        this.update()
        Dep.target = null
    }
    update() {
        //这一步并不是直接修改,而是更新虚拟dom,这里只是简化了
        this.callback()
    }
}

//发布者
class Dep {
    constructor() {
        this.subs = []
    }
    notify() {
        this.subs.forEach(item => {
            item.update()
        })
    }
}
<!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">
        <p>{{username}}</p>
        <p>{{age}}</p>
        <p>{{obj.c.x}}</p>
        <p>{{obj.a}}</p>
    </div>
</body>
<script src="./vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data() {
            return {
                username: 'xiaoming',
                age: 10,
                obj:{
                    a:123,
                    b:456,
                    c:{
                        x:0
                    }
                }
            }
        }
    })

</script>

</html>

另一个版本(包含指令处理):

 

function defineReactive(obj, key, value) {
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        get() {
            Dep.target && dep.add(Dep.target)
            return value
        },
        set(val) {
            observe(val)
            value = val
            dep.notify()
        }
    })
}
//动态添加响应式属性
function set(obj, key, val) {
    defineReactive(obj, key, val)

}
function observe(obj) {
    if (typeof obj != 'object' || obj == null) {
        return obj
    }
    //没出现一个对象,就创建一个Ob实例
    new Observer(obj)
}
//判断传入obj类型,做对应的响应式处理
class Observer {
    constructor(obj) {
        this.value = obj

        if (Array.isArray()) {

        } else {
            this.walk(obj)
        }
    }
    //对象响应式
    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key]

            },
            set(val) {
                vm.$data[key] = val
            }
        })
    })
}
class KVue {
    constructor(options) {
        //保存选项
        this.$options = options
        this.$data = options.data
        this.data = this.$data
        //响应式处理
        observe(this.$data)
        //代理data到kVue实例
        proxy(this)
        //编译
        new Compile(options.el, this)
    }
}

class Compile {
    //el宿主 vm kvue实例
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el)
        this.compile(this.$el)
    }
    compile(el) {
        //遍历 dom树
        el.childNodes.forEach(node => {
            if (this.isElement(node)) {
                this.compileElement(node)
                //需要处理属性和子节点,递归子节点
                if (node.childNodes && node.childNodes.length > 0) {
                    this.compile(node)
                }
            } else if (this.isInter(node)) {
                this.compileText(node)
            }
        })
    }
    isElement(node) {
        return node.nodeType === 1
    }
    //满足{{}}的标签
    isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
    isDir(attr) {
        console.log(attr);
        return attr.startsWith("k-")
    }
    update(node, exp, dir) {
        const fn = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])

        new Watcher(this.$vm, exp, function (val) {
            fn && fn(node, val)
        })
    }
    //渲染对应的值到页面
    compileText(node) {
        this.update(node, RegExp.$1, 'text')
    }
    textUpdater(node, val) {
        node.textContent = val
    }
    //处理元素所有动态属性
    compileElement(node) {
        Array.from(node.attributes).forEach(attr => {
            //有name和value
            const attrName = attr.name
            const exp = attr.value

            //判断是否是一个指令
            if (this.isDir(attrName)) {
                //执行指令的处理函数
                //k-text,关心text
                const dir = attrName.substring(2)
                this[dir] && this[dir](node, exp)
            }
        })
    }
    // k-text的处理函数
    text(node, exp) {
        this.update(node, exp, 'text')
    }
    html(node, exp) {
        this.update(node, exp, 'html')
    }
    htmlUpdater(node, val) {
        node.innerHTML = val
    }
}

//做dom更新
class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm
        this.key = key
        this.updateFn = updateFn
        //读取一下key的值触发get,从而收集依赖
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
    }
    update() {
        this.updateFn.call(this.vm, this.vm[this.key])
    }
}
//依赖:和响应式对象的key一一对应
class Dep {
    constructor() {
        this.deps = []
    }
    add(dep) {
        this.deps.push(dep)
    }
    notify() {
        this.deps.forEach(dep => dep.update())
    }
}
  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Goat恶霸詹姆斯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值