彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式
欢迎关注我的博客:https://github.com/wangweianger/myblog
Vue内部实现了一组观察数组的变异方法,例如:push(),pop(),shift()等。
Object.definePropert只能把对象属性改为getter/setter,而对于数组的方法就无能为力了,其内部巧妙的使用了数组的属性来实现了数据的双向绑定,下面我们来一步一步的实现一个简单版。
下文大部分代码摘自Vue源码
首先我们来实现给定一个数组 调用相关方法时触发自定义的函数
定义一个需要监听变化的数组
let obarr = []
来copy一份数组的原型方法,防止污染原生数组方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
我们先把arrayMethods对象上的push转换为观察者对象
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
console.log('obarr.push会走这里')
}
})
此时arrayMethods定义了一个push的新属性,那么我们如何把它和 let obarr = [] 绑定起来呢,来看看下面的实现?
obarr.__proto__ = arrayMethods
使用arrayMethods覆盖obarr的所有方法
到此现在完整代码如下:
let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
console.log('obarr.push会走这里')
}
})
obarr.__proto__ = arrayMethods;
向obarr中push一个值看看,是不是走了console呢,肯定的答复你:yes 走了。
obarr.push(0)
针对于不支持__proto__的浏览器实现如下:
let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
console.log('obarr.push会走这里')
}
})
Object.defineProperty(obarr,'push',{
value:arrayMethods.push
})
来真正的为arr赋值代码如下:
let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
//缓存原生方法,之后调用
const original = arrayProto['push']
let args = Array.from(arguments)
original.apply(this,args)
console.log(obarr)
}
})
obarr.__proto__ = arrayMethods;
现在每次执行obarr.push(0)时,obarr都会新增一项。
上面实现了push方法,其他的方法同理,我们只需要把所有需要实现的方法循环遍历执行即可,升级后代码如下:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//缓存原生方法,之后调用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
},
})
})
function protoAugment (target,src) {
target.__proto__ = src
}
// 调用
let obarr = []
protoAugment(obarr, arrayMethods)
来多试几次吧:
obarr.push(1)
obarr.push(2)
obarr.push(3)
obarr.push(4)
分析:
1、经过以上的代码可以看出,只会更改我们给定数组(obarr)的相关方法,而不会污染Array的原生方法,因此其他普通数组不受影响。
2、从新赋值数组的__proto__属性为arrayMethods,而arrayMethods我们从新定义了push,pop等相关属性方法,因此当我们使用数组的push,pop等方法时会调用arrayMethods的相关属性方法,达到监听数组变化的能力。
3、对于不支持__proto__属性的浏览器,直接使用Object.defineProperty从新定义相关属性。
4、而Vue的实现方法正如上,更改我们需要监听的Array数组属性值(属性值为函数),在监听函数里执行数组的原生方法,并通知所有注册的观察者进行响应式处理。
下面来简单的实现Vue对数组的依赖收集和通知更新
实现Vue的数据双向绑定有3大核心:Observer,Dep,Watcher,来个简单实现
首先来实现dep,dep主要负责依赖的收集,get时触发收集,set时通知watcher通信:
class Dep{
constructor () {
// 存放所有的监听watcher
this.subs = []
}
//添加一个观察者对象
addSub (Watcher) {
this.subs.push(Watcher)
}
//依赖收集
depend () {
//Dep.target 作用只有需要的才会收集依赖
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 调用依赖收集的Watcher更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 为Dep.target 赋值
function pushTarget (Watcher) {
Dep.target = Watcher
}
再来简单的实现Watcher,Watcher负责数据变更之后调用Vue的diff进行视图的更新:
class Watcher{
constructor(vm,expOrFn,cb,options){
//传进来的对象 例如Vue
this.vm = vm
//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
this.cb = cb
//收集Deps,用于移除监听
this.newDeps = []
this.getter = expOrFn
//设置Dep.target的值,依赖收集时的watcher对象
this.value =this.get()
}
get(){
//设置Dep.target值,用以依赖收集
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
//添加依赖
addDep (dep) {
// 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
this.newDeps.push(dep)
dep.addSub(this)
}
//更新
update () {
this.run()
}
//更新视图
run(){
//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
}
}
简单实现Observer,Observer负责数据的双向绑定,并把对象属性改为getter/setter
//获得arrayMethods对象上所有属性的数组
const arrayKeys = Object.getOwnPropertyNames(arrayMethods) class Observer{
constructor (value) {
this.value = value
// 增加dep属性(处理数组时可以直接调用)
this.dep = new Dep()
//将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
//处理数组是也可直接获取Observer对象
def(value, '__ob__', this)
if (Array.isArray(value)) {
//处理数组
const augment = value.__proto__ ? protoAugment : copyAugment
//此处的 arrayMethods 就是上面使用Object.defineProperty处理过
augment(value, arrayMethods, arrayKeys)
// 循环遍历数组children进行oberve
this.observeArray(value)
} else {
//处理对象
this.walk(value)
}
} walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
if(keys[i]=='__ob__') return;
defineReactive(obj, keys[i], obj[keys[i]])
}
} observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
//数据重复Observer
function observe(value){
if(typeof(value) != 'object' ) return;
let ob = new Observer(value)
return ob;
}
// 把对象属性改为getter/setter,并收集依赖
function defineReactive (obj,key,val) {
const dep = new Dep()
//处理children
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`调用get获取值,值为${val}`)
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
//此处是对Array数据类型的依赖收集
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
console.log(`调用了set,值为${newVal}`)
const value = val
val = newVal
//对新值进行observe
childOb = observe(newVal)
//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
dep.notify()
}
})
} //辅助方法
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
})
} //重新赋值Array的__proto__属性
function protoAugment (target,src) {
target.__proto__ = src
}
//不支持__proto__的直接修改相关属性方法
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
//收集数组的依赖
function dependArray (value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
//循环遍历chindren进行依赖收集
dependArray(e)
}
}
}
Observer中写了一些相关需要的方法。
让我们来修改下处理数组的相关方法,当使用Array.push相关方法时可以调用Watcher更新视图
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//缓存原生方法,之后调用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
const ob = this.__ob__
ob.dep.notify()
},
})
})
大功至此告成,把所有代码整理完整如下:
/*----------------------------------------处理数组------------------------------------*/
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//缓存原生方法,之后调用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
const ob = this.__ob__
ob.dep.notify()
},
})
})
/*----------------------------------------Dep---------------------------------------*/
class Dep{
constructor () {
// 存放所有的监听watcher
this.subs = []
} //添加一个观察者对象
addSub (Watcher) {
this.subs.push(Watcher)
} //依赖收集
depend () {
//Dep.target 作用只有需要的才会收集依赖
if (Dep.target) {
Dep.target.addDep(this)
}
} // 调用依赖收集的Watcher更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
} // 为Dep.target 赋值
function pushTarget (Watcher) {
Dep.target = Watcher
} /*----------------------------------------Watcher------------------------------------*/
class Watcher{
constructor(vm,expOrFn,cb,options){
//传进来的对象 例如Vue
this.vm = vm
//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
this.cb = cb
//收集Deps,用于移除监听
this.newDeps = []
this.getter = expOrFn
//设置Dep.target的值,依赖收集时的watcher对象
this.value =this.get()
} get(){
//设置Dep.target值,用以依赖收集
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
} //添加依赖
addDep (dep) {
// 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
this.newDeps.push(dep)
dep.addSub(this)
} //更新
update () {
this.run()
} //更新视图
run(){
//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
}
} /*----------------------------------------Observer------------------------------------*/
//获得arrayMethods对象上所有属性的数组
const arrayKeys = Object.getOwnPropertyNames(arrayMethods) class Observer{
constructor (value) {
this.value = value
// 增加dep属性(处理数组时可以直接调用)
this.dep = new Dep()
//将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
//处理数组是也可直接获取Observer对象
def(value, '__ob__', this)
if (Array.isArray(value)) {
//处理数组
const augment = value.__proto__ ? protoAugment : copyAugment
//此处的 arrayMethods 就是上面使用Object.defineProperty处理过
augment(value, arrayMethods, arrayKeys)
// 循环遍历数组children进行oberve
this.observeArray(value)
} else {
//处理对象
this.walk(value)
}
} walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
if(keys[i]=='__ob__') return;
defineReactive(obj, keys[i], obj[keys[i]])
}
} observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
//数据重复Observer
function observe(value){
if(typeof(value) != 'object' ) return;
let ob = new Observer(value)
return ob;
}
// 把对象属性改为getter/setter,并收集依赖
function defineReactive (obj,key,val) {
const dep = new Dep()
//处理children
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`调用get获取值,值为${val}`)
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
//此处是对Array数据类型的依赖收集
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
console.log(`调用了set,值为${newVal}`)
const value = val
val = newVal
//对新值进行observe
childOb = observe(newVal)
//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
dep.notify()
}
})
} //辅助方法
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
})
} //重新赋值Array的__proto__属性
function protoAugment (target,src) {
target.__proto__ = src
}
//不支持__proto__的直接修改相关属性方法
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
//收集数组的依赖
function dependArray (value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
//循环遍历chindren进行依赖收集
dependArray(e)
}
}
}
依赖收集流程图:

