IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 你这手写vue2.x/3.x的响应式原理保熟吗? -> 正文阅读

[JavaScript知识库]你这手写vue2.x/3.x的响应式原理保熟吗?

vue源码限于水平,看的乱七八糟,只是看了一些文章解析有些收获,遂记。

现在vue3已经出来了,是不是研究vue2.x过时了?非也非也。

抛去大部分vue使用者还在用的情况,vue3的很多思路都可以在vue2中得到体现,所以学习还是有必要的。

这篇文章就聊聊到响应式原理,从百草园到三味书屋,哦不,从 Vue2 到 Vue3。 聊Vue2响应式就必须聊聊 Objet.defineProperty?

talk is cheap…

Objet.defineProperty

用过 vue 的都知道,vue 如果有个数据 firstName 改变了,那么相应用到 firstName 的地方都得改变,
比如
计算属性(computed

computed: {
	fullName: () => this.firstName + this.lastName
}

数据监听(watch

watch: {
    firstName: function (val, oldVal) {
      // dosomething
    }
}

模板渲染(render

<span>姓: {{ firstName }}<span>

这三个地方就相当于 firstName 的依赖(Watcher),后面细讲。

另外 Vue 中数据定义是这样的

new Vue({
	el: '#app',
	data: { firstName: '王' }
})

那么怎么才能监听到 fisrtName 的改变并更新相应的数据呢?

在 Object.defineProperty 之前, 是 MutationObserver -MDN ?,他的作用如下

MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品

这是已经淘汰的东西,不多做介绍, vue2.x 做法就是利用 Objet.defineProperty 监听 data 的改变从而去触发各个部分的更新。

做个简单的介绍,可以猛戳 这里? 了解更多.
以上面为例

const data = { firstName: '李' };
const vm = {};
Object.defineProperty(vm, "firstName", {
  enumerable: true,
  configurable: true,
  get() { console.log('我被获取了!'); return data.firstName; },
  set(newValue) { 
    data.firstName = newValue;
    console.log('我被改变了, 这是新值:', newValue) 
  },
});
vm.firstName = '李';
console.log(vm.firstName)
// 我被改变了, 这是新值: 李
// 我被获取了!
// 李

上面给vm 定义一个 响应式 firstName 属性,并且这个属性可枚举(enumerable : true)、可配置(configurable : true)。firstName 改变就就会触发 set 方法,获取 firstName 就会触发 get 方法。

把响应式核心 实现方法说了,再来理一理 vue 响应式实现原理。

vue响应式原理

先上一张图,来自 掘金小册(推荐!) 剖析 Vue.js 内部运行机制?

上面的图基本就很经典的概括了 Vue 的生命周期。包含 初始化、编译、挂载、更新 。。。而Vue的响应式也基本贯穿了整个周期。

另外注意:

  • 上图 watcher 不是代码中的 vue 中的 watch ,而是依赖
  • watcher(依赖) 有三类: computed watcherwatch watcherrender watcher,这里提一嘴,后面再说。

关于响应式原理这张图概括的比较全了👍(看完手写,建议回头再来看看这张图)。但凡看着好的我都采用拿来主义🤪
在这里插入图片描述图来自 图解 Vue 响应式原理?

手写一个vue2.x响应式

本节主要参考 (摘抄)😅 手写一个简易vue响应式带你了解响应式原理 ?

效果 如下(v-model, v-show, v-text {{}})
请添加图片描述

原版实现

首先回忆一下 Vue 的使用

new Vue({
  el: '#app',
  data: {
      name: 'ethan',
      text: 'text',
    }
})

让我们由 Vue 类切入

Vue

首先需要定义一个 Vue 类, 需要完成以下几点功能:

  1. 将 data 变为响应式。
  2. 能编译模板,识别其中绑定的数据。

对于第一点 利用 Observer 将数据变为响应式,另外为了 在模板 template 中 使用 {{ firstName }} 而不是 {{ data.firsName }}, 那么需要将 data 的属性映射到 Vue 中方便直接调用,利用 _proxyData 方法实现。
对于第二点 专门写个 Compiler 实现。

class Vue {
  constructor(options) {
    this.$options = options || {}
    // 传ID或者Dom都可以。
    this.$el = typeof options.el === 'string' ?
      document.querySelector(options.el) : options.el;
    this.$data = options.data;
    // 处理data中的属性 
    this._proxyData(this.$data);
    // 将data变为响应式
    new Observer(this.$data)  
    // 模板编译
    new Compiler(this)
  }
  // 将data中的属性注册到vue
  _proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get(){ return data[key]  },
        set (newValue) {
          if(newValue === data[key]) return;
          data[key] = newValue;
        }
      })
    })
  }
}

