网站链接: element-ui dtcms
当前位置: 首页 > 技术博文  > 技术博文

vue2和vue3实现响应式的区别

2021/6/28 13:57:42 人评论

vue2响应式 什么是响应式 首先我们先来了解一下什么是响应式,我们先来看一下官方的解释 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部…

vue2响应式

  1. 什么是响应式

首先我们先来了解一下什么是响应式,我们先来看一下官方的解释

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

ok,我们通过上面那句话得知,他是使用了Object.defineProperty来完成响应式处理的,那他究竟是怎么使用的呢?

  1. 怎么实现的响应式

废话不多说,我们点这里 Object.defineProperty 进入到MDN看一下里面的例子

​
var bValue = 38;

Object.defineProperty(o, "b", {

    // 使用了方法名称缩写(ES2015 特性)

    // 下面两个缩写等价于:    

    // get : function() { return bValue; },

    // set : function(newValue) { bValue = newValue; },

    get() { return bValue; },

    set(newValue) { bValue = newValue; },

    enumerable : true,

    configurable : true

});

​

我们看到该方法接收三个参数 Object.defineProperty(obj, prop, descriptor)

obj 要定义属性的对象。

prop 要定义或修改的属性的名称或 Symbol 。(该对象的key)

descriptor 要定义或修改的属性描述符。(你要更改该对象的哪些方法)

看上去很简单,我们只需要传入一个对象,再传入一个key值,再定义一些方法,就可以实现简单的 响应式了,那我们接下来自己实现一下吧

  1. Object的响应式处理

 
<!DOCTYPE html>

<html lang="en">

<body>

<h1 id="name"></h1>

<input id="input" type="text">

</body>

<script>

const Input = document.getElementById("input");

const Name = document.getElementById("name");

const Data = {name: 'jackliu'}

Name.innerHTML = Data.name

Input.oninput = (e) => {

    Data.name = e.target.value

}

function watchKey(obj, key, val) {

    Object.defineProperty(obj, key, {

        get: () => {

            return val

        },

        set: (newVal) => {

            if (newVal !== val) {

                console.log('渲染视图')

                val = newVal

                Name.innerHTML = val

            }

        }

    });

}

watchKey(Data, 'name', Data.name)

</script>

</html>

这样我们就能进行单个值的监听了,那我们如何进行多个值的监听呢?我们只需要对对象进行循环就好了

 
<!DOCTYPE html>

<html lang="en">

<head>

<title>Document</title>

</head>

<body>

<h1 id="show"></h1>

<input id="Name" type="text">

<input id="Id" type="text">

</body>

<script>

const Name = document.getElementById("Name");

const Id = document.getElementById("Id");

const Show = document.getElementById("show");

const Data = {

    name: 'jackliu',

    id: 0

}

Show.innerHTML = Data.Show

Name.oninput = (e) => {

    Data.name = e.target.value

}

Id.oninput = (e) => {

    Data.id = e.target.value

}

// 判断类型

function checkObjOrArr(obj) {

const type = Object.prototype.toString.call(obj)

if (['[object Object]', '[object Array]'].includes(type)) {

    return true

} else {

    return false

}

}

// 给对象的所有key添加响应式

function watchAll(obj) {

    if (!checkObjOrArr(obj)) {

        return obj

    }

    for (let key in obj) {

        watchKey(obj, key, obj[key])

    }

}

function watchKey(obj, key, val) {

    Object.defineProperty(obj, key, {

        get: () => {

            return val

        },

        set: (newVal) => {

            if (newVal !== val) {

                console.log('渲染视图')

                val = newVal

                changeShow()

            }

        }

    });

}

watchAll(Data)

function changeShow() {

    Show.innerHTML = `name: ${Data.name} id: ${Data.id}`

}

changeShow()

</script>

</html>

如此一来我们就能监听一个对象上所有的key,但是这里有一个大问题,我们现在监听的都只是值类型,

 
const Data = {

    name: 'jackliu',
    
    id: 0

}

如果我们的key中的类型是对象或数组,会怎么样呢?

 
const Data = {

    name: 'jackliu',

    ids: {

        id: 0

    }

}

我们可以看到,值类型的name是有响应式的,但是对象类型的ids下面的id失去了响应式

那我们该如何解决这个问题呢?

 
// 单个key添加响应式

