>웹 프론트엔드 >JS 튜토리얼 >MVVM 프레임워크가 양방향 바인딩을 해결하는 방법

MVVM 프레임워크가 양방향 바인딩을 해결하는 방법

亚连
亚连원래의
2018-06-09 16:10:402154검색

이 글은 주로 MVVM 프레임워크 분석의 양방향 바인딩을 소개하고 참고용으로 제공합니다.

MVVM 프레임워크

최근 몇 년간 분명한 프런트 엔드 개발 추세는 아키텍처를 전통적인 MVC 모델에서 MVVM 모델로 마이그레이션하는 것입니다. 기존 MVC에서는 현재 프런트엔드와 백엔드 간의 데이터 상호 작용 후 전체 페이지가 새로 고쳐져 사용자 경험이 저하됩니다. 따라서 우리는 Ajax를 통해 게이트웨이 REST API와 통신하고 페이지의 특정 섹션을 비동기식으로 새로 고쳐 경험을 최적화하고 향상시킵니다.

MVVM 프레임워크의 기본 개념

MVVM 프레임워크에는 View(뷰)와 Model(데이터)이 직접 통신할 수 없으며, 이들 사이에 관찰자 역할을 하는 ViewModel이 있습니다. 사용자가 View를 조작하면 ViewModel이 변경 사항을 감지한 다음 해당 변경 사항을 Model에 알립니다. 반대로 Model(데이터)이 변경되면 ViewModel도 변경 사항을 감지하여 그에 따라 View를 업데이트합니다. 이 앞뒤 프로세스는 우리가 양방향 바인딩으로 알고 있는 것입니다.

MVVM 프레임워크의 애플리케이션 시나리오

MVVM 프레임워크의 이점은 분명합니다. 프런트 엔드가 데이터에 대해 작동할 때 데이터는 Ajax 요청을 통해 지속될 수 있고 필요한 데이터 콘텐츠의 일부만 유지될 수 있습니다. 전체 페이지를 새로 고쳐야 합니다. 특히 모바일에서는 페이지를 새로 고치는 데 비용이 너무 많이 듭니다. 일부 리소스는 캐시되지만 페이지의 DOM, CSS 및 JS는 브라우저에서 다시 구문 분석되므로 모바일 페이지는 일반적으로 SPA 단일 페이지 애플리케이션으로 만들어집니다. 이를 바탕으로 React.js, Vue.js, Angular.js 등 많은 MVVM 프레임워크가 탄생했습니다.

MVVM 프레임워크의 간단한 구현

Vue의 양방향 바인딩 흐름을 시뮬레이션하고 간단한 MVVM 프레임워크를 구현합니다. 위 그림에서 볼 수 있듯이 점선 사각형은 앞서 언급한 ViewModel 중간 계층입니다. 관찰자의 역할을 하는 것입니다. 또한 양방향 바인딩 흐름에서 View to Model이 실제로 입력의 이벤트 리스닝 기능을 통해 구현되는 것을 확인할 수 있습니다(단방향 바인딩 흐름). 이 단계에서는 상태 관리 도구(예: Redux)를 사용합니다. 또한 양방향 바인딩 흐름에서 볼 수 있는 모델은 실제로 각 MVVM 프레임워크에서 동일한 방식으로 구현되며, 사용되는 핵심 메서드는 Object.defineProperty()입니다. , 후속 처리를 위해 그에 따라 변경될 수 있습니다.

Mvvm(항목 파일) 구현

일반적으로 Mvvm 프레임워크를 다음과 같이 호출합니다

const vm = new Mvvm({
      el: '#app',
      data: {
       title: 'mvvm title',
       name: 'mvvm name'
      },
     })

그러나 이 경우 제목 속성을 얻으려면 다음과 같은 형식으로 가져와야 합니다. vm.data.title이 제목 속성을 얻을 수 있도록 하기 위해 Mvvm 프로토타입에 프록시 메서드를 추가합니다.