Observer

再来实现 Observer, 它作用如下:

  1. 将对象数据每个属性及其子属性变为响应式,

对于这一点, 遍历每个属性将其变为响应式,利用 方法 walk 实现.对于单个属性的响应式,利用 Object.defineProperty 处理, 封装 为 defineReactive 方法。

注意:

  • 利用 walk 递归,使data的每一个属性都具有响应性,包含新更改的值
  • 每次在获取的时候添加依赖.
  • 每次数据更新时通知 Dep 通知(notify)更新

于是有下面代码

/*
 将数据变为响应式对象
*/
class Observer {
  constructor(data) {
    this.walk(data);
  }
  walk(data) {
    if(!data || typeof data != 'object') return;
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    })
  }
  defineReactive(obj, key, value) {
    // 递归的将对象子属性变为响应式
    this.walk(value);
    const self= this;
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get(){
      	  //这里 target 即为 Watcher, 存在即添加依赖
        Dep.target && dep.addSub(Dep.target)
        // 注意,这里返回 Obj[key] 会爆栈!!(死循环)
        return value
      },
      set (newValue) {
        // 旧值与新值相同则不更新
        if(newValue === obj[key]) return;
        value = newValue;
        // 如果新值是
        self.walk(newValue)
        // 更新视图
        dep.notify()
      }
    })
  }
}

Compiler

接下来 是 模板编译 Compiler 的实现,作用如下

  1. 解析 Dom 中绑定的数据
  2. 解析 Dom 中的指令(v-modelv-text等)

实现上面两点,只需要 把 dom 当成一个字符串 利用正则解析即可。

注意

  • 文本节点,元素节点有不同的解析方法,针对性处理
  • 对于指令,通过获取 js 获取 Dom 自定义属性即可解决
  • 对于input节点需要监听改变。利用 change 事件

其实除了文本节点、元素节点还有很多节点需要针对性处理,想要了解更多,建议查看 vue-design—渲染器之挂载?

下面是 Compiler 的实现

//解析模板template 内容,变成dom树
class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compile(this.el)
  }
  compile(el) {
    let childrenNodes = [...el.childNodes]
    // 遍历每一个节点 针对性的解析
    childrenNodes.forEach(node => {
      if(this.isTextNode(node)){
        this.compileText(node)
      }else if(this.isElementNode(node)) {
        this.compileElement(node)
      }
      if(node.childNodes && node.childNodes.length) this.compile(node)
    })
  }
  // 文本节点编译
  compileText(node){
    let reg  = /\{\{(.+?)\}\}/
    let val = node.textContent
    if(reg.test(val)){
      let key = RegExp.$1.trim()
      const value = this.vm[key];
      node.textContent = val.replace(reg, value)
      new Watcher(this.vm, key, (newVal) => {
        node.textContent = newVal
      })
    }
  }
  // 元素节点编译
  compileElement(node) {
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attributes
    ![...node.attributes].forEach(attr => {
      let attrName = attr.name
      if(this.isDirective(attrName)){
        attrName = attrName.substring(2);
        let key = attr.value;
        this.update(node, key, attrName)
      }
    })
  }
  // 动态运行不同的更新方法
  update(node, key, attrName) {
    let updateFn = this[attrName+'Update']
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }
  // 文本节点的值有更新就会重新渲染。本质还是利用 js修改 textContent
  textUpdate(node, key, content ){
    node.textContent = content
    new Watcher(this.vm, key, newVal => { node.textContent = newVal })
  }
  // v-model 更新
  modelUpdate(node, key, value) {
    const typeAttr = node.getAttribute('type')
    // 对于输入框,做了过滤
    if(typeAttr == "text") {
      node.value = value;
      new Watcher(this.vm, key, newVal => { node.value = newVal})
      node.addEventListener('keyup', () => {
        this.vm.$data[key] = node.value
      })
    }
  }
  isDirective(attr) {
    return attr.startsWith('v-')
  }
  isTextNode(node){
    return node.nodeType === 3
  }
  isElementNode(node) {
    return node.nodeType === 1
  }
}