function watchKey(obj, key, val) {

    watchAll(val)

    Object.defineProperty(obj, key, {

        get: () => {

            return val

        },

        set: (newVal) => {

            if (newVal !== val) {

                console.log('渲染视图')

                val = newVal

                changeShow()

                return

            }

            watchAll(val)
    
        }

    });

}

很简单,只需要在添加单个key的响应式时,再执行一次 watchAll 就可以了,如果当前key的值是对象,那么就对其所有的key进行绑定,如果是值类型,就直接返回,不做处理,走到这一步,对对象的监听其实已经算是完成的差不多了,但是,还有一个很严重的问题,大家不妨先思考一下,是什么问题

没错,就是值类型,一旦被改为object类型,那我们的新值就会失去响应式,这可咋整?

 
// 单个key添加响应式

function watchKey(obj, key, val) {

    watchAll(val)

    Object.defineProperty(obj, key, {

        get: () => {

            return val

        },

        set: (newVal) => {

            if (newVal !== val) {
    
                console.log('渲染视图')

                val = newVal

                watchAll(val)

                changeShow()
    
            }

        }

    });

}

是的,聪明的你一定想到了,我们只需要在值变更的时候再执行一次 watchAll 对新值绑定响应式即可,该方法中我们已经做了判断,如果是值类型,我们直接返回不做处理,如果是对象或数组,我们就会绑定响应式,到此为止,对象和值类型的响应式,我们已经搞明白了,那数组类型该如何处理呢?

  1. Array的响应式处理

由于Object.defineProperty只能对object实现响应式,当我们的数据是数组时,上面的代码就无能为力了,怎么办呢,我们需要对数组,做一下处理

 
<!DOCTYPE html>

<html lang="en">

<head>

<title>Document</title>

</head>

<body>

<h1 id="show"></h1>

<input id="Name" type="text" />

<input id="Id" type="text" />

<input id="Add" type="button" value="Add" />

</body>

<script>

const Name = document.getElementById("Name");

const Id = document.getElementById("Id");

const Show = document.getElementById("show");

const Data = {

name: 'jackliu',

    ids: {

        id: 0

    },

    money: [1]

}

Show.innerHTML = Data.Show

Name.oninput = (e) => {

    Data.name = e.target.value

}

Id.oninput = (e) => {

    Data.ids.id = e.target.value

}

Add.onclick = () => {

    Data.money.push(0)

}

// 判断类型

function checkObjOrArr(obj) {

    const type = Object.prototype.toString.call(obj)

    if (['[object Object]', '[object Array]'].includes(type)) {

        return true

    } else {

        return false

    }

}

// 获取数组的显式原型

const arrayProto = Array.prototype;

// 通过Object.create 得到转换后的具有数组显式原型的对象

const newProto = Object.create(arrayProto);

// 定制想要实现监听的原型属性

['push', 'pop', 'shift', 'unshift'].forEach((name) => {

    newProto[name] = function(...args) {

        arrayProto[name].call(this, ...args)

        console.log('视图渲染', this)

        changeShow()

    }

})

// 给对象的所有key添加响应式

function watchAll(obj) {

    if (!checkObjOrArr(obj)) {

        return obj

    }

// 修改数组的隐式原型

    if (Array.isArray(obj)) {

        obj.__proto__ = newProto;

    }

    for (let key in obj) {

        watchKey(obj, key, obj[key])

    }

}

// 单个key添加响应式

function watchKey(obj, key, val) {

    watchAll(val)

    Object.defineProperty(obj, key, {

        get: () => {

            return val

        },

        set: (newVal) => {

            if (newVal !== val) {

                console.log('渲染视图')

                val = newVal

                watchAll(val)

                changeShow()

            }

        }

    });

}

watchAll(Data)

// 修改dom内容,此处应为虚拟dom,但是我们本章不讲,所以直接操作了原生dom

function changeShow() {

    Show.innerHTML = `name: ${Data.name} id: ${Data.ids.id} 存款:     ${Data.money.join('')}`

}

changeShow()

</script>

</html>

优点

基于es5实现,支持绝大部分浏览器

缺点

由于是递归实现监听,所以如果数据层级过深,会导致初始化的时间过长,而且原生不支持监听数组,需要进行处理,对象中新增的key无法获取响应性,通过数组的下标改变数据时,也无法触发响应式

vue3响应式

        值类型响应式

