在vue2中,什么是双向绑定,为什么vue3要进行优化?

一、什么是双向绑定

我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定举个栗子

当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定关系图如下

二、双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 "数据双向绑定" 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

三、实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

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

流程图如下:

实现

先来一个构造函数:执行初始化,对data执行响应化处理

bash 复制代码
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选项执行响应化具体操作

bash 复制代码
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

对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

bash 复制代码
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统⼀通知

实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中
  4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
bash 复制代码
// 负责更新视图 
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

bash 复制代码
class Dep { 
  constructor() { 
    this.deps = [];  // 依赖管理 
  } 
  addDep(dep) { 
    this.deps.push(dep); 
  } 
  notify() {  
    this.deps.forEach((dep) => dep.update()); 
  } 
}

创建watcher 时触发getter

bash 复制代码
class Watcher { 
  constructor(vm, key, updateFn) { 
    Dep.target = this; 
    this.vm[this.key]; 
    Dep.target = null; 
  } 
}

依赖收集,创建Dep实例

bash 复制代码
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

bash 复制代码
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方法,实现数据响应式

bash 复制代码
const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
    obj.foo = new Date().toLocaleTimeString()
},1000)

在对象存在多个key情况下,需要进行遍历

bash 复制代码
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
}

如果存在嵌套对象的情况,还需要在defineReactive中进行递归

bash 复制代码
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属性中进行递归

bash 复制代码
set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()
    }
}

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题

现在对一个对象进行删除与添加属性操作,无法劫持到

bash 复制代码
const obj = {
    foo: "foo",
    bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

当我们对一个数组进行监听的时候,并不那么好使了

bash 复制代码
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

bash 复制代码
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
}

测试一下简单数据的操作,发现都能劫持

bash 复制代码
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 了

bash 复制代码
const state = reactive({
    bar: { a: 1 }
})
 
// 设置嵌套对象属性
state.bar.a = 10 // no ok

如果要解决,需要在get之上再进行一层代理

bash 复制代码
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只能遍历对象属性进行劫持

bash 复制代码
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
}

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

bash 复制代码
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)

bash 复制代码
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方法)

bash 复制代码
// 数组重写
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

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

相关推荐
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
fmdpenny8 小时前
Vue3初学之商品的增,删,改功能
开发语言·javascript·vue.js
涔溪9 小时前
有哪些常见的 Vue 错误?
前端·javascript·vue.js
亦黑迷失11 小时前
vue 项目优化之函数式组件
前端·vue.js·性能优化
计算机-秋大田11 小时前
基于SpringBoot的高校教师科研的设计与实现(源码+SQL脚本+LW+部署讲解等)
java·vue.js·spring boot·后端·课程设计
eason_fan11 小时前
分析vue3源码23(异步组件实现)
vue.js·前端框架·源码阅读
BigData-014 小时前
vue视频流播放,支持多种视频格式,如rmvb、mkv
前端·javascript·vue.js
工业互联网专业14 小时前
基于springboot+vue的高校社团管理系统的设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计
白宇横流学长15 小时前
基于SpringBoot+Vue的旅游管理系统【源码+文档+部署讲解】
vue.js·spring boot·旅游