function Mvvm (options) {
 this.data = options.data
 const self = this
 Object.keys(this.data).forEach(key =>
  self.proxyKeys(key)
 )
}
Mvvm.prototype = {
 proxyKeys: function(key) {
  const self = this
  Object.defineProperty(this, key, {
   get: function () { // 这里的 get 和 set 实现了 vm.data.title 和 vm.title 的值同步
    return self.data[key]
   },
   set: function (newValue) {
    self.data[key] = newValue
   }
  })
 }
}

프록시 메서드를 구현한 후 the main process

function Mvvm (options) {
 this.data = options.data
 // ...
 observe(this.data)
 new Compile(options.el, this)
}

observer(관찰자) 구현

관찰자의 임무는 모델(JS 개체)의 변경 사항을 모니터링하는 것입니다. 핵심 부분은 Object.defineProperty()의 get 및 set 메서드를 사용하는 것입니다. . Model(JS 객체)의 값을 얻으려면 get 메소드가 자동으로 호출되며, Model(JS 객체)의 값이 변경되면 set 메소드가 자동으로 호출됩니다. 데이터, 코드는 다음과 같습니다.

let data = {
 number: 0
}
observe(data)
data.number = 1 // 值发生变化
function observe(data) {
 if (!data || typeof(data) !== 'object') {
  return
 }
 const self = this
 Object.keys(data).forEach(key =>
  self.defineReactive(data, key, data[key])
 )
}
function defineReactive(data, key, value) {
 observe(value) // 遍历嵌套对象
 Object.defineProperty(data, key, {
  get: function() {
   return value
  },
  set: function(newValue) {
   if (value !== newValue) {
    console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
    value = newValue
   }
  }
 })
}

코드를 실행하면 콘솔 출력 값이 newValue:1 oldValue:0으로 변경되는 것을 볼 수 있습니다. 이 시점에서 관찰자의 로직이 완성됩니다.

Dep(구독자 배열)과 Watcher(구독자)의 관계

변화를 관찰한 후에는 항상 특정 그룹의 사람들에게 알리고 그에 따라 처리하도록 해야 합니다. 좀 더 쉽게 이해하기 위해, 구독은 WeChat 공식 계정을 구독하는 것으로 생각하면 됩니다. WeChat 공식 계정의 콘텐츠가 업데이트되면 구독하는 사람들에게 콘텐츠가 푸시(업데이트)됩니다.

동일한 WeChat 공식 계정에 수천명의 사람들이 가입하고 있기 때문에 가장 먼저 떠오르는 것은 새로운 Array()를 사용하여 이러한 사람들(html 노드)을 저장하는 것입니다. 그래서 우리는 다음과 같은 코드를 가지고 있습니다:

// observer.js
function Dep() {
 this.subs = [] // 存放订阅者
}
Dep.prototype = {
 addSub: function(sub) { // 添加订阅者
  this.subs.push(sub)
 },
 notify: function() { // 通知订阅者更新
  this.subs.forEach(function(sub) {
   sub.update()
  })
 }
}
function observe(data) {...}
function defineReactive(data, key, value) {
 var dep = new Dep()
 observe(value) // 遍历嵌套对象
 Object.defineProperty(data, key, {
  get: function() {
   if (Dep.target) { // 往订阅器添加订阅者
    dep.addSub(Dep.target)
   }
   return value
  },
  set: function(newValue) {
   if (value !== newValue) {
    console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
    value = newValue
    dep.notify()
   }
  }
 })
}

코드는 언뜻 보면 비교적 매끄러우나 Dep.target 및 sub.update에 걸릴 수 있으므로 자연스럽게 watcher에게 주의를 돌립니다.

// watcher.js
function Watcher(vm, exp, cb) {
 this.vm = vm
 this.exp = exp
 this.cb = cb
 this.value = this.get()
}
Watcher.prototype = {
 update: function() {
  this.run()
 },
 run: function() {
  // ...
  if (value !== oldVal) {
   this.cb.call(this.vm, value) // 触发 compile 中的回调
  }
 },
 get: function() {
  Dep.target = this // 缓存自己
  const value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数
  Dep.target = null // 释放自己
  return value
 }
}