测试:
定义一个data对象:
let data={
name:'zane',
blog:'https://blog.seosiwei.com/',
hobby:['basketball','football'],
list:[
{name:'zhangsan'},
{name:'lishi'}
]
}
调用watcher,并进行数据监听
let getUpdates = (vm)=>{
console.log('默认调用一次,进行依赖收集')
}
new Watcher(this,getUpdates)
observe(data)
调用get收集依赖
//收集name依赖
data.name
//收集hobby依赖
data.hobby
测试数据监听
//都会打印这里会去执行Vue的diff相关方法,进而更新数据
data.name = 'zhangshan'
data.hobby.push('volleyball')
是不时出现可可爱的 这里会去执行Vue的diff相关方法,进而更新数据 日志呢。
没进行依赖收集的属性会打印日志吗,来尝试一下吧
//不会打印更新
data.blog = 'http://www.seosiwei.com/'
//不会调用每一个children的打印更新
data.list.push({name:'xiaowang'})
以上基本实现了Vue对数组和对象的双向绑定处理方式,收集依赖和更新视同原理,当然代码并没有做太多的优化,比如(Watcher重复的收集)。
大部分代码摘自与Vue源码,部分实现的比较简单,做了一些更改,代码进行的从新组织,整体能很好的说明Vue的核心MVVM实现方式。
彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式的更多相关文章
- vue双向绑定的原理及实现双向绑定MVVM源码分析
vue双向绑定的原理及实现双向绑定MVVM源码分析 双向数据绑定的原理是:可以将对象的属性绑定到UI,具体的说,我们有一个对象,该对象有一个name属性,当我们给这个对象name属性赋新值的时候,新值 ...
- vue实现双向绑定mvvm
剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1.了解vue的双向数据绑定原理以及核心代码模块2.缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自 ...
- vue中的数据双向绑定
学习的过程是漫长的,只有坚持不懈才能到达到自己的目标. 1.vue中数据的双向绑定采用的时候,数据劫持的模式.其实主要是用了Es5中的Object.defineProperty;来劫持每个属性的get ...
- 剖析Vue原理&实现双向绑定MVVM
转自:http://www.w3cmark.com/2016/496.html 本文能帮你做什么? 1.了解vue的双向数据绑定原理以及核心代码模块 2.缓解好奇心的同时了解如何实现双向绑定 为了便于 ...
- Vue - 如何实现一个双向绑定
JS - 如何实现一个类似 vue 的双向绑定 Github JS 实现代码 先来看一张图: 这张图我做个简要的描述: 首先创建一个实例对象,分别触发了 compile 解析指令 和 observe ...
- Vue父子组件数据双向绑定,子组件可修改props
第一种,子组件通过监听父组件数据,子组件改变数据之后通知给父组件 原文链接:https://blog.csdn.net/m0_37728716/article/details/81776929 父组件 ...
- Vue.js 3.x 双向绑定原理
什么是双向绑定? 废话不多说,我们先来看一个 v-model 基本的示例: <input type="text" v-model="search"> ...
- VUE 表单元素双向绑定总结
checkbox最基本用法: <input type="checkbox" v-model="inputdata" checked/> <in ...
- vue 自定义组件 v-model双向绑定、 父子组件同步通信
父子组件通信,都是单项的,很多时候需要双向通信.方法如下: 1.父组件使用:msg.sync="aa" 子组件使用$emit('update:msg', 'msg改变后的值xxx ...
- vue 结合localStorage 来双向绑定数据
结合localStorage 来双向绑定数据(超级神奇) localStorage.js: const STORAGE_KEY = 'todos_vuejs' export default { fet ...
随机推荐
- 2022-06-18:golang与 C++数据结构类型对应关系是怎样的?
2022-06-18:golang与 C++数据结构类型对应关系是怎样的? 答案2022-06-18: uintptr和unsafe.Pointer相当于c++的void*,也就是任意指针. uint ...
- 2022-04-17:给定一个数组arr,其中的值有可能正、负、0, 给定一个正数k。 返回累加和>=k的所有子数组中,最短的子数组长度。 来自字节跳动。力扣862。
2022-04-17:给定一个数组arr,其中的值有可能正.负.0, 给定一个正数k. 返回累加和>=k的所有子数组中,最短的子数组长度. 来自字节跳动.力扣862. 答案2022-04-17: ...
- 2021-10-27:课程表。你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisi
2021-10-27:课程表.你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 .在选修某些课程之前需要一些先修课程. 先修课程按数组 prerequisi ...
- drf——5个视图扩展类、9个视图子类、视图集、drf之路由
5个视图扩展类 # 写5个类(不叫视图类 视图拓展类 需要配合GenericAPIView一起用) 每个类有一个方法 以后想写哪个接口 就继承哪个类即可 from rest_framework.res ...
- flutter填坑之旅(配置本地资源文件)
一.添加单个资源文件 项目下创建一个assets目录,在该目录下存放对应的资源文件(json.image.-) pubspec.yaml文件中配置资源文件路径(注意缩进要与上下文一直) assets: ...
- 用go封装一下封禁功能
用go封装一下封禁功能 本篇为用go设计开发一个自己的轻量级登录库/框架吧 - 秋玻 - 博客园 (cnblogs.com)的封禁业务篇,会讲讲封禁业务的实现,给库/框架增加新的功能. 源码:http ...
- 小程序提交POST请求,出现405错误
status": 405, "error": "Method Not Allowed", "message": "Req ...
- CSS3 clip-path:打造独特创意设计效果的秘密武器
大家好,我是程序视点的小二哥. 今天小二哥将给大家分享一篇有前端实验室的文章.一部由CSS技术实现的作品.它将再一次证明CSS的强大力量. 欣赏 这是一部由阿姆斯特丹设计师Bryan James通过3 ...
- 技术招聘漫谈 | 正在招Golang工程师的你,赶快收藏这份识人秘籍!
各位技术面试官,欢迎来到新一期的技术招聘漫谈专栏. 在前两期的专栏中,我们解析了前端工程师以及 Java 工程师 这两个常见技术岗位的招聘技巧. 今天,我们想把目光聚焦在一个前景与"钱&qu ...
- 7-9 《Fibonacci 数列》
7-9 <Fibonacci 数列> 思路:吃过前面<序列求和>问题的亏 但还是要尝试一下循环大法 Fn=Fn-1+Fn-2 定义int型 aFn-1 , bFn-2 , cF ...