defineProperty 详解

251 阅读2分钟

defineProperty 2.0 (3.0 proxy)

vue 是如何做的?

html: 
<div id="app">
    {{message}}
</div>


js:
let vm = new Vue({
    el: '#app',
    data: {
        message: '测试数据'
    }
})
console.log(vm._data)

setTimeout(() => {
    vm._data.message = 'update'
}, 2000);

2s后:

几个问题:

数据是怎么渲染到视图的?数据变了视图怎么变化?插值表达式要怎么实现?

Object.defineProperty
  • 拿返回值:创建
  • 修改
// obj

let obj = {
    name:"张安"
}
console.log(obj);

// 用defineProperty劫持过的obj
// 获取obj.name 触发get

let obj = {
    name:"张安"
}
Object.defineProperty(obj,"name",{
    configurable: true, // 是否可删除 默认false
    enumerable: true, // 可枚举的属性 默认false
    get(){
        console.log("get...");
        return "张安"
    },
    set(newValue){
        console.log("set...",newValue)
    }
})
console.log(obj);
console.log(obj.name);

// 设置属性,触发set

let obj = {
    name:"张安"
}
Object.defineProperty(obj,"name",{
    configurable:true, // 是否可删除 默认false
    enumerable:true, // 可枚举的属性 默认false
    get(){
        console.log("get...");
        return "张安"
    },
    set(newValue){
        console.log("set...",newValue)
    }
})
obj.name = "王五"

用自定义事件实现上述问题

“数据是怎么渲染到视图的?数据变了视图怎么变化?插值表达式要怎么实现?”

<!-- 
    1. 数据渲染到视图
        - 在根结点(#app)作用域内查找所有节点,寻找符合条件的{{message}}
        - 正则匹配出满足条件的{{ }}
        - 拿到message 在data中找到message的值,替换#app 中的{{message}}

    2. 数据更新到视图
        - Object.defineProperty 劫持 data
        - EventTarget、dispatchEvent、addEventListener
        - 新值替换旧值
 -->
<div id="app">
    <div>

    </div>
    <div>
    <!--递归遍历所有的message-->
        {{message}}
    </div>
    <!-- 表达式 -->
    {{message}}
</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: 123,
            mess: 456
        }
    })
    // 加了定时器,数据变化,才能劫持到
    setTimeout(()=>{
        vm.$data.message = "修改的数据"
    },1000)
</script>
class Vue extends EventTarget{
    constructor (options){
        super()
        this.$options = options
        this.$el = this.$options.el
        this.$data = this.$options.data
        this.observer(this.$data)
        this.complie()
    }
    observer(data){
        for(let key in data) {
            let val = data[key]; 
            let that = this
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    // console.log('get')
                    // 在外层定义一个val = data[key]而不是在这里直接 return data[key] 是因为:
                    // 获取数据会触发get 从而造成死循环
                    return val
                },
                set(newVal) {
                    // console.log('set')
                    let event = new CustomEvent(key, {
                        detail: newVal
                    })
                    // 经测试,this指向会发生改变指向obj
                    that.dispatchEvent(event)
                    val = newVal
                }
            })
        }
    }
    complie() {
        let ele = document.querySelector(this.$options.el)
        this.complieNodes(ele)
    }
    complieNodes(ele) {
        let childs = ele.childNodes
        let childList = [...childs].filter(v => {
            return v.nodeType == 3 || v.nodeType == 1
        });
        
        childList.forEach((v) => {
            if(v.nodeType == 3) {
            // 文本节点
                let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
                
                // console.log(reg.test(v.textContent))
                // 这样写会有问题,RegExp对象有一个lastIndex的属性。如果使用了全局修饰符,那么执行test方法后,lastIndex就会记录匹配的字符串在原始字 符串中最后一位的索引加一。
                // 如果没有发现匹配,lastIndex置为0。当下次再执行时,对给定的字符串匹配不是从开头位置,而是要依据lastIndex提供的位置
                // 会造成结果反复的问题
                
                if(reg.test(v.textContent)){
                    let $1 = RegExp.$1
                    v.textContent = v.textContent.replace(v.textContent, this.$data[$1]);
                    this.addEventListener($1, e=>{
                        let oldValue = this.$data[$1]
                        let newValue = e.detail
                        let reg = new RegExp(oldValue)
                        v.textContent = v.textContent.replace(reg, newValue)
                    })
                }
            } else if(v.nodeType == 1) {
                // 递归
                if(v.childNodes.length > 0) {
                    this.complieNodes(v)
                }
            }
        })
    }
}

