VUE原理(一)响应式原理实现

150 阅读4分钟

实现响应式

响应式意味着一旦数据发生变化,我们可以立即知道并作出一些对应的操作,如DOM操作或者发送个请求等。

JS中实现响应式有2种方法,分别是对象属性拦截,(Vue2.x)使用的Object.defineProperty和对象整体代理(Vue3.x)使用的Proxy

Object.defineProperty

基本使用方法如下:

字面量定义

let data = {
    name: 'zs'
}
data.name = 'zs1' // 然而我们并不能立即知道

let data = {}
Object.defineProperty(data, 'name', {
    // 当使用data.name 是自动调用这个函数
    get(){
        return 
    },
    // 当使用data.name = 'zs' 自动调用这个函数,并把'zs'自动当作实参传入
    set(value){
        log(value)
        // 这个位置只要你修改了name属性就会得到执行
        // 所以如果你想要在name变化的时候完成一些自己的事情都可以放到这里来执行

    }
}


然而这样的操作并没有把get和set联动起来,想要联动可以引入一个中间变量并简单改造一下,

let data = {}
let _value = 'zs'
Object.defineProperty(data, 'name', {
    get(){
        return _value
    },
    set(value){
        _value = value
    }
}

然而在vue里面,数据都是在data里返回,那么如何改造成响应式数据?

data(){
    return{
        name: 'zs',
        age: 12
    }
}

Object.keys(data).forEach((key) => {
    log(key) // name age
    log(data[key]) // 'zs' 12
    
    turn(data, key, data[key])
})

function turn (data, name, value){
    Object.defineProperty(data, name, {
        get(){
            return value
        },
        set(newValue){
            value = newValue
        }
    })
}

image.png

当有getset的时候就已经被处理成响应式数据了。

Object.keys().forEach()多次遍历的时候形成了不同的作用域,其内部的变量互不干扰(defineReactive === turn),并形成了闭包,函数结束后内存不释放,长期留在内存中。

闭包:函数内部有的变量被回内部其它函数使用了则形成闭包。例如turn函数里value被Object.defineProperty使用了,则形成闭包,value不会释放,长期在内存存在。

image.png

简单总结:

1.函数定义形参相当于在内部中明了和形参名字对应的变量并且初始值为undefined 2.函数调用传入实参的时候相当于给内部中明好的变量做了赋值操作(首次遍历举例) 3. defineReactive函数调用完毕本来应该内部所有的变量都会被回收但是如果内部有其它函数使用了当前变量则形成了闭包不会被回收 4. 内部由于有其它方法引用了value属性所以defineReactive函数的执行并不会导致value变量的销毁会一直常驻内存 5. 由于闭包的特性每一个传入下来的value都会常驻内存相当于我们上一节讲的中间变量_value目的是为了set和get的联动 6. 同时,vue2.x中不要向data里放太冗余的数据,要精简,这个操作很消耗性能。

数据变化映射到视图

M-V

<div id='app' class='ap'>
    <p></p>
</div>
let data ={
    name: 'zs'
}

Object.keys(data).forEach((key) => {
    turn(data, key, data[key])
})

function turn (data, name, value){
    Object.defineProperty(data, name, {
        get(){
            return value
        },
        set(newValue){
            value = newValue
            if(newValue === value){
                return
            }
            document.querySelector('.ap p').innerHTML = value
        }
    })
}
document.querySelector('.ap p').innerHTML = data.name

声明式的操作指令--实现一下简单的v-text

目标:一旦data中的name发生变化之后标记了v-text的p标签的文本内容会立刻得到更新 实现指令的核心:不管是指令也好还是插值表达式也好它们都是数据和视图之间建立关联的‘标识’所以本质就是通过一定的手段找到符合标识的dom元素然后把数据放上去每当数据发生变化就重新执行一遍放置数据的操作 实现步骤:

  1. 先通过标识查找把数据放到对应的dom上.显示出来
  2. 数据变化之后再次执行将最新的值放到对应的dom上
let app = document.getElementById('app')
// childNode 包含了所有,标签节点和文本节点
app.childNodes.forEach((node) => {
    if(node.nodeType === 1){  // 是固定的
        const attr = node.attributes // {v-text,class}
        Array.from(attr).forEach((attr) => {
            const nodeType = nodeName
            const nodeValue = nodeValue
            if(nodeType === 'v-text'){
                node.innerText = data[nodeValue]
            }
        })
    }
})

V-M

实现方法就算增加个监听事件

利用input简单实现v-model

let app = document.getElementById('app')
// childNode 包含了所有,标签节点和文本节点
app.childNodes.forEach((node) => {
    if(node.nodeType === 1){  // 是固定的
        const attr = node.attributes // {v-text,class}
        Array.from(attr).forEach((attr) => {
            const nodeType = nodeName
            const nodeValue = nodeValue
            if(nodeType === 'v-text'){
                node.innerText = data[nodeValue]
            }
            // 实现v-model
            if(nodeType === 'v-model'){
                node.value = data[nodeValue]
                node.addEventListener('input',(e) => {
                    let newValue = e.target.value
                    data[nodeType] = newValue
                })
            }
        })
    }
})

但是此法有一定的问题,就是当每次修改v-text的时候把每个v-text都给重新赋值修改了。例如修改v-text = name,但是实际上name、age都修改了,虽然数据是正确的。

image.png

解决思路: 发布订阅者模式优化(见后文)