可以看到,上面三处添加了 Watcher (Watcher 的实现后面会讲)

  • 文本节点更新时(compileText), 如 <span>{{ name }}</span>
  • 自定义指令 v-text 更新时(textUpdate), 如<span v-text="name"></span>
  • 自定义指令 v-modal 更新时(modelUpdate), 如<input v-modal="name"></input>

因为在上述三种情况会产生name的依赖(前面提到的 三种Watcher 中的一种 render Watcher)。

Dep

再来说说 Dep 的实现,主要作用

  • 收集依赖 和 更新依赖

主要是暴露方法 addSubnotify 。请看下面👇

// 收集依赖 和 通知依赖更新
class Dep {
 constructor() {
    this.subs = []
  }
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Watcher

最后说说 Watcher, 文中说的最多的 依赖 就是它!作用是什么呢?

  • 更新 Dep 的 target(值为 Watcher 本身)
  • 利用 回调函数 cb 触发更新

代码如下

class Watcher {
  constructor(vm, key, cb){
    this.vm = vm
    this.key = key
    this.cb = cb
    Dep.target = this
    this.oldVal = vm[key]
    Dep.target = null
  }
  update(){
    let newValue = this.vm.$data[this.key]
    if(newValue === this.oldVal) return;
    // 调用回调更新
    this.cb(newValue)
  }
}

还记得吗 ? 在 前面 Observe 定义响应时有这一段代码👇

class Observer{
	//...
    get(){
      Dep.target && dep.addSub(Dep.target)
      return value
    },
}

每次在实例化 Watcher 的时候就为 Dep 添加 target, 每次执行 data[prop] 的时候 就会进行依赖收集

好了,终于都写的差不多了。测试一下看看?

测试一下

麻雀虽小,五脏俱全了,不过他们的执行顺序需要做些调整:

<body>
  <div id="app">
    <input type="text" v-model="name" /><br/>
    <b>姓名:</b><span>{{ name }}</span> <br/>
    <b>性别:</b><span>{{ sex }}</span>
    <hr />
    <div v-text="text"></div>
  </div>
  <script>
		// 依赖收集和更新
		class Dep { //... }
		// 依赖,暴露更新方法 notify
		class Watcher { //... }
		// 解析模板 Dom 中的依赖 
		class Compiler { //... }
		// 将Data变为响应式
		class Observer { //.. }
		// Vue 实例
		class Vue { //.. }
		