修改前:

修改后:

// 修改成订阅发布后

<div id="app">
    <div>
        {{mess}}
    </div>
    <!-- <div v-text="message" class="box"> -->
    <!-- </div> -->
    <!-- 表达式 -->
    {{message}}

    <hr>
    <!-- <input type="text" v-model="test"> -->
    {{test}}
    <hr>
    {{message}}

</div>

<script>
    let vm = new Vue({
        el: '#app',
        data: {
            message: '历史的值',
            mess: 'mess',
            test: '123'
        }
    })
    // console.log(vm)
    setTimeout(()=>{
        vm.$data.message = "修改的数据"
    },1000)
</script>

class Vue{
    constructor (options){
        // super()
        this.$options = options
        this.$el = this.$options.el
        this.$data = this.$options.data
        this.observer(this.$data)
        this.complie()
    }
    observer(data){
        for(let key in data) {
            let val = data[key];
            // let that = this
            let dep = new Dep()
            console.log(dep)
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    if(Dep.target) {
                        dep.addSub(Dep.target)
                        // console.log(dep)
                    }
                    // console.log(dep)
                    return val
                },
                set(newValue) {
                    dep.notify(newValue)
                    // console.log('set')
                    // let event = new CustomEvent(key, {
                    //     detail: newVal
                    // })
                    // that.dispatchEvent(event)
                    val = newValue
                    // console.log(event)
                }
            })
        }
    }
    complie() {
        let ele = document.querySelector(this.$options.el)
        this.complieNodes(ele)
    }
    complieNodes(ele) {
        let childs = ele.childNodes
        let childList = [...childs].filter(v => {
            return v.nodeType == 3 || v.nodeType == 1
        });
        
        childList.forEach(v => {
            if(v.nodeType == 3) {
                let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/;

                // 这样写会有问题,RegExp对象有一个lastIndex的属性。如果使用了全局修饰符,那么执行test方法后,lastIndex就会记录匹配的字符串在原始字 符串中最后一位的索引加一。
                // 如果没有发现匹配,lastIndex置为0。当下次再执行时,对给定的字符串匹配不是从开头位置,而是要依据lastIndex提供的位置
                // 会造成结果反复的问题

                // console.log(reg.test(v.textContent))

                if(reg.test(v.textContent)){
                    let $1 = RegExp.$1
                    v.textContent = v.textContent.replace(v.textContent, this.$data[$1]);
                    // this.addEventListener($1, e=>{
                    //     let oldValue = this.$data[$1]
                    //     let newValue = e.detail
                    //     let reg = new RegExp(oldValue)
                    //     v.textContent = v.textContent.replace(reg, newValue)
                    // })

                    // 实例化watcher, 重新编译模版
                    new Watcher(this.$data, $1, (newValue) => {
                        let oldValue = this.$data[$1]
                        let reg = new RegExp(oldValue)
                        v.textContent = v.textContent.replace(reg, newValue)
                    })
                }
            } else if(v.nodeType == 1) {

                let attrs = v.attributes;

                [...attrs].forEach(attr => {
                    let attrName = attr.name
                    let attrValue = attr.value

                    if(attrName == 'v-text') {
                        v.innerText = this.$data[attrValue]
                    } else if(attrName == 'v-model') {
                        v.value = this.$data[attrValue];

                        v.addEventListener('input', e => {
                            this.$data[attrValue] = e.target.value
                        })
                    }
                });
                // 递归
                if(v.childNodes.length > 0) {
                    this.complieNodes(v)
                }
            }
        })
    }
}

// 依赖收集器
class Dep {
    constructor() {
        this.subs = []
    }
    // 收集订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    notify(newValue) {
        this.subs.forEach(sub => {
            sub.update(newValue)
        })
    }
}

// 订阅者
class Watcher {
    constructor(data, key, cb) {
        Dep.target = this
        data[key]
        this.cb = cb

        Dep.target = null
    }
    update(newValue) {
        // console.log('update...', newValue)
        this.cb(newValue)
    }
}