一场电影时间,了解MVVM原理

如果是用Vue的选手的话,面试一般都会被问到响应式是如何实现的。

而如果我们直接按网上的大部分的答案(或者相近的答案):

  • 1、实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
  • 2、实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
  • 3、实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;
  • 4、实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。

我们很有可能会被认为是靠出来的,并非真正了解,就算你了解,如此答复,也有可能被认为是。试想,面试官并非面试过你一个人可能好几十个,如果答复都大近相同的话,很难让面试官眼前一亮。

而如果说手写过一个小demo,并且能举一反三,这b是否拿捏的很准。

而响应式无非是 数据劫持发布订阅,也正如上述所描述的一样,但是我们是实现它的过程。

准备

...
<body>
  <div id="app">
    {{name}}
    <div>
      {{age}}
    </div>
    <input type="text" v-model="price">
  </div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        name: 'W',
        age: '18',
        price: '无价'
      }
    })
  </script>
</body>
...

数据劫持

// 先实现数据劫持
function Vue(options = {}) {
  this._data = options.data || {}
  this._el = options.el || '#app'
  this._options = options

  observe(this._data) // 劫持data所有属性

  // 实现 this.name 能访问到 this._data.name(例子)
  Object.keys(this._data).forEach(key => {
    Object.defineProperty(this, key, {
      configurable: true,
      get() {
        return this._data[key]
      },
      set(newVal) {
        this._data[key] = newVal
      }
    })
  })
}

// Vue2使用defineProperty后没有实现的点(使用别的方法实现):
// (1)只能劫持对象已存在的属性,新增则无法劫持到
// (2)无法监听到数组内部变化,数组长度变化等
function Observe(data) {
  Object.keys(data).forEach(key => {
    let val = data[key]
    observe(val) // 实现深度监听
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true, // 必须为true,否则后面无法枚举
      get() {
        console.log('获取', key, '值:', val)
        return val // 不能使用data[key],否则会循环调用get方法
      },
      set(newVal) {
        console.log('设置', key, '值:', newVal)
        if (newVal === val) {
          return
        }
        val = newVal
        observe(val) // 判断newVal是否为对象
      }
    })
  })
}

function observe(data) {
  // console.log(data)
  // console.log(typeof data)
  if (!data || typeof data !== 'object') {
    return
  }
  new Observe(data)
  return
}

验证下:

Object.defineProperty实际上是可以实现对数组的数据劫持的,那为何不实现,可能尤大大考虑到性能的问题吧。那如何改变数组内容后,触发视图更新呢,这里给出部分代码:

数组方法变异

// 不是完整代码,无法正常运行
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 类似创建了一个实例,改变实例的内容并不会影响到 Array.prototype

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
  .forEach(function (method) {
    const original = arrayProto[method] // 获取原生Array方法
    def(arrayMethods, method, function mutator(...args) {
      const result = original.apply(this, args)
      const ob = this.__ob__
      let inserted
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      if (inserted) ob.observeArray(inserted)
      ob.dep.notify()
      return result
    })
  })

// 贴出def部分的代码
export function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

上述可能有点难懂,这里举一个简单的例子:

const push = Array.prototype.push

Array.prototype.push = function mutator(...arg) {
  const result = push.apply(this, arg)
  doSomething()
  return result
}

function doSomething() {
  console.log('do something')
}

let arr = []
arr.push(1)
arr.push(2)

解析

function Vue(options = {}) {
 ...
 new Compile(this._el, this)
}

// 解析
function Compile(el, vm) {
  vm._el = document.querySelector(el)

  let fragment = document.createDocumentFragment() // 创建虚拟DOM
  let child

  // 将真实DOM移入到虚拟DOM中
  // 遍历完毕后,真实dom为空
  while (child = vm._el.firstChild) {
    fragment.appendChild(child)
  }

  const replace = function (frag) {

    Array.from(frag.childNodes).forEach((node) => {
      let reg = /\{\{(.*?)\}\}/g
      let text = node.textContent

      // console.log('text', text)
      // console.log('node.nodeType', node.nodeType)
      // console.log('reg.test(text)', reg.test(text))

      // 文本节点并且存在{{}}字符
      if (node.nodeType === 3 && reg.test(text)) {
        let arr = RegExp.$1.split('.')
        let val = vm

        // 如果dom存在 {{a.b}} 则获取到a对象的b属性值
        arr.forEach(key => {
          val = val[key]
        })

        // 将文本内容 替换
        node.textContent = text.replace(reg, val).trim()
      }

      // 元素节点
      if (node.nodeType === 1) {
        let nodeAttr = node.attributes
        Array.from(nodeAttr).forEach(att => {
          let name = att.name
          let exp = att.value
          // console.log('att.name', att.name)
          // console.log('att.value', att.value)
          if (name.includes('v-model')) {
            node.value = vm[exp]
          }
        })
      }

      if (node.childNodes && node.childNodes.length) {
        replace(node)
      }

    })
  }

  replace(fragment)
  // 将虚拟DOM重新导入到真实DOM中
  vm._el.appendChild(fragment)
}