		new Vue({
		  el: '#app',
		  data: {
		      name: 'ethan',
		      sex: '男',
		      text: 'text',
		    }
		})
  </script>
</body>

请添加图片描述
当然,v-text 也是双向绑定,主要是 compileText 的作用。改成 name 试一下

<body>
  <div id="app">
    <input type="text" v-model="name" /><br/>
    <b>姓名:</b><span>{{ name }}</span> <br/>
    <b>性别:</b><span>{{ sex }}</span>
    <hr />
    <div v-text="name"></div>
  </div>
  <script>
  	//...
  </script>
</body>

请添加图片描述

好像可以哎~~~,哇哦🤸?♂?🤸?♂?🤸?♂?,不得不说我抄写代码有点🤏东西,家人们,点个赞吧🤡 什么?不给?没事😁请继续看下去。

修复原版bug和扩展v-show

Bug发现

上面实现了,我就有个想法,性别通过选择更改可以不可以呢🤷?♂??
说干就干,首先就要监听选择更改的值,然后将 data.sex 赋值为新值不就好了。

当时心想怪简单的,结果我折腾了好一会🤣,都忘完了。

html

<div id="app">
  <input type="text" v-model="name" /><br/>
  <input id="male" name="sex" type="radio" v-model="sex" value="">
    <label for="male"></label>
  </input>
  <input id="female" name="sex" type="radio" v-model="sex" value="">
    <label for="female"></label>
  </input><br/>
  <b>姓名:</b><span>{{ name }}</span> <br/>
  <b>性别:</b><span>{{ sex }}</span>
  <hr />
  <div v-text="text"></div>
</div>

js

//...
class Compiler {
  constructor(vm) {}
  compile(el) {}
  compileText(node){}
  compileElement(node) {}
  update(node, key, attrName) {
    let updateFn = this[attrName+'Update']
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }
  // 文本节点的值有更新就会重新渲染。本质还是利用 js修改 textContent
  textUpdate(node, key, content ){}
  modelUpdate(node, key, value) {
    const typeAttr = node.getAttribute('type')
    if(typeAttr == "text") {
    }
    // 增加这里
    else if(typeAttr === "radio") {
    	// 可以不用 Wather 也可以 用个Watcher 去更改类名, 在下一小节展示
      const nameAttr = node.getAttribute('name')
      // 这里不需要 watch, 
      node.addEventListener('change', (ev) => {
        this.vm.$data[key] = ev.target.value
      })
    }
  }
  isDirective(attr) {}
  isTextNode(node){}
  isElementNode(node) {}
}

请添加图片描述
可以看到,第一次切换 "女" 时更新,后面的切换都不更新了?为啥,各位同志们可以先想想💭。

不卖关子了,原因就是 旧值 this.oldVal 是在 new Watcher 时确定,后面在更新"男"=> "女" 时,自然更新 ;当再选择 "男" ,上次选择 "女" 时Watcher 中的 this.oldVal 并没有更新,所以值相同不会触发 Watcher,具体代码在 Watcherupdate 方法内体现。

当再选择 "女" 虽然触发了 Watcher, 但是看起来也没有改变(原来的值就为 "女"

找到原因就好改了。在 notify 中将旧值传入,在 Watcherupdate 中接收并更新 this.oldVal,具体就是

class Dep {
	//...
  notify(oldValue){
  	// 接收旧值↑ 并传递↓
    this.subs.forEach(sub => {
      sub.update(oldValue)
    })
  }
}
class Watcher {
	//...
  update(oldValue){
  	// 接收↑ 并更新旧值↓
    this.oldVal = oldValue;
    let newValue = this.vm.$data[this.key]
    if(newValue === this.oldVal) return;
    this.cb(newValue)
  }
}
class Observer {
  //...
  defineReactive(obj, key, value) {
    //...
    Object.defineProperty(obj, key, {
    	//...
      set (newValue) {
      	  // 拿旧值
        const oldval = obj[key]
        if(newValue === obj[key]) return;
        value = newValue;
        self.walk(newValue)
        // 更新视图,并传递旧值
        dep.notify(oldval)
      }
    })
  }
}

这样就可以了。
请添加图片描述

扩展v-show

有了前面的实现, v-show 实现起来就简单了,主要监听输入,更新值即可。

css

<style>
  .show-txt {
    opacity: 1;/* 为了观赏性,不用display */
    transition: all .5s linear;
  }
  .show-txt.hidden {
    opacity: 0;
    color: #eee;
  }
</style>

html

<div id="app">
  <input type="text" v-model="name" /><br/>
  <input id="male" name="sex" type="radio" v-model="sex" value="">
    <label for="male"></label>
  </input>
  <input id="female" name="sex" type="radio" v-model="sex" value="">
    <label for="female"></label>
  </input><br/>
  <input name="show" type="checkbox" v-model="show" checked>是否展示</input>
  <div class="show-txt" v-show="show">展示文本示例</div><br/>
  <b>姓名:</b><span>{{ name }}</span> <br/>
  <b>性别:</b><span>{{ sex }}</span>
  <hr />
  <div v-text="text"></div>
</div>

js

//...
class Compiler {
	//..
  modelUpdate(node, key, value) {
    const typeAttr = node.getAttribute('type')
    if(typeAttr == "text") {}
    else if(typeAttr === "radio") {}
    else if(typeAttr === 'checkbox') { // 监听值
      node.addEventListener('change', (ev) => {
        this.vm.$data[key] = ev.target.checked
      })
    }
  }
  // v-show 指令绑定值更新的回调
  showUpdate(node, key, value){
    const change = (val) => { 
      const operate = !!val ? 'remove' : 'add';
      node.classList[operate]('hidden') 
    }
    change(value);
    new Watcher(this.vm, key, (newVal) => { change(newVal) })
  }
  // ...
}
//...

// 测试
new Vue({
  el: '#app',
  data: {
      name: 'ethan',
      sex: '男',
      text: 'text',
      show: true,
    }
})

请添加图片描述
好了目前就实现到到这里了,Vue2的响应式原理手写实现告一段落。如果你更倾向源码去寻找答案 推荐 Vue 源码解读(3)—— 响应式原理 ?

当然上面只是对象属性的更新,Vue源码对数组的更新检测利用重写原型的方法。下面稍作介绍。

数组检测

以下八种数组方法,vue是可以检测其改变的,

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

其做法就是在数组原型拦截这些方法,可从源码窥其一二。

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // 缓存原始方法
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    // 拿到全局的观察者,相当于上面的 Observer
    var ob = this.__ob__;
    var 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
  });
});

扩展:Vue3的响应式实现

Vue3对Vue2来说大大的升级了,本文只探究响应式原理,这里大概总结一下。

Vue3响应式原理

先来个对比。

Vue2.x

