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

648 阅读3分钟

如果是用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)
}

验证下: 完整的git地址


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

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