观察者 发布订阅

function Observe(data) {
  Object.keys(data).forEach(key => {
  ...
  +  let dep = new Dep()
      get() {
       ...
  +     Dep.target && dep.add(Dep.target)
      },
      set(newVal) {
         ...
  +      dep.notify(newVal)
      }
})

function Observe(data) {
          return
        }
        val = newVal
        dep.notify(newVal)
        observe(val) // 判断newVal是否为对象
      }
})

function Compile(el, vm) {
        ...
        // 文本节点并且存在{{}}字符
       if (node.nodeType === 3 && reg.test(text)) {       
 +       new Watcher(vm, RegExp.$1, function (newVal) {
 +         node.textContent = text.replace(reg, newVal).trim()
 +       })
       }

      if (node.nodeType === 1) {
        if (name.includes('v-model')) {
        ...
 +        new Watcher(vm, RegExp.$1, function (newVal) {
 +          node.value = text.replace(reg, newVal).trim()
 +        })
        }
      }
}

// 添加发布订阅
function Dep() {
  this.subs = []
}

// 存Watcher实例
Dep.prototype.add = function (sub) {
  this.subs.push(sub)
}

Dep.prototype.notify = function () {
  this.subs.forEach(sub => {
    sub.update()
  })
}

// 观察者
function Watcher(vm, exp, fn) {
  this.vm = vm
  this.exp = exp 
  this.fn = fn // 回调
  Dep.target = this
  let val = vm
  let arr = exp.split('.')
  arr.forEach(exp => {
    val = val[exp]
  })
  Dep.target = null
}

Watcher.prototype.update = function () {
  let val = this.vm
  let arr = this.exp.split('.')
  arr.forEach(key => {
    val = val[key]
  })
  this.fn(val)
}

验证下:


分享不易额,喜欢的话一定别忘了点💖!!!

只关注不点💖的都是耍流氓,只收藏也不点💖的也一样是耍流氓。

原文链接:juejin.im

上一篇:Electron入门篇
下一篇:前端高度还原设计稿(字体篇)

相关推荐

  • 那些前端MVVM框架是如何诞生的

    那些前端MVVM框架是如何诞生的 以下内容纯属扯淡,跟着文章思路慢慢看。。 刀耕火种时代 &lt;p id="userInfo"&gt; 姓名:&lt;span id="name"&gt;Gloria...

    2 年前
  • 通过简易项目浅析vue的生命周期并理解MVVM(不仅理论,图文并茂)

    文章写作提示, 文章还没有写完。。。 1.v-if 在组件上和组件根元素上的生命周期 2.附带vue-table 3.vue-cli构建的项目组件切换时的生命周期问题最后提问 开始前说一说 吐槽 vu...

    3 年前
  • 试着用Proxy 实现一个简单mvvm

    Proxy、Reflect的简单概述 Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

    2 年前
  • 设计模式之MVC,MVVM,MVP

    M: modelV: viewC: controlerP: presenterVM: view-model mvp 是mvc的演变,在mvp里,m和v不直接再有关联,他们的交互完全通过p层来管理m...

    5 个月前
  • 记mvvm的阶段性理解

    初识mvvm还是在入门vue的时候,看见官方文档说vue是个响应式的mvvm框架,当时哪会注意这个,管他什么vm,和我入门vue有毛线关系。是的,抛弃它你可以很愉快的入门,但是入门之后,必然会进入'深...

    1 年前
  • 简单实现 VUE 中 MVVM - step6 - Array

    看这篇之前,如果没看过之前的文章先移步看 简单实现 VUE 中 MVVM - step1 - defineProperty 简单实现 VUE 中 MVVM - step2 - Dep 简单实现 VU...

    3 年前
  • 盘点12部程序员必看电影

    1 黑客帝国媒体和技术批判理论,99年的电影,上个世纪的思考,豆瓣9.0分~ 2 源代码人死亡后大脑也能保持8分钟的回路,这个回路类似于容器,可装入其他记忆,可编程,这就是源代码区··· ··· ...

    4 个月前
  • 百度开源的 MVVM 组件框架 SAN - 12.6K,兼容 IE6

    A Flexible JavaScript Component Framework. HomePage Download NPM: $ npm i san CDN: &lt;script...

    2 年前
  • 电影天堂RN客户端V2.0发布

    电影天堂RN客户端V2.0 重新开始! 重新开始 两年前发布了第一个版本。 现在, 使用最新的react-native 0.57和全新的设计完成了V2.0 免责声明 本项目仅供学习交流使用,不得用于其...

    2 年前
  • 电影《动物世界》对战系统(Javascript)

    最近刷了一部电影《动物世界》,感概原来简单的“剪刀石头布”游戏还可以这么烧脑,强大的数据分析能力、对人性心理的灵敏嗅觉等。看完之后饶有兴致,于是便利用socket技术,实现了一个“动物世界”多人对战系...

    2 年前

官方社区

扫码加入 JavaScript 社区