vue响应式原理学习

231 阅读2分钟

vue响应式

Snipaste_2021-05-12_11-30-19.jpg

简单实现

my-vue/reactive.js

const obj = {}
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', val)
            return val
        },
        set(newVal) {
            val = newVal
            console.log('set', val)
        }
    })
}
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'fooooooooo'

综合视图实现

监听数据后,数据一更新,在set属性中通知视图更新 (my-vue/ractive.html)

<div id="app"></div>
<script>
    const obj = {}

    function defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
            get() {
                // console.log('get', val)
                return val
            },
            set(newVal) {
                if (newVal != val) {
                    val = newVal
                    // 通知视图更新
                    update()
                }
            }
        })
    }
    defineReactive(obj, 'foo', '')
    obj.foo = new Date().toLocaleTimeString()

    function update() {
        app.innerText = obj.foo
    }
    setInterval(() => {
        obj.foo = new Date().toLocaleTimeString()
    }, 1000)
</script>

对象嵌套不能监听数据

递归处理

// 数组处理
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
methods.forEach(method => {
    arrayProto[method] = function() {
        originalProto[method].apply(this, arguments)
        console.log('执行了:' + method + '操作')
    }
})
function defineReactive(obj, key, val) {
    observe(val)
}
function observe(obj) {
    // 递归判断
    if(typeof obj != 'object' || obj == null) {
        return
    }
    if (Array.isArray(obj)) {
        obj.__proto__ = arrayProto
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            observe(obj[i])
        }
    } else {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}
const obj = {foo: 'foo',bar: {b: {c: 2}},baz:{a: 1}, arr: [1, 2, 3]}
observe(obj)
obj.baz.a = 2 
obj.bar.b.c = 3
obj.arr.push(4) // push操作
obj.arr // [1, 2, 3, 4]

往对象中添加新数据,无法监听该数据

重新监听对象新增的数据

function set(obj, key, val) {
    defineReactive(obj, key, val)
}
set(obj, 'k', 3)
obj.k

测试用例

<meta charset="UTF-8">
<div id="app">
    <p @click="add">{{counter}}</p>
    <p my-html="desc"></p>
    <input type="text" my-model="desc">
</div>
<script src="./my-vue.js"></script>
<script>
    const app = new myVue({
        el: '#app',
        data: {
            counter: 1,
            desc: '<span>222</span>'
        },
        methods: {
            add() {
                this.counter++
            }
        }
    })
</script>

框架构造:执行初始化

  • 执⾏初始化,对data执⾏响应化处理
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) return
    new Observer(obj)
}

function defineReactive(obj, key, val) {

}

class myVue {
    constructor(options) {
        this.$options = options
        this.$data = options.data
        // 监听数据
        observe(this.$data)
    }
}

class Observer {
    constructor(value) {
        this.value = value
        this.walk(value)
    }
    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}
  • data做代理vue组件中直接调用data内的数值:例如this.counter,但是这样的调用需要我们做一层代理,即this.data做代理 vue 组件中直接调用data内的数值:例如this.counter,但是这样的调用需要我们做一层代理,即this.data[counter] ——> this.counter
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key]
            },
            set(newVal) {
                vm.$data[key] = newVal
            }
        })
    })
}

class myVue {
    constructor(options) {
        // 为$data做代理
        proxy(this)
    }
}

编译 - Compile

编译模板中vue模板特殊语法,初始化视图、更新视图,v-html、v-text、@event...

  • 根据节点类型编译
class myVue {
    constructor(options) {
        // 编译
        new Compile(options.el, this)
    }
}
class Compile {
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el)
        if (this.$el) {
            this.compile(this.$el)
        }
    }
    compile(el) {
        const childNodes = el.childNodes
        Array.from(childNodes).forEach(node => {
            if (this.isElement(node)) {
                // console.log('编译元素'+node.nodeType)
                this.compileElement(node)
            } else if (this.isInterpolation(node)) {
                // console.log('编译插值文本' + node.nodeType)
                this.compileText(node)
            }
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }
    isElement(node) {
        return node.nodeType == 1
    }
    isInterpolation(node) {
        return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
    compileText(node) {
        // node.textContent = this.$vm[RegExp.$1]
        this.update(node, RegExp.$1, 'text')
    }
    compileElement(node) {
        let nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach(attr => {
            // my-text="xxx"
            // name = my-text,value = xxx
            let attrName = attr.name
            let exp = attr.value
            if (this.isDirective(attrName)) {
                let dir = attrName.substring(3)
                this[dir] && this[dir](node, exp)
            }
            // 事件@
            if (this.isEvent(attrName) {
                let dir = attrName.sunstring(1)
                this.eventHandler(node, exp, dir)
            }
        })
    }
    isDirective(attr) {
        return attr.indexOf('my-') == 0
    }
    isEvent(attr) {
        return attr.startsWith('@')
    }
    eventHandler(node, exp, dir) {
        const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
        node.addEventListener(dir, fn.bind(this.$vm))
    }
    text(node, exp) {
        // node.textContent = this.$vm[exp]
        this.update(node, exp, 'text')
    }
    html(node, exp) {
        // node.innerHTML = this.$vm[exp]
        this.update(node, exp, 'html')
    }
    model(node, exp) {
        this.update(node, exp, 'model')
        // 事件监听
        node.addEventListener('input', event => {
            this.$vm[exp] = event.target.value
        })
    }
    textUpdater(node, val) {
        node.textContent = val
    }
    htmlUpdater(node, val) {
        node.innerHTML = val
    }
    modelUpdater(node, val) {
        node.value = val
    }
    update(node, exp, dir) {
        // 1.init
        const fn = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])
        // 2.update
        new Watcher(this.$vm, exp, val => {
            fn && fn(node, val)
        })
    }
}

依赖收集

  • 创建Watcher
const watchers = [] // 临时保存watcher
// 监听器:负责更新视图
class Watcher {
    constructor(vm, key, updateFn) {
        // myVue实例
        this.vm = vm
        // 依赖key值
        this.key = key
        // 更新函数
        this.updateFn = updateFn
        // 临时放入watcher数组
        watchers.push(this)
    }
    // 更新
    update(){
        this.updateFn.call(this.vm, this.vm[this.key])
    }
}
  • 声明Dep
class Dep {
    constructor() {
        this.deps = []
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    notify() {
        this.deps.forEach(dep => dep.update())
    }
}
  • 创建watcher时触发getter
class Watcher {
    constructor(vm, key, updateFn) {
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
    }
    // 更新
    update() {
        this.updateFn.call(this.vm, this.vm[this.key])
    }
}
  • 依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
    observe(val)
    // val值的唯一性,所以一个dep对应多个watcher
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        get() {
            Dep.target && dep.addDep(Dep.target)
            return val
        },
        set(newVal) {
            if (newVal == val) return
            val = newVal
            observe(newVal)
            dep.notify()
        }
    })
}

总结

  • new Vue()首先执行初始化,对data执行响应式处理,这个过程发生在Observer中
  • 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compiler中
  • 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  • 由于data的某个key值在一个视图中可能多次出现所以每个key都需要一个管家Dep来管理多个Watcher
  • 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数