手写简易版vue响应式

411 阅读3分钟

手写简易版vue响应式

vue的响应式实现原理:

vue2中利用object.definePeoperty()实现变更检测。

实现原理分析:

  1. new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中

  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在

    Compile中

  3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

  4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个

    Watcher

  5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

image-20210901103030992.png

核心实现:

KVue:框架构造函数

Observer:执行数据响应化(分辨数据是对象还是数组)

Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)

Watcher:执行更新函数(更新dom)

Dep:管理多个Watcher,批量更新

Kvue.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>{{counter}}</p>
        <p k-text="text"></p>
        <p k-html="desc"></p>
    </div>
    <script src="kvue.js"></script>
    <script src="compile.js"></script>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                counter : 1,
                text: 'text值',
                desc: "<span style='color:red'>kvue可还行?</span>"
            }
        })

        setInterval(() => {
            app.counter++
        },1000)
    </script>
</body>
</html>

K-vue.js

// 创建kvue的构造函数

// 响应式
function defineRective(obj, key, val) {
    // 递归
    // observe里面会做判断,不是对象就return
    observe(val)

    // 创建一个Dep和当前key一一对应
    const dep = new Dep()

    // 对传入的obj进行访问拦截
    Object.defineProperty(obj, key, {
        get() {
            console.log('get ' + key)
            // 依赖收集
            Dep.target && dep.addDep(Dep.target)
            return val
        },
        // 闭包会将旧值的状态进行保存
        set(newVal) {
            if(newVal !== val) {
                console.log('set ' + key + ":" + newVal)
                // 如果传入的newVal依然是obj,需要做响应化处理
                observe(newVal)
                val = newVal

                // 通知更新
                // watcher.forEach(w => w.update())
                dep.notify()
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        // 希望传入的是obj
        return
    }

    // 创建Observer(执行数据响应化,分辨数据是对象还是数组)
    new Observer(obj)
}

//代理韩束,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
    // vm[sourceKey]就是vm[$data]
    Object.keys(vm[sourceKey]).forEach(key => {
        // 将$data中的[key]代理到vm属性中
        Object.defineProperty(vm, key, {
            get() {
                return vm[sourceKey][key]
            },
            set(newVal) {
                vm[sourceKey][key] = newVal
            }
        })
    })
}

class Vue {
    constructor(options) {
        // 保存选项
        this.$options = options
        this.$data = options.data

        // 响应化处理
        observe(this.$data)

        // 代理
        proxy(this, '$data')

        // 创建编译器
        new Compile(options.el, this)
    }
}
// 根据对象的类型决定如何响应化
class Observer {
    constructor(value) {
        this.value = value

        // 判断其类型
        if (typeof value === 'object') [
            this.walk(value)
        ]
    }
    
    // 对象数据响应化
    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineRective(obj, key, obj[key])
        })
    }

    // 数组对象的响应化
}

// 观察者:保存更新函数,值发生变化调用更新函数
// const watcher = []
class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm
        this.key = key
        this.updateFn = updateFn

        // watcher.push(this)

        // Dep.target静态属性上设置为当前watcher实例
        Dep.target = this
        this.vm[this.key]   // 读取触发getter
        Dep.target = null   // 收集完置空
    }

    update() {
        this.updateFn.call(this.vm, this.vm[this.key])
    }
}

// Dep:依赖,管理某个key相关所以Watcher实例
class Dep {
    constructor() {
        this.deps = []
    }

    addDep(dep) {
        this.deps.push(dep)
    }

    notify() {
        console.log(this.deps)
        this.deps.forEach(dep => dep.update())
    }
}

compile.js

// 编译器
// 递归便利dom树
// 判断节点类型,如果是文本,则判断是否是插值绑定
// 如果是元素,则遍历其属性判断是否是指令或者是事件,然后递归子元素
class Compile {
    // el是宿主元素
    // vm是kVue实例
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el)

        if (this.$el) {
            // 执行编译
            this.compile(this.$el)
        }
    }

    compile(el) {
        // 遍历el树
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
            // 判断是否是元素
            if (this.isElement(node)) {
                console.log('编译元素' + node.nodeName);
                this.compileElement(node)
            } else if (this.isInter(node)) {
                console.log('编译插值绑定' + node.textContent);
                this.compileText(node)

            }


            // 递归子节点
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        });
    }

    isElement(node) {
        return node.nodeType === 1
    }

    isInter(node) {
        // 首先是文本标签,其次内容是{{xxx}}
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }

    compileText(node) {
        this.updata(node, RegExp.$1, 'text')
    }

    compileElement(node) {
        // 节点是元素
        // 遍历其属性列表
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach(attr => {
            // 规定:指令以k-xx="oo"定义
            const attrName = attr.name // k=xx
            const exp = attr.value  // oo
            if (this.isDirective(attrName)) {
                const dir = attrName.substring(2)   // xx
                // 执行指令 dir是截取的text方法
                this[dir] && this[dir](node, exp)
            }
        })
    }

    isDirective(attr) {
        return attr.indexOf('k-') === 0
    }

    updata(node, exp, dir) {
        // 指令对应的更新函数xxUpdater
        const fn = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])

        // 更新处理,封装一个更新函数,可以更新对应dom元素
        new Watcher(this.$vm, exp, function(val) {
            fn && fn(node, val)
        })
    }

    textUpdater(node, value) {
        node.textContent = value
    }

    // k-text
    text(node, exp) {
        this.updata(node, exp, 'text')
    }
    // k-html
    html(node, exp) {
        this.updata(node, exp, 'html')
    }

    htmlUpdater(node,value) {
        node.innerHTML = value
    }

}

defProps.js

// 响应式
function defineRective(obj, key, val) {
    // 递归
    // observe里面会做判断,不是对象就return
    observe(val)

    // 对传入的obj进行访问拦截
    Object.defineProperty(obj, key, {
        get() {
            console.log('get ' + key)
            return val
        },
        // 闭包会将旧值的状态进行保存
        set(newVal) {
            if(newVal !== val) {
                console.log('set ' + key + ":" + newVal)
                // 如果传入的newVal依然是obj,需要做响应化处理
                observe(newVal)
                val = newVal
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        // 希望传入的是obj
        return
    }

    Object.keys(obj).forEach(key => {
        defineRective(obj, key, obj[key])
    })
}

function set(obj, key, val) {
    defineRective(obj, key, val)
}


// defineRective(obj, 'foo', 'foo')
// obj.foo
// obj.foo = 'foooooooooooooooo'

const obj = {foo: 'foo', bar:'bar', baz: {a:1}, arr:[1,2,3]}

//遍历做响应化处理
observe(obj)

obj.foo
obj.foo = 'fooooooooo'
obj.bar
obj.bar = 'barrrrrr'

// baz里面的a不行,没有递归
obj.baz.a = 10
// 传进来的新值是对象,不会做响应化处理
obj.baz = {a: 100}
obj.baz.a = 10000000
// 添加/删除新的属性无法检测

// obj.dong = 'dong'
set(obj, 'dong', 'dong')
obj.dong
obj.dong = 'dongggggggggg'

// object.defineProperty()对数组无效
// 分析:改变数组的方法只有七个
// 解决方案:替换数组实例的原型方法
obj.arr.push(4)