从代码中可以看到当构造 Watcher 实例时,会调用 get() 方法,接着重点关注 const value = this.vm.data[this.exp] 这句,前面说了当要获取 Model(JS 对象) 的值时,会自动调用 Object.defineProperty 的 get 方法,也就是当执行完这句的时候,Dep.target 的值传进了 observer.js 中的 Object.defineProperty 的 get 方法中。同时也一目了然地在 Watcher.prototype 中发现了 update 方法,其作用即触发 compile 中绑定的回调来更新界面。至此解释了 Observer 中 Dep.target 和 sub.update 的由来。

来归纳下 Watcher 的作用,其充当了 observer 和 compile 的桥梁。

1 在自身实例化的过程中,往订阅器(dep) 中添加自己

2 当 model 发生变动,dep.notify() 通知时,其能调用自身的 update 函数,并触发 compile 绑定的回调函数实现视图更新

最后再来看下生成 Watcher 实例的 compile.js 文件。

compile(编译) 的实现

首先遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成 fragment(文档碎片) 进行解析编译,解析完成,再将 fragment 添加回原来的真实 dom 节点中。代码如下:

function Compile(el, vm) {
 this.vm = vm
 this.el = document.querySelector(el)
 this.fragment = null
 this.init()
}
Compile.prototype = {
 init: function() {
  if (this.el) {
   this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片
   this.compileElement(this.fragment) // 对 fragment 进行编译解析
   this.el.appendChild(this.fragment)
  }
 },
 nodeToFragment: function(el) {
  const fragment = document.createDocumentFragment()
  let child = el.firstChild // △ 第一个 firstChild 是 text
  while(child) {
   fragment.appendChild(child)
   child = el.firstChild
  }
  return fragment
 },
 compileElement: function(el) {...},
}

这个简单的 mvvm 框架在对 fragment 编译解析的过程中对 {{}} 文本元素、v-on:click 事件指令、v-model 指令三种类型进行了相应的处理。

Compile.prototype = {
 init: function() {
  if (this.el) {
   this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片
   this.compileElement(this.fragment) // 对 fragment 进行编译解析
   this.el.appendChild(this.fragment)
  }
 },
 nodeToFragment: function(el) {...},
 compileElement: function(el) {...},
 compileText: function (node, exp) { // 对文本类型进行处理,将 {{abc}} 替换掉
  const self = this
  const initText = this.vm[exp]
  this.updateText(node, initText) // 初始化
  new Watcher(this.vm, exp, function(value) { // 实例化订阅者
   self.updateText(node, value)
  })
 },
 compileEvent: function (node, vm, exp, dir) { // 对事件指令进行处理
  const eventType = dir.split(':')[1]
  const cb = vm.methods && vm.methods[exp]
  if (eventType && cb) {
   node.addEventListener(eventType, cb.bind(vm), false)
  }
 },
 compileModel: function (node, vm, exp) { // 对 v-model 进行处理
  let val = vm[exp]
  const self = this
  this.modelUpdater(node, val)
  node.addEventListener('input', function (e) {
   const newValue = e.target.value
   self.vm[exp] = newValue // 实现 view 到 model 的绑定
  })
 },
}

在上述代码的 compileTest 函数中看到了期盼已久的 Watcher 实例化,对 Watcher 作用模糊的朋友可以往上回顾下 Watcher 的作用。另外在 compileModel 函数中看到了本文最开始提到的双向绑定流中的 View 到 Model 是借助 input 监听事件变化实现的。

项目地址

本文记录了些阅读 mvvm 框架源码关于双向绑定的心得,并动手实践了一个简版的 mvvm 框架,不足之处在所难免,欢迎指正。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

通过微信小程序如何实现验证码获取倒计时效果

ES6 迭代器和 for.of循环(详细教程)

在vue中使用better-scroll滚动插件

在VUE + UEditor中如何实现单图片跨域上传功能

위 내용은 MVVM 프레임워크가 양방향 바인딩을 해결하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.