Vue如何实现响应式系统


Posted in Javascript onJuly 11, 2018

前言

最近深入学习了Vue实现响应式的部分源码,将我的些许收获和思考记录下来,希望能对看到这篇文章的人有所帮助。有什么问题欢迎指出,大家共同进步。

什么是响应式系统

一句话概括:数据变更驱动视图更新。这样我们就可以以“数据驱动”的思维来编写我们的代码,更多的关注业务,而不是dom操作。其实Vue响应式的实现是一个变化追踪和变化应用的过程。

vue响应式原理

以数据劫持方式,拦截数据变化;以依赖收集方式,触发视图更新。利用es5 Object.defineProperty拦截数据的setter、getter;getter收集依赖,setter触发依赖更新,而组件render也会变为一个watcher callback被加入相应数据的依赖中。

发布订阅

利用发布订阅设计模式实现,Observer作为发布者,Watcher作为订阅者,两者无直接交互,通过Dep进行统一调度。
Observer负责拦截get, set;get时触发dep添加依赖,set时调度dep发布;添加Watcher时会触发订阅数据的get,并加入到dep调度中心的订阅者队列中。

以下的UML类图是Vue实现响应式功能的类,以及他们之间的引用关系。

只包含部分属性方法

Vue如何实现响应式系统

上图中的类已经标识的蛮清楚了,但是还是需要一个调用关系图,让调用过程更加清晰,如下图所示。

响应式data对象中,每一项key的劫持get/set函数都闭包了Dep调度实例,这张图显示了一个key更改过程中的数据流转。

Vue如何实现响应式系统

部分源码

数据变更过程中的订阅/发布模型上图已经清晰的展示了,从图中我们已经知道了可以通过增加watcher来订阅某一项数据的变更。那么,我们只需要把组件render作为一个watcher订阅的话,数据驱动视图的渲染岂不是水到渠成了。Vue正是这么做的!
以下代码片段来自Vue.prototype._mount函数

callHook(vm, 'beforeMount')
vm._watcher = new Watcher(vm, () => {
 vm._update(vm._render(), hydrating)
}, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
 vm._isMounted = true
 callHook(vm, 'mounted')
}

一些问题思考

#person赋值新的对象,新对象里的属性是否也是响应式的呢?

var vm = new Vue({
 el: '#app',
 data: () => ({
  person: null
 })
})
vm.person = {name: 'zs'}
setTimeout(() => {
 // 更改name
 vm.person.name = 'finally zs'
}, 3000)

答案:是响应式的。

原因:因为Vue劫持set时,会对value再次做observe,源码如下。

function reactiveSetter (newVal) {
 /* ...省略部分代码 */
 // 这里会再次对新的value做拦截
 childOb = observe(newVal)
 dep.notify()
}

#当我们监听多层属性时,上层引用变更,是否会触发回调?

var vm = new Vue({
 data: () => ({
  person: {name: '令狐洋葱'}
 }),
 watch: {
  'person.name'(val) {
   console.log('name updated', val)
  }
 }
})
vm.person = {}

答案:会。

原因:person.name作为一个表达式传入Watcher时,会被解析成类似这样的函数

() => {this.vm.person.name}

这样就会先触发person get, 然后触发name get;所以我们配置的回调函数,不仅仅加入到了name依赖中,person也有。

#接着上个问题,person如果被赋值了新的对象,老对象和老对象上的依赖如何垃圾回收的?

  • 老对象的回收:由于老对象的直接引用只有vue实例上的person,person切换到了新的引用,所以老对象没有引用了,就会被回收掉。
  • 老对象上的依赖dep,watcher的依赖里还存在;但是在run执行时,会调用watcher的get() 获取当前值;get中会执行新的依赖收集,并在收集完毕后,清空老的依赖。

具体源码如下:

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
  pushTarget(this)
  const value = this.getter.call(this.vm, this.vm)
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) {
    traverse(value)
  }
  popTarget()
  this.cleanupDeps()
  return value
}

#当我们多次同步修改name时,回调函数是否会触发多次?

var vm = new Vue({
  data: () => ({
    person: {name: '令狐洋葱'}
  }),
  watch: {
    'person.name': (val) {
      console.log('name updated: ' + val)
    }
  }
})
vm.person = {name: 'zs'}
vm.person.name = '无敌'

答案: 不会,因为watch回调函数执行是异步的,且会去重。可以通过sync强制配置成同步run,就会执行2次了。

自己实现一个响应式系统

只包含核心功能,具体源码可以看这里https://github.com/Zenser/z-vue,欢迎来star。

实现功能非常基础啦,重在理解,功能不全的。

Observer

class Observe {
  constructor(obj) {
    Object.keys(obj).forEach(prop => {
      reactive(obj, prop, obj[prop])
    })
  }
}
function reactive(obj, prop) {
  let value = obj[prop]
  // 闭包绑定依赖
  let dep = new Dep()
  Object.defineProperty(obj, prop, {
    configurable: true,
    enumerable: true,
    get() {
      //利用js单线程,在get时绑定订阅者
      if (Dep.target) {
        // 绑定订阅者
        dep.addSub(Dep.target)
      }
      return value
    },
    set(newVal) {
      value = newVal
      // 更新时,触发订阅者更新
      dep.notify()
    }
  })
  // 对象监听
  if (typeof value === 'object' && value !== null) {
    Object.keys(value).forEach(valueProp => {
      reactive(value, valueProp)
    })
  } 
}

Dep

class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    if (this.subs.indexOf(sub) === -1) {
      this.subs.push(sub)
    }
  }
  notify() {
    this.subs.forEach(sub => {
      const oldVal = sub.value
      sub.cb && sub.cb(sub.get(), oldVal)
    })
  }
}

Watcher

class Watcher {
  constructor(data, exp, cb) {
    this.data = data
    this.exp = exp
    this.cb = cb
    this.get()
  }
  get() {
    Dep.target = this
    this.value = (function calcValue(data, prop) {
      for (let i = 0, len = prop.length; i < len; i++ ) {
        data = data[prop[i]]
      }
      return data
    })(this.data, this.exp.split('.'))
    Dep.target = null
    return this.value
  }
}

参考文档:https://cn.vuejs.org/v2/guide/reactivity.html

Javascript 相关文章推荐
jquery maxlength使用说明
Sep 09 Javascript
jquery实现tr元素的上下移动示例代码
Dec 20 Javascript
js创建一个input数组并绑定click事件的方法
Jun 12 Javascript
JavaScript实现N皇后问题算法谜题解答
Dec 29 Javascript
jQuery实现获取隐藏div高度的方法示例
Feb 09 Javascript
详解如何使用Vue2做服务端渲染
Mar 29 Javascript
vue router仿天猫底部导航栏功能
Oct 18 Javascript
从零开始搭建webpack+react开发环境的详细步骤
May 18 Javascript
JS加密插件CryptoJS实现AES加密操作示例
Aug 16 Javascript
初探Vue3.0 中的一大亮点Proxy的使用
Dec 06 Javascript
ES6中Set和Map数据结构,Map与其它数据结构互相转换操作实例详解
Feb 28 Javascript
nest.js 使用express需要提供多个静态目录的操作方法
Oct 24 Javascript
vue.js内置组件之keep-alive组件使用
Jul 10 #Javascript
小程序图片剪裁加旋转的示例代码
Jul 10 #Javascript
vue使用中的内存泄漏【推荐】
Jul 10 #Javascript
Vue脚手架的简单使用实例
Jul 10 #Javascript
vue自定义移动端touch事件之点击、滑动、长按事件
Jul 10 #Javascript
微信小程序中换行空格(多个空格)写法详解
Jul 10 #Javascript
在小程序中使用腾讯视频插件播放教程视频的方法
Jul 10 #Javascript
You might like
德生S2000收音机更换“钕铁硼”全频扬声器
2021/03/02 无线电
Windows下的PHP5.0安装配制详解
2006/09/05 PHP
使用PHP Socket写的POP3类
2013/10/30 PHP
PHP Yaf框架的简单安装使用教程(推荐)
2016/06/08 PHP
ECMAScript 5中的属性描述符详解
2015/03/02 Javascript
JavaScript字符串常用类使用方法汇总
2015/04/14 Javascript
基于jQuery实现仿百度首页选项卡切换效果
2016/05/29 Javascript
JS仿淘宝搜索框用户输入事件的实现
2017/06/19 Javascript
Vue-cli配置打包文件本地使用的教程图解
2018/08/02 Javascript
webuploader分片上传的实现代码(前后端分离)
2018/09/10 Javascript
用jQuery将JavaScript对象转换为querystring查询字符串的方法
2018/11/12 jQuery
一步步教你利用Docker设置Node.js
2018/11/20 Javascript
JavaScript算法学习之冒泡排序和选择排序
2019/11/02 Javascript
Vue+Openlayers自定义轨迹动画
2020/09/24 Javascript
VUE 项目在IE11白屏报错 SCRIPT1002: 语法错误的解决
2020/09/27 Javascript
Python实现抓取百度搜索结果页的网站标题信息
2015/01/22 Python
python学习教程之Numpy和Pandas的使用
2017/09/11 Python
浅谈python for循环的巧妙运用(迭代、列表生成式)
2017/09/26 Python
python做量化投资系列之比特币初始配置
2018/01/23 Python
Flask实现跨域请求的处理方法
2018/09/27 Python
Python3 文章标题关键字提取的例子
2019/08/26 Python
Python分割训练集和测试集的方法示例
2019/09/19 Python
python 计算方位角实例(根据两点的坐标计算)
2020/01/17 Python
Python列表倒序输出及其效率详解
2020/03/04 Python
html5是什么_动力节点Java学院整理
2017/07/07 HTML / CSS
受希腊女神灵感的晚礼服、鸡尾酒礼服和婚纱:THEIA
2018/04/15 全球购物
俄罗斯有趣和原创礼物网上商店:MagicMag
2019/08/01 全球购物
金蝶的一道SQL笔试题
2012/12/18 面试题
幼儿园教师请假制度
2014/01/16 职场文书
校园绿化美化方案
2014/06/08 职场文书
电教室标语
2014/06/20 职场文书
党的群众路线对照检查材料
2014/09/22 职场文书
监护人证明
2015/06/19 职场文书
重阳节活动主持词
2015/07/04 职场文书
导游词之杭州岳王庙
2019/11/13 职场文书
Nginx中使用Lua脚本与图片的缩略图处理的实现
2022/03/18 Servers