在vue2中,什么是双向绑定,为什么vue3要进行优化?
一、什么是双向绑定
我们先从单向绑定切入单向绑定非常简单,就是把Model
绑定到View
,当我们用JavaScript
代码更新Model
时,View
就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View
,Model
的数据也自动被更新了,这种情况就是双向绑定举个栗子
当用户填写表单时,View
的状态就被更新了,如果此时可以自动更新Model
的状态,那就相当于我们把Model
和View
做了双向绑定关系图如下
二、双向绑定的原理是什么
我们都知道 Vue
是数据双向绑定的框架,双向绑定由三个重要部分构成
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解ViewModel
它的主要职责就是:
- 数据变化后更新视图
- 视图变化后更新数据
当然,它还有两个主要部分组成
- 监听器(Observer):对所有数据的属性进行监听
- 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
三、实现双向绑定
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中- 同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数 - 由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
流程图如下:
实现
先来一个构造函数:执行初始化,对data
执行响应化处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Vue { constructor(options) { this .$options = options; this .$data = options.data; // 对data选项做响应式处理 observe( this .$data); // 代理data到vm上 proxy( this ); // 执行编译 new Compile(options.el, this ); } } |
对data
选项执行响应化具体操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function observe(obj) { if ( typeof obj !== "object" || obj == null ) { return ; } new Observer(obj); } class Observer { constructor(value) { this .value = value; this .walk(value); } walk(obj) { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } } |
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Compile { constructor(el, vm) { this .$vm = vm; this .$el = document.querySelector(el); // 获取dom 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.nodeName); } else if ( this .isInterpolation(node)) { console.log( "编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}} } 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); } } |
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
- 由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep中 - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 负责更新视图 class Watcher { constructor(vm, key, updater) { this .vm = vm this .key = key this .updaterFn = updater // 创建实例时,把当前实例指定到Dep.target静态属性上 Dep.target = this // 读一下key,触发get vm[key] // 置空 Dep.target = null } // 未来执行dom更新函数,由dep调用的 update() { this .updaterFn.call( this .vm, this .vm[ this .key]) } } |
声明Dep
1 2 3 4 5 6 7 8 9 10 11 | class Dep { constructor() { this .deps = []; // 依赖管理 } addDep(dep) { this .deps.push(dep); } notify() { this .deps.forEach((dep) => dep.update()); } } |
创建watcher
时触发getter
1 2 3 4 5 6 7 | class Watcher { constructor(vm, key, updateFn) { Dep.target = this ; this .vm[ this .key]; Dep.target = null ; } } |
依赖收集,创建Dep
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function defineReactive(obj, key, val) { this .observe(val); const dep = new Dep(); Object.defineProperty(obj, key, { get () { Dep.target && dep.addDep(Dep.target); // Dep.target也就是Watcher实例 return val; }, set (newVal) { if (newVal === val) return ; dep.notify(); // 通知dep执行更新方法 }, }); } |
四.vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
1、Object.defineProperty
定义:Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
为什么能实现响应式
通过defineProperty
两个属性,get
及set
- get
属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
- set
属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
下面通过代码展示:
定义一个响应式函数defineReactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function update() { app.innerText = obj.foo } function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get () { console.log(` get ${key}:${val}`); return val }, set (newVal) { if (newVal !== val) { val = newVal update() } } }) } |
调用defineReactive
,数据发生变化触发update
方法,实现数据响应式
1 2 3 4 5 | const obj = {} defineReactive(obj, 'foo' , '' ) setTimeout(()=>{ obj.foo = new Date().toLocaleTimeString() },1000) |
在对象存在多个key
情况下,需要进行遍历
1 2 3 4 5 6 7 8 | function observe(obj) { if ( typeof obj !== 'object' || obj == null ) { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } |
如果存在嵌套对象的情况,还需要在defineReactive
中进行递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function defineReactive(obj, key, val) { observe(val) Object.defineProperty(obj, key, { get () { console.log(` get ${key}:${val}`); return val }, set (newVal) { if (newVal !== val) { val = newVal update() } } }) } |
当给key
赋值为对象的时候,还需要在set
属性中进行递归
1 2 3 4 5 6 | set (newVal) { if (newVal !== val) { observe(newVal) // 新值是对象的情况 notifyUpdate() } } |
上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题
现在对一个对象进行删除与添加属性操作,无法劫持到
1 2 3 4 5 6 7 | const obj = { foo: "foo" , bar: "bar" } observe(obj) delete obj.foo // no ok obj.jar = 'xxx' // no ok |
当我们对一个数组进行监听的时候,并不那么好使了
1 2 3 4 5 6 7 | const arrData = [1,2,3,4,5]; arrData.forEach((val,index)=>{ defineProperty(arrData,index,val) }) arrData.push() // no ok arrData.pop() // no ok arrDate[0] = 99 // ok |
可以看到数据的api
无法劫持到,从而无法实现数据响应式,
所以在Vue2
中,增加了set
、delete
API,并且对数组api
方法进行一个重写
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题
小结
- 检测不到对象属性的添加和删除
- 数组
API
方法无法监听到 - 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
2、proxy
Proxy
的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了
在ES6
系列中,我们详细讲解过Proxy
的使用,就不再述说了
下面通过代码进行展示:
定义一个响应式方法reactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function reactive(obj) { if ( typeof obj !== 'object' && obj != null ) { return obj } // Proxy相当于在对象外层加拦截 const observed = new Proxy(obj, { get (target, key, receiver) { const res = Reflect. get (target, key, receiver) console.log(`获取${key}:${res}`) return res }, set (target, key, value, receiver) { const res = Reflect. set (target, key, value, receiver) console.log(`设置${key}:${value}`) return res }, deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key) console.log(`删除${key}:${res}`) return res } }) return observed } |
测试一下简单数据的操作,发现都能劫持
1 2 3 4 5 6 7 8 9 10 11 | const state = reactive({ foo: 'foo' }) // 1.获取 state.foo // ok // 2.设置已存在属性 state.foo = 'fooooooo' // ok // 3.设置不存在属性 state.dong = 'dong' // ok // 4.删除属性 delete state.dong // ok |
再测试嵌套对象情况,这时候发现就不那么 OK 了
1 2 3 4 5 6 | const state = reactive({ bar: { a: 1 } }) // 设置嵌套对象属性 state.bar.a = 10 // no ok |
如果要解决,需要在get
之上再进行一层代理
1 2 3 4 5 6 7 8 9 10 11 12 13 | function reactive(obj) { if ( typeof obj !== 'object' && obj != null ) { return obj } // Proxy相当于在对象外层加拦截 const observed = new Proxy(obj, { get (target, key, receiver) { const res = Reflect. get (target, key, receiver) console.log(`获取${key}:${res}`) return isObject(res) ? reactive(res) : res }, return observed } |
3、总结
Object.defineProperty
只能遍历对象属性进行劫持
1 2 3 4 5 6 7 8 | function observe(obj) { if ( typeof obj !== 'object' || obj == null ) { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } |
Proxy
直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function reactive(obj) { if ( typeof obj !== 'object' && obj != null ) { return obj } // Proxy相当于在对象外层加拦截 const observed = new Proxy(obj, { get (target, key, receiver) { const res = Reflect. get (target, key, receiver) console.log(`获取${key}:${res}`) return res }, set (target, key, value, receiver) { const res = Reflect. set (target, key, value, receiver) console.log(`设置${key}:${value}`) return res }, deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key) console.log(`删除${key}:${res}`) return res } }) return observed } |
Proxy
可以直接监听数组的变化(push
、shift
、splice
)
1 2 3 | const obj = [1,2,3] const proxtObj = reactive(obj) obj.psuh(4) // ok |
Proxy
有多达13种拦截方法,不限于apply
、ownKeys
、deleteProperty
、has
等等,这是Object.defineProperty
不具备的
正因为defineProperty
自身的缺陷,导致Vue2
在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set
、delete
方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 数组重写 const originalProto = Array.prototype const arrayProto = Object.create(originalProto) [ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'reverse' , 'sort' ].forEach(method => { arrayProto[method] = function () { originalProto[method].apply( this .arguments) dep.notice() } }); // set、delete Vue. set (obj, 'bar' , 'newbar' ) Vue.delete(obj), 'bar' ) |
Proxy
不兼容IE,也没有 polyfill
, defineProperty
能支持到IE9
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体