vue中声明响应式的方式可以简单分为两种,一种是值类型响应式,例如字符串,布尔值,数字,一种是复杂类型响应式,例如对象,数组,map,set

我们先来看一下值类型是如何实现响应式的

 
<!DOCTYPE html>

<html lang="en">

<head>

<title>Document</title>

</head>

<body>

<h1 id="Show"></h1>

<input id="input" type="text">

</body>

<script>

const Input = document.getElementById("input");

const Show = document.getElementById("Show");

const Data = refFn('jackliu')

Show.innerHTML = Data.Show

Input.oninput = (e) => {

    Data.value = e.target.value

}

function refFn(_val) {

    return {

        get value() {

            return _val

        },

        set value(newVal) {

            _val = newVal

            // vue触发数据变化的操作,我们不展开,统一改成修改dom

            changeShow()

        }

    }

}

function changeShow() {

    Show.innerHTML = Data.value

}

changeShow()

</script>

</html>

去掉注释,10行代码搞定,只能说 牛啊牛啊

我们从上边的代码可以看出,ref方法返回了一个对象,该对象有个被设置了set和get方法的value,这个value,通过在set方法中触发我们自定义的事件,来实现响应式

        复杂类型响应式

vue3是使用了 Proxy 和 Reflect 来完成的响应式处理,我们需要先了解这两个是什么东西,究竟是怎么用的

        Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。简而言之,我们可以使用proxy来代理一个对象,被我们代理之后的对象,访问其方法都会被我们设置的捕捉器所拦截。

        Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。该方法拥有Object对象的所有方法,不过存在一些细微差别。

如何使用

<!DOCTYPE html>

<html lang="en">

<head>

<title>Document</title>

</head>

<body>

<h1 id="Show"></h1>

<h1 id="Id"></h1>

<input id="input" type="text">

</body>

<script>

const Input = document.getElementById("input");

const Id = document.getElementById("Id");

const Show = document.getElementById("Show");

const Data = { name: 'jackliu' }

Show.innerHTML = Data.Show

Input.oninput = (e) => {

newData.name = e.target.value

}

const ProxyData = (data) => {

if (!data || typeof data !== 'object') {

return data;

}

const config = {

/**

* 属性读取操作的捕捉器。

* @target 目标对象。

* @key 被获取的属性名。

* @receiver Proxy或者继承Proxy的对象

*/

get: (target, key, receiver) => {

// Reflect.ownKeys 返回对象的非原型属性 类似 Object.keys

const keys = Reflect.ownKeys(target)

// 判断当前获取的key是否已存在

if (keys.includes(key)) {

console.log("get", target, key, receiver)

}

console.log('执行代理')

// Reflect.get 如果未获取到对应的值,会返回false

const result = Reflect.get(target, key, receiver)

// 此步返回了一个ProxyData方法,解释一下为什么要返回它,如果result为值类型,那么则会直接返回当前值,如果非值类型,那么,就对其进行代理

return ProxyData(result)

},

/**

* 属性读取操作的捕捉器。

* @target 目标对象。

* @key 被获取的属性名。

* @value 新属性值。

* @receiver 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。

*/

set: (target, key, value, receiver) => {

const keys = Reflect.ownKeys(target)

// 如果当前值已存在,那就是修改之前的值,否则就是新增

if (keys.includes(key)) {

// 判断值是否相同,如果相同,直接return

if (value === Reflect.get(target, key, receiver)) {

console.log("重复修改", target, key, receiver)

return true

}

} else {

console.log("新增", target, key, receiver)

}

const result = Reflect.set(target, key, value, receiver)

// 执行我们的方法

changeShow()

return result

},

}

return new Proxy(data, config)

}

const newData = ProxyData({ ...Data })

window.newData = newData

function changeShow() {

Show.innerHTML = newData.name

Id.innerHTML = newData.id

}

changeShow()

</script>

</html>

大致的原理和vue2的类似,都是更改了对象原始的get,set,只不过proxy原生支持数组,不用另外特殊处理,且通过下标更改值,依然能触发响应式

优点

速度快,只有get数据时才会添加响应式,不用初始化时深层次递归,可以检测到代理对象属性的动态添加和删除,可以监测到数组的下标和length属性的变更

缺点

ES6的proxy语法对于低版本浏览器不支持,IE11

有错误的或有遗漏的地方希望大家可以进行指正和补充,感谢!

相关资讯

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?