前端必刷手写题系列 [23]

455 阅读5分钟

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。

平时被考到的 api 如果不知道或不清楚,直接问面试官就行, api 怎么用这些 Google 下谁都能马上了解的知识也看不出水平。关键是在实现过程,和你的编码状态习惯思路清晰程度等。

注意是简单实现,不是完整实现,重要的是概念清晰实现思路清晰,建议先解释清楚概念 => 写用例 => 写伪代码 => 再实现具体功能,再优化,一步步来。

33. 手写简单 Vue2.x 响应式原理

分析

首先响应式原理,请看官方文档,说的也是很清晰了,这个图非常好

image.png

基本原理就是:

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

当然 vue2.x 会有下面两个点要注意

  • Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

    • 但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
  • Vue 不能检测以下数组的变动:

    • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    • 当你修改数组的长度时,例如:vm.items.length = newLength
    • 你也可以使用 vm.$set 实例方法,vm.$set(vm.items, indexOfItem, newValue) 来处理

那么我们手写一个基本的响应式demo,应该从那些个方面思考呢?

根据上面的原理分解成这几块:

  • Observer
  • Watcher
  • Dep

其实你把这几块搞清楚是什么,以及他们之间的关系,响应式基本就明白了。

  • 首先 Observer 是遍历组件中的数据,并把这些数据经过 defineReactive 方法变成响应式的,也就是对数据进行劫持。从而在每次取值(get)、设值时(set) 都能做操作。

  • Watcher 每个组件实例都对应一个 watcher 实例,watcher 负责渲染和更新视图 (图中向左灰色平箭头)

    • 其中 watcher 分类型
      • 视图datawatcherrenderWatcher
      • 计算属性 ComputedwatchercomputedWatcher
      • 用户自己定义的watch选项中的watcheruserWatcher
  • Dep 依赖收集器,我们了解了 Observer 监听了数据变化,Watcher 用来更新视图,而 DepObserverWatcher 的桥梁,负责

    1. 记录渲染的 watcher
    2. 当被监听 data 变化时通知之前记住的 watcher 去更新视图

了解了这些,还建议看下之前的观察者模式的手写实现

手写实现

我们直接上代码, 一块块实现

function Observer(data) {
  if (!data || typeof data != 'object') return
  for (let key in data) {
    defineReactive(data, key);
  }
}

const defineReactive = function(obj, key) {
  let val = obj[key];
  // 如果 data 中有嵌套对象,需要递归去深度监听
  Observer(val)
  // 依赖收集器实例
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('get val');
      // 在对模板属性进行求值来触发依赖收集
      if (target) {
        dep.addSub(target)
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      console.log('set val')
      val = newVal;
      // 当值发生变更时,依赖收集器通知更新每个需要更新的Watcher去更新视图 update(),
      // 就是dep.subs中的所有依赖项
      dep.notify();
    }
  });
}

Observer 监听我们搞完了,下面解析代码模板,遇到{{}}或者 computed, 就要依赖收集过程

// 作为观察者的消息中心, 解耦属性的依赖和更新操作
class Dep {
  constructor() {
    this.subs = []
  }
  // 把对应 watcher 放入依赖收集器的 subs 中
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 通知 subs 中所有 watcher 去更新视图
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}

// 全局属性,用来配置 watcher
target = null

class Watcher {
  constructor(obj, key) {
    // 将 target 指向自己
    target = this
    this.obj = obj
    this.key = key
    this.value = obj[key]
    // 最后置空 target
    // 是为了防止 notify 触发时,不停的绑定 Watcher 与 Dep, 会死循环
    target = null
  }

  update() {
    this.value = this.obj[this.key]
    this.divChange(this.value)
  }

  // 更新视图, 改 DOM
  divChange(value) {
    document.querySelector('div').innerText = value
  }
}

var data = { name: 'keal' }

// 当然这些在 Vue 框架都帮你做了
Observer(data)
new Watcher(data, 'name')

// 现在你可以随便改 data.name 已经变成响应式了
data.name = 'neverMore'

// 你可以打开百度,打开 devtool 复制代码运行试试

今天就到这里。

另外向大家着重推荐下另一个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列 记得点赞哈

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考

  • vue 文档
  • 面试之道