  • 基于 Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
  • Object.defineProperty 无法检测到对象属性的添加和删除 。
  • 由于Vue会在初始化实例时对属性执行 getter/setter 转化,所有属性必须在 data 对象上存在才能让 Vue 将它转换为响应式。
  • 深度监听需要一次性递归,对性能影响比较大。

Vue3
- 基于 ProxyReflect,可以原生监听数组,可以监听对象属性的添加和删除。
- 不需要一次性遍历 data 的属性,可以显著提高性能。
- 因为 Proxy 是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。

对于Vue3的响应式,网上?找到一张图,总结比较贴切。
在这里插入图片描述

关于 Proxy

相比 Object.definePropertyProxy? 支持的对象操作十分全面:getsethasdeletePropertyownKeysdefineProperty…等等

掘金不少讲的好的文章 🏸- proxy,不再赘述,给一个例子,可以带着好奇去对 Proxy 扫盲。

let data = [1, 2, 3]
let p = new Proxy(data, {
    get(target, key, receiver) {
        console.log('get value:', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set value:', key, value)
        Reflect.set(target, key, value, receiver)
        return true // 在严格模式下,若set方法返回false,则会抛出一个 TypeError 异常。
    }
})
p.length = 4 // set value: length 4
console.log(data) // [1, 2, 3, empty]
p.shift()
// get value: shift
// get value: length
// get value: 0
// get value: 1
// set value: 0 2
// get value: 2
// set value: 1 3
// set value: length 3
p.push(4)
// get value: push
// get value: length
// set value: 3 4
// set value: length 4
p
// Proxy {0: 2, 1: 3, 3: 4}
p[3] = 0
// set value: 3 0

手写响应式

我要开始动手了!什么?动手?不是不是,亲爱读者们,我是说我要动手开代码了

主要的函数如下:

仅涉及 Composition API 响应式原理实现,没有涉及到编译部分(Dom部分)

/* 依赖收集:建立 数据&cb 映射关系 */
function track(target, key){}

/* 触发更新:根据映射关系,执行cb */
function trigger(target, key){}

/* 建立响应式数据 */
function reactive(obj){}

/* 声明响应函数cb(依赖响应式数据) */
function effect(cb){}

限于篇幅且代码也不多,这里就直接给代码了,请参考注释阅读。

代码来自(建议精读): 林三心画了8张图,最通俗易懂的Vue3响应式核心原理解析?

const targetMap = new WeakMap()
// 收集依赖(effect)
function track(target, key) {
    // 如果此时activeEffect为null则不执行下面
    // 这里判断是为了避免例如console.log(person.name)而触发track
    if (!activeEffect) return
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, depsMap = new Map())
    }

    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, dep = new Set())
    }
    dep.add(activeEffect) // 把此时的activeEffect添加进去
}
// 触发更新
function trigger(target, key) {
    let depsMap = targetMap.get(target)
    if (depsMap) {
        const dep = depsMap.get(key)
        if (dep) {
            dep.forEach(effect => effect())
        }
    }
}
// 定义响应式,劫持数据
function reactive(target) {
    const handler = {
   			// reciver 为 Proxy 对象
        get(target, key, receiver) {
            track(receiver, key) // 访问时收集依赖
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            Reflect.set(target, key, value, receiver)
            trigger(receiver, key) // 设值时自动通知更新
        }
    }

    return new Proxy(target, handler)
}
let activeEffect = null
// 依赖存储,其实也可以用个栈来存依赖,下面 的实现方式 是不是多少// 有点熟悉,没错 vue2 响应式实现时是这样干的
function effect(fn) {
    activeEffect = fn
    activeEffect()
    activeEffect = null
}

上面代码如果理解比较吃力,建议查看上面提到 原文 👆以及文章 Vue3响应式原理 + 手写reactive?(看完记得回来,我后面有一些高质量扩展给到大家🤡),讲的比较细。

主要用了 Proxy 劫持数据(替代 Object.defineProperty), 其他方面比如依赖收集、触犯更新实现和 Vue2 基本一样。

测试一下

const data = { name: '二蛋' }
const rData = reactive(data)
// 有一个 data.name 的依赖
effect(() => { 
    console.log('我依赖二蛋', rData.name); 
})
rData.name = '王二蛋'
// 我依赖二蛋 二蛋
// 我依赖二蛋 王二蛋

除了上面的 reactive,还有个 ref, 是针对单个变量响应式设计的。另外 reactive 实现了,ref 其实是 基于reactive 的扩展, 仅 属性 value 为响应式!

  • ref
function ref(initValue) {
 	return reactive({
      value: initValue
   })
}

基于上面代码,其它的响应式 API? computedwatchEffectwatch也可以实现了。

  • watchEffect
function watchEffect(fn) {
	effect(() => fn())
}
const eData = ref(5);
    watchEffect(() => { console.log('effect测试: ', eData.value) })
    eData.value = 666
}
// effect测试:  5
// effect测试:  666
  • Computed
function computed(fn) {
    const result = ref()
    effect(() => result.value = fn())
    return result
}
const ref1 = ref(5);
const cRef1 = computed(() => {
    console.log('computed测试: ', ref1);
    return ref1.value
});
ref1.value = 666;
console.log('last: ', ref1, cRef1)
// computed测试:  Proxy {value: 5}
// computed测试:  Proxy {value: 666}
  • watch

支持监听单个值和多个值,并且新旧值都要回传,另外要注意的是 监听多个源时 某一个 更新,要触发回调 fn 从而精确更新(map实现)。当然前面的代码要稍作修改,具体如下:

function track(target, key) {
    if (!activeEffect.fn) return
    //...
    dep.add(activeEffect.fn) // 原来的回调我放在了 fn 上
    // 此时的 dep 才是后面更新的 dep 将其当做更新的 key
    activeEffect.key = dep;
}

function trigger(target, key, { oldValue }) {
    //...
    // 旧值和dep(作为key)一起传递
    dep.forEach(effect => effect(oldValue, dep))
}

function reactive(target) {
    const handler = {
        //...
        set(target, key, value, receiver) {
        	// 获取旧值以返回
            const oldValue = Reflect.get(target, key, receiver)
            if(value === oldValue) return;
            const result = Reflect.set(target, key, value, receiver)
            trigger(receiver, key, { newValue: value, oldValue}) // 设值时自动通知更新
            return result
        }
    }
    return new Proxy(target, handler)
}
let activeEffect = {fn: null, key: null}

function effect(fn) {
    activeEffect.fn = fn
    activeEffect.fn()
    activeEffect = {fn: null, key: null}
}
function watch(source, fn) {
    let oldValues = new Map(), newValues = new Map(), isArray = false; 
    function handleEffects(rValue){
        console.log('start values: ', newValues.values(), oldValues.values());
        effect((oldValue, upKey = null) => {
            console.log('~~~~', rValue);
            // 这里去触发get 搜集依赖
            const newValue = typeof rValue === 'function' ? rValue().value : rValue.value;
            console.log('values: ', newValue, oldValue, newValues.values(), oldValues.values(), activeEffect.key);
            if(activeEffect.fn) {
                // 遍历原始数据时,新值旧值都一样, 添加依赖的时候 增加 activeEffect.key
                oldValues.set(activeEffect.key, newValue)
                newValues.set(activeEffect.key, newValue)
            }else {// 同时更新旧值与新值
                console.log(newValue, oldValue);
                oldValues.set(upKey, oldValue)
                newValues.set(upKey, newValue)
                console.log('last values: ', newValues.values(), oldValues.values());
                // 同源和多源做不同处理
                isArray 
                    ? fn([[...newValues.values()], [...oldValues.values()]]) 
                    : fn([...newValues.values()][0], [...oldValues.values()][0]);
            }
        })
    }
    // 监听多源
    if(Array.isArray(source)) {
        isArray = true;
        source.forEach(rValue => {
            handleEffects(rValue)
        })
    }
    // 监听单一源
    else 
        handleEffects(source)
}

测试一下

const wRef = ref(5);
watch(wRef, (value, preValue) => {
    console.log('watch监听单源测试:', value, preValue)
})
wRef.value = 66
// watch监听单源测试: 666 5
const wRef = ref(1);
const wRef1 = ref(2);
const wRef2 = ref(3);

watch([wRef, () => wRef1, wRef2], (values, preValues) => { 
    console.log('watch监听多源测试:', values, preValues)
})
wRef.value = 11;
// watch监听多源测试:[11, 2, 3] [1, 2, 3]
wRef1.value = 22;
// watch监听多源测试:[11, 22, 3] [1, 2, 3]
wRef2.value = 33
// watch监听多源测试:[111, 22, 33] [11, 2, 3]
wRef.value = 111
// watch监听多源测试:[11, 22, 33] [1, 2, 3]
wRef2.value = 333
// watch监听多源测试:[111, 22, 33] [11, 2, 33]

简版 watch 基本就实现了,仅支持 ref, reactive 的数据会有问题,各位可以想想哪里有问题。

总结

vue2 和vue3 响应基本思路都差不多。收集依赖数据源改变时更新依赖

写完感觉自己越来越无知,吾生也有涯,而知也无涯

另外看起来可能好理解,建议还是动动手吧。

纸上得来终觉浅,绝知此事要躬行!

限于技术和写文章水平,行文有误欢迎👐大家指出

参考

站在别人肩膀上才能看的更远,感谢 💖💖💖 文章中所有参考链接文章作者的分享。

🔗 | 掘金小册 - 剖析 Vue.js 内部运行机制
🔗 | 手写一个简易vue响应式带你了解响应式原理
🔗 | VUE源码相关面试题汇总
🔗 | Vue3为什么选择Proxy做双向绑定?
🔗 | Vue 3 响应式原理及实现
🔗 | 浅析Vue3中的响应式原理

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-04 17:24:58  更:2021-09-04 17:25:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:49:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码