从0开始实现响应式,仿Vue写Demo

988 阅读3分钟

前言

本文不扒源码,只需要基本得js知识就可以跟着写。跟着写可以体会一下Vue2得响应式实现。话不多说马上开始。

需求

首先我们要思考,响应式大概的效果是当js代码直接改变一个值时,页面中相应的内容需要及时更新。那要怎么知道js代码改变了一个值呢?没错,就是用defineProperty,用这个方法就可以在一个值改变时触发一个set的方法,我们可以在这个set方法里更新页面。当然,用Proxy也是一样的,这里就用defineProperty了。

响应式核心——defineProperty

想必看过点文章的都知道这个了,这里就不详细解释了,放个mdn的链接 defineProperty

先封装个方法

function defineReactive(obj, key) {
  let val = obj[key]
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //有的同学可能要问为什么要另外建一个变量来操作
      //因为如果这里返回obj[key]会再次触发reactiveGetter方法,会套娃递归到爆栈为止
      return val 
    },
    set: function reactiveSetter (newVal) {
      const value = val
      //这里的判断是避免一样的值会重复触发后续的行为
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal
    }
  })
}

响应方法有了,第二个问题是怎么知道要更新哪些视图,怎么更新。

如果我们把所有key对应要更新的视图保存一个对象,set的时候去找可以嘛。 {key:View}

当然可以,但是你写vue的时候有做过这种事嘛?没有,这太麻烦了。

其实我们拦截了set方法同时我们也拦截了get方法,当视图渲染时是必然会访问到变量而且触发get方法的,那我们就可以在get方法里把对应的视图关系保存好,在set里用。

响应式核心设计——观察者模式

很多人听到设计模式会有点懵,我们先忘掉这个词,把响应式实现出来时也许你就能理解了。

我们先实现一个类,在get里收集对应关系,在set里更新视图

function Dep() {
  this.subs = []//视图列表
}
Dep.target = null
Dep.prototype.depend = function() {//收集对应关系
  if(Dep.target) {
    this.subs.push(Dep.target)
  }
}
Dep.prototype.notify = function() {//通知视图更新
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

Dep就是depend的缩写,依赖的意思。

这里要特别讲解一下Dep.target这个全局属性,因为在get方法里,是没法传递视图进去的,所以必须通过一个全局属性来传递。update是视图更新的方法。

调整一下defineReactive方法

function defineReactive(obj, key) {
  let val = obj[key]
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      dep.depend()
      return val
    },
    set: function reactiveSetter (newVal) {
      const value = val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal
      dep.notify()
    }
  })
}

接着我们来简单实现一个vue

function Vue(options) {
  this._data = options.data
  this.__ob__ = new Observer(this._data)//把_data设为响应式
  Dep.target = this //把当前vue对象设为收集对象
  this.render()
}
Vue.prototype.render = function () {
  let app = document.getElementById('app')
  app.innerHTML = null
  let title = document.createElement('h4')
  title.innerText = this._data.title
  let content = document.createElement('p')
  content.innerText = this._data.content
  app.appendChild(title)
  app.appendChild(content)
}
Vue.prototype.update = function () {
  this.render()
}
function Observer(obj) {
  this.obj = obj
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {//遍历key,把属性转为响应式
    defineReactive(obj, keys[i])
  }
}

html文件

<!DOCTYPE html>
<html>
  <body>
    <div id="app"></div>
  </body>
</html>
<script src="index1.js"></script>
<script>
  let vm = new Vue({
    data: {
      title: 'Hello world!',
      content: 'Tech change the world'
    }
  })
</script>

ok我们看看效果

xy-20210811-160126.gif

nice,基本效果已经有了。不过出现了第一个问题。

image.png 在Dep.prototype.notify方法里打印subs数组时发现,同一个vue对象被重复添加了。实际上,也只有在初次渲染时需要去添加vue对象,所以在Vue方法里需要在render后把Dep.target置空

function Vue(options) {
  this._data = options.data
  this.__ob__ = new Observer(this._data)//把_data设为响应式
  Dep.target = this //把当前vue对象设为收集对象
  this.render()
  Dep.target = null
}

Ok问题解决。

接下来的问题是我们希望它角色分工更加明确,不要直接取vue对象来操作,那么我们新建一个类

function Watcher(vm) {
  this.vm = vm
  Dep.target = this
  vm.render()
  Dep.target = null
}
Watcher.prototype.update = function() {
  this.vm.render()
}

这个就是观察者了,感受一下这个角色的逻辑。观察数据变化,通知视图更新。

调整一下vue方法

function Vue(options) {
  this._data = options.data
  this.__ob__ = new Observer(this._data)
  this.watcher = new Watcher(this)
}

image.png

ok没问题。

相信大家大概可以理解这个观察者模式了把。

示例代码