vue3的数据响应原理和实现

更新日期: 2019-10-07 阅读: 2.8k 标签: 原理

话说vue3已经发布,就引起了大量前端人员的关注,木得办法,学不动也得硬着头皮学呀,本篇文章就简单介绍一下「vue3的数据响应原理」,以及简单实现其reactive、effect、computed函数,希望能对大家理解vue3响应式有一点点的帮助。话不多说,看下面栗子的代码和其运行的结果。

<div id="root"></div>
<button id="btn">年龄+1</button>
const root = document.querySelector('#root')
const btn = document.querySelector('#btn')
const ob = reactive({
  name: '张三',
  age: 10
})

let cAge = computed(() => ob.age * 2)
effect(() => {
  root.innerhtml = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})
btn.onclick = function () {
  ob.age += 1
}

上面带代码,是每点击一次按钮,就会给obj.age + 1 然后执行effect,计算属性也会相应的 ob.age * 2 执行。

所以,针对上面的栗子,制定一些小目标,然后一一实现,如下:

  • 1、实现reactive函数
  • 2、实现effect函数
  • 3、把reactive 和 effect 串联起来
  • 4、实现computed函数


实现reactive函数

reactive其实数据响应式函数,其内部通过es6的proxy api 来实现,
下面面其实通过简单几行代码,就可以对一个对象进行代理拦截了。

const handlers = {
  get (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
}
function reactive (target) {
  observed = new Proxy(target, handlers)
  return observed
}
let person = {
  name: '张三',
  age: 10
}

let ob = reactive(person)

但是这么做的话有缺点,1、重复多次写ob = reactive(person)就会一直执行new Proxy,这不是我们想要的。理想情况应该是,代理过的对象缓存下来,下次访问直接返回缓存对象就可以了;2、同理多次这么写ob = reactive(person); ob = reactive(ob) 那也要缓存下来。下面我们改造一下上面的reactive函数代码。

const toProxy = new WeakMap() // 缓存代理过的对象
const toRaw = new WeakMap() // 缓存被代理过的对象
// handlers 跟上面的一样,为了篇幅这里省略
function reactive (target) {
  let observed = toProxy.get(target)
  // 如果是缓存代理过的
  if (observed) {
    return observed
  }
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // 缓存observed
  toRaw.set(observed, target) // 缓存target
  return observed
}

let person = {
  name: '张三',
  age: 10
}

let ob = reactive(person)
ob = reactive(person) // 返回都是缓存的
ob = reactive(ob) // 返回都是缓存的

console.log(ob.age) // 10
ob.age = 20
console.log(ob.age) // 20

这样子调用reactive()返回都是我们第一次的代理对象啦(ps:WeakMap是弱引用)。缓存做好了,但是还有新的问题,如果代理target对象层级嵌套比较深的话,上面的proxy是做不到深层代理的。例如

let person = {
  name: '张三',
  age: 10,
  hobby: {
    paly: ['basketball', 'football']
  }
}
let ob = reactive(person)
console.log(ob)


从上面的打印结果可以看出hobby 对象没有我们上面的handlers 代理,也就是说当我们对hobby做一些依赖收集的时候是没有办法的,所以我们改写一下handlers对象。

// 对象类型判断
const isObject = val => val !== null && typeof val === 'object'
const toProxy = new WeakMap() // 缓存代理过的对象
const toRaw = new WeakMap() // 缓存被代理过的对象
const handlers = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // TODO: effect 收集
    return isObject(res) ? reactive(res) : res
  },
  set (target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    // TODO: trigger effect
    return result
  }
}
function reactive (target) {
  let observed = toProxy.get(target)
  // 如果是缓存代理过的
  if (observed) {
    return observed
  }
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // 缓存observed
  toRaw.set(observed, target) // 缓存target
  return observed
}

上面的代码通过在get里面添加 return isObject(res) ? reactive(res) : res,意思是当访问到某一个对象时候,如果判断类型是「object」,那么就继续调用reactive代理。上面也是我们的reactive函数的完整代码。


实现effect函数

到了这里离我们的目标又近了一步,这里来实现effect函数,首先我们先看看effect的用法。

effect(() => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})

第一感觉看起来很简单嘛,就是函数当做参数传进去,然后调用传进来函数,完事。下面代码最简单实现

function effect(fn) {
  fn()
}

但是到这里,所有人都看出来缺点了,这只是执行一次呀?怎么跟响应式联系起来呀?还有后面computed怎么基于这个实现呀?等等。带着一大堆问题,通过改写effect和增加effect功能去解决这一系列问题。

function effect (fn, options = {}) {
  const effect = createReactiveEffect(fn, options)
  // 不是理解计算的,不需要调用此时调用effect
  if (!options.lazy) {
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function effect(...args) {
    return run(effect, fn, args) // 里面执行fn
  }
  // 给effect挂在一些属性
  effect.lazy = options.lazy
  effect.computed = options.computed
  effect.deps = []
  return effect
}

在createReactiveEffect函数中:创建一个新的effect函数,并且给这个effect函数挂在一些属性,为后面做computed准备,这个effect函数里面调用run函数(此时还没有实现), 最后在返回出新的effect。

在effect函数中:如果判断options.lazy是false就调用上面创建一个新的effect函数,里面会调用run函数。


把reactive 和 effect 串联起来

其实上面还没有写好的这个run函数的作用,就是把reactive 和 effect 的逻辑串联起来,下面去实现它,目标又近了一步。

const activeEffectStack = [] // 声明一个数组,来存储当前的effect,订阅时候需要
function run (effect, fn, args) {
  if (activeEffectStack.indexOf(effect) === -1) {
    try {
      // 把effect push到数组中
      activeEffectStack.push(effect)
      return fn(...args)
    }
    finally {
      // 清除已经收集过得effect,为下个effect做准备
      activeEffectStack.pop()
    }
  }
}

上面的代码,把传进来的effect推送到一个activeEffectStack数组中,然后执行传进来的fn(...args),这里的fn就是

fn = () => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
}

执行上面的fn访问到ob.name、ob.age、cAge.value(这是computed得来的),这样子就会触发到proxy的getter,就是执行到下面的handlers.get函数

const handlers = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // effect 收集
    track(target, key)
    return isObject(res) ? reactive(res) : res
  },
  set (target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    const extraInfo = { oldValue: target[key], newValue: value }
    // trigger effect
    trigger(target, key, extraInfo)
    return result
  }
}

聪明的小伙伴看到这里已经看出来,上面handlers.get函数里面track的作用是依赖收集,而handlers.set里面trigger是做派发更新的。
下面补全track函数代码

// 存储effect
const targetMap = new WeakMap()
function track (target, key) {
  // 拿到上面push进来的effect
  const effect = activeEffectStack[activeEffectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      depsMap = new Map()
      // targetMap如果不存在target 的 Map 就设置一个
      targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (dep === void 0) {
      dep = new Set()
      // 如果depsMap里面不存在key 的 Set 就设置一个
      depsMap.set(key, dep)
    }
    if (!dep.has(effect)) {
      // 收集当前的effect
      dep.add(effect)
      // effect 收集当前的dep
      effect.deps.push(dep)
    }
  }
}

看到这里呀,大家别方,上面的代码意思就是,从run函数里面的activeEffectStack拿到当前的effect,如果有effect,就从targetMap里面拿depsMap,targetMap如果不存在target 的 Map 就设置一个targetMap.set(target, depsMap),再从depsMap 里面拿 key 的 Set ,如果depsMap里面不存在 key 的 Set 就设置一个depsMap.set(key, dep),下面就是收集前的effect和effect 收集当前的dep了。收集完毕后,targetMap的数据结构就类似下面的样子的了。

// track的作用就是完成下面的数据结构
targetMap = {
  target: {
    name: [effect],
    age: [effect]
  }
}
// ps: targetMap 是WeakMap 数据结构,为了直观和理解就用对象表示
//     [effect] 是 Set数据结构,为了直观和理解就用数组表示

track执行完毕之后,handlers.get就会返回 res,进行一系列收集之后,fn执行完毕,run函数最后就执行finally {activeEffectStack.pop()},因为effect已经收集结束了,清空为了下一个effect收集做处理。

依赖收集已经完毕了,但是当我们更新数据的时候,例如ob.age += 1,更改数据会触发proxy的getter,也就是会调用handlers.set函数,里面就执行了trigger(target, key, extraInfo),trigger函数如下

// effect 的触发
function trigger(target, key, extraInfo) {
  // 拿到所有target的订阅
  const depsMap = targetMap.get(target)
  // 没有被订阅到
  if (depsMap === void 0) {
    return;
  }
  const effects = new Set() // 普通的effect
  const computedRunners = new Set() // computed 的 effect
  if (key !== void 0) {
    let deps = depsMap.get(key)
    // 拿到deps订阅的每个effect,然后放到对应的Set里面
    deps.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
  const run = effect => {
    effect()
  }
  // 循环调用effect
  computedRunners.forEach(run)
  effects.forEach(run)
}

上面的代码的意思是,拿到对应key的effect,然后执行effect,然后执行run,然后执行fn,然后就是get上面那一套流程了,最后拿到数据是更改后新的数据,然后更改视图。

下面简单弄一个帮助理解的流程图,实在不能理解,大家把仓库代码拉下来,debuger执行一遍

targetMap = {
  name: [effect],
  age: [effect]
}
ob.age += 1 -> set() -> trigger() -> age: [effect] -> effect() -> run() -> fn() -> getget() -> 渲染视图


实现computed函数

还是先看用法,let cAge = computed(() => ob.age * 2),上面写effect的时候,有很多次提到为computed做准备,其实computed就是基于effect来实现的,下面我们看代码

function computed(fn) {
  const getter = fn
  // 手动生成一个effect,设置参数
  const runner = effect(getter, { computed: true, lazy: true })
  // 返回一个对象
  return {
    effect: runner,
    get value() {
      value = runner()
      return value
    }
  }
}

值得注意的是,我们上面 effet函数里面有个判断

if (!options.lazy) {
  effect()
}

如果options.lazy为true就不会立刻执行,就相当于let cAge = computed(() => ob.age * 2)不会立刻执行runner函数,当cAge.value才真正的执行。

最后,所有的函数画成一张流程图。

如果文章有哪些不对,请各位大佬指出来,我有摸鱼时间一定会修正过来的。


至此,所有的的小目标我们都已经完成了

ps:源码地址(大家可以clone下来执行一遍)
博客文章地址(这里有新的阅读体验,也有微信,欢迎来撩)

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/5813

相关推荐

React Native之原理浅析

讲React Native之前,了解JavaScriptCore会有帮助,也是必要的。React Native的核心驱动力就来自于JS Engine. 你写的所有JS和JSX代码都会被JS Engine来执行, 没有JS Engine的参与,你是无法享受ReactJS给原生应用开发带来的便利的

连v-show都不会你还敢说熟悉 Vue 原理?

Vue 作为最主流的前端框架,中文资料齐全、入门简单、生态活跃,可以说是工作中最常用的,如今对 Vue 原理的熟悉基本上是简历的标配了。之前参与了部分 2019 校园招聘的面试工作,发现很多简历上都写了:

Angular ZoneJS 原理

如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测

js中flat方法的实现原理

Array.prototype.flat()在Array的显示原型下有一个flat方法,可以将多维数组,降维,传的参数是多少就降多少维,自定义flat的步骤1、第一步是类型判断,需要判断当前调用方法的this是否为一个数组,若不是数组则返回undefined

JavaScript 中的函数式编程原理

做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。

理解Vue的Watch原理

watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。

写一个简单的vue-router来剖析原理

随着前端业务的发展, 我们一般在写一个较为大型的vue项目时候,会使用到vue-router,来根据指定的url或者hash来进行内容的分发,可以达到不像服务端发送请求,就完成页面内容的切换,能够减少像服务器发送的请求

CSS定位之BFC背后的神奇原理

BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与

前端手写代码原理实现

现在的前端门槛越来越高,不再是只会写写页面那么简单。模块化、自动化、跨端开发等逐渐成为要求,但是这些都需要建立在我们牢固的基础之上。不管框架和模式怎么变,把基础原理打牢才能快速适应市场的变化。下面介绍一些常用的源码实现

关于vue过滤器的原理解析

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!