详解Vue源码学习之双向绑定


Posted in Javascript onApril 10, 2019

原理

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。

上面那段话是Vue官方文档中截取的,可以看到是使用Object.defineProperty实现对数据改变的监听。Vue主要使用了观察者模式来实现数据与视图的双向绑定。

function initData(vm) { //将data上数据复制到_data并遍历所有属性添加代理
 vm._data = vm.$options.data;
 const keys = Object.keys(vm._data); 
 let i = keys.length;
 while(i--) { 
  const key = keys[i];
  proxy(vm, `_data`, key);
 }
 observe(data, true /* asRootData */) //对data进行监听
}

在第一篇数据初始化中,执行new Vue()操作后会执行initData()去初始化用户传入的data,最后一步操作就是为data添加响应式。

实现

在Vue内部存在三个对象:Observer、Dep、Watcher,这也是实现响应式的核心。

Observer

Observer对象将data中所有的属性转为getter/setter形式,以下是简化版代码,详细代码请看这里。

export function observe (value) {
 //递归子属性时的判断
 if (!isObject(value) || value instanceof VNode) {
  return
 }
 ...
 ob = new Observer(value)
}
export class Observer {
 constructor (value) {
  ... //此处省略对数组的处理
  this.walk(value)
 }

 walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
   defineReactive(obj, keys[i]) //为每个属性创建setter/getter
  }
 }
 ...
}

//设置set/get
export function defineReactive (
 obj: Object,
 key: string,
 val: any
) {
 //利用闭包存储每个属性关联的watcher队列,当setter触发时依然能访问到
 const dep = new Dep()
 ...
 //如果属性为对象也创建相应observer
 let childOb = observe(val)
 Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   if (Dep.target) {
    dep.depend() //将当前dep传到对应watcher中再执行watcher.addDep将watcher添加到当前dep.subs中
    if (childOb) { //如果属性是对象则继续收集依赖
     childOb.dep.depend()
     ...
    }
   }
   return value
  },
  set: function reactiveSetter (newVal) {
   ...
   childOb = observe(newVal) //如果设置的新值是对象,则为其创建observe
   dep.notify() //通知队列中的watcher进行更新
  }
 })
}

创建Observer对象时,为data的每个属性都执行了一遍defineReactive方法,如果当前属性为对象,则通过递归进行深度遍历。该方法中创建了一个Dep实例,每一个属性都有一个与之对应的dep,存储所有的依赖。然后为属性设置setter/getter,在getter时收集依赖,setter时派发更新。这里收集依赖不直接使用addSub是为了能让Watcher创建时自动将自己添加到dep.subs中,这样只有当数据被访问时才会进行依赖收集,可以避免一些不必要的依赖收集。

Dep

Dep就是一个发布者,负责收集依赖,当数据更新是去通知订阅者(watcher)。源码地址

export default class Dep {
 static target: ?Watcher; //指向当前watcher
 constructor () {
  this.subs = []
 }
 //添加watcher
 addSub (sub: Watcher) {
  this.subs.push(sub)
 }
 //移除watcher
 removeSub (sub: Watcher) {
  remove(this.subs, sub)
 }
 //通过watcher将自身添加到dep中
 depend () {
  if (Dep.target) {
   Dep.target.addDep(this)
  }
 }
 //派发更新信息
 notify () {
  ...
  for (let i = 0, l = subs.length; i < l; i++) {
   subs[i].update()
  }
 }
}

Watcher

源码地址

//解析表达式(a.b),返回一个函数
export function parsePath (path: string): any {
 if (bailRE.test(path)) {
  return
 }
 const segments = path.split('.')
 return function (obj) {
  for (let i = 0; i < segments.length; i++) {
   if (!obj) return
   obj = obj[segments[i]]  //遍历得到表达式所代表的属性
  }
  return obj
 }
}
export default class Watcher {
 constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
 ) {
  this.vm = vm
  if (isRenderWatcher) {
   vm._watcher = this
  } 
  //对创建的watcher进行收集,destroy时对这些watcher进行销毁
  vm._watchers.push(this)
  // options
  if (options) {
   ...
   this.before = options.before
  }
  ...
  //上一轮收集的依赖集合Dep以及对应的id
  this.deps = []
  this.depIds = new Set()
  //新收集的依赖集合Dep以及对应的id
  this.newDeps = []
  this.newDepIds = new Set()
  this.expression = process.env.NODE_ENV !== 'production'
   ? expOrFn.toString()
   : ''
  // parse expression for getter
  if (typeof expOrFn === 'function') {
   this.getter = expOrFn
  } else {
   this.getter = parsePath(expOrFn)
   ...
  }
  ...
  this.value = this.get()
 }

 /** * Evaluate the getter, and re-collect dependencies. */
 get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
   value = this.getter.call(vm, vm)
  } catch (e) {
   if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
   } else {
    throw e
   }
  } finally {
   // "touch" every property so they are all tracked as
   // dependencies for deep watching
   if (this.deep) {
    traverse(value)
   }
   popTarget()
   this.cleanupDeps() //清空上一轮的依赖
  }
  return value
 }

 /** * Add a dependency to this directive. */
 addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) { //同一个数据只收集一次
   this.newDepIds.add(id)
   this.newDeps.push(dep)
   if (!this.depIds.has(id)) {
    dep.addSub(this)
   }
  }
 }

 //每轮收集结束后去除掉上轮收集中不需要跟踪的依赖
 cleanupDeps () {
  let i = this.deps.length
  while (i--) {
   const dep = this.deps[i]
   if (!this.newDepIds.has(dep.id)) {
    dep.removeSub(this)
   }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
 },
 update () {
  ...
  //经过一些优化处理后,最终执行this.get
  this.get();
 }
 // ...
}

依赖收集的触发是在执行render之前,会创建一个渲染Watcher:

updateComponent = () => {
 vm._update(vm._render(), hydrating) //执行render生成VNode并更新dom
}
new Watcher(vm, updateComponent, noop, {
 before () {
  if (vm._isMounted) {
   callHook(vm, 'beforeUpdate')
  }
 }
}, true /* isRenderWatcher */)

在渲染Watcher创建时会将Dep.target指向自身并触发updateComponent也就是执行_render生成VNode并执行_update将VNode渲染成真实DOM,在render过程中会对模板进行编译,此时就会对data进行访问从而触发getter,由于此时Dep.target已经指向了渲染Watcher,接着渲染Watcher会执行自身的addDep,做一些去重判断然后执行dep.addSub(this)将自身push到属性对应的dep.subs中,同一个属性只会被添加一次,表示数据在当前Watcher中被引用。

当_render结束后,会执行popTarget(),将当前Dep.target回退到上一轮的指,最终又回到了null,也就是所有收集已完毕。之后执行cleanupDeps()将上一轮不需要的依赖清除。当数据变化是,触发setter,执行对应Watcher的update属性,去执行get方法又重新将Dep.target指向当前执行的Watcher触发该Watcher的更新。

这里可以看到有deps,newDeps两个依赖表,也就是上一轮的依赖和最新的依赖,这两个依赖表主要是用来做依赖清除的。但在addDep中可以看到if (!this.newDepIds.has(id))已经对收集的依赖进行了唯一性判断,不收集重复的数据依赖。为何又要在cleanupDeps中再作一次判断呢?

while (i--) {
   const dep = this.deps[i]
   if (!this.newDepIds.has(dep.id)) {
    dep.removeSub(this)
   }
  }
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0

在cleanupDeps中主要清除上一轮中的依赖在新一轮中没有重新收集的,也就是数据刷新后某些数据不再被渲染出来了,例如:

<body>
 <div id="app">
  <div v-if='flag'> </div>   
  <div v-else> </div> 
  <button @click="msg1 += '1'">change</button>   
  <button @click="flag = !flag">toggle</button>  
 </div> 
  <script type="text/javascript">
  var vm = new Vue({
   el: '#app',
   data: {
    flag: true,
    msg1: 'msg1',
    msg2: 'msg2'
   }
  })
  </script> 
</body>

每次点击change,msg1都会拼接一个1,此时就会触发重新渲染。当我们点击toggle时,由于flag改变,msg1不再被渲染,但当我们点击change时,msg1发生了变化,但却没有触发重新渲染,这就是cleanupDeps起的作用。如果去除掉cleanupDeps这个步骤,只是能防止添加相同的依赖,但是数据每次更新都会触发重新渲染,又去重新收集依赖。这个例子中,toggle后,重新收集的依赖中并没有msg1,因为它不需要被显示,但是由于设置了setter,此时去改变msg1依然会触发setter,如果没有执行cleanupDeps,那么msg1的依赖依然存在依赖表里,又会去触发重新渲染,这是不合理的,所以需要每次依赖收集完毕后清除掉一些不需要的依赖。

总结

依赖收集其实就是收集每个数据被哪些Watcher(渲染Watcher、computedWatcher等)所引用,当这些数据更新时,就去通知依赖它的Watcher去更新。

以上所述是小编给大家介绍的Vue源码学习之双向绑定详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
JS+CSS制作DIV层可(最小化/拖拽/排序)功能实现代码
Feb 25 Javascript
jquery $.each()使用探讨
Sep 23 Javascript
jQuery制作简单柱状图实例
Jan 28 Javascript
js实现的牛顿摆效果
Mar 31 Javascript
JavaScript获取页面中表单(form)数量的方法
Apr 03 Javascript
js时钟翻牌效果实现代码分享
Jul 31 Javascript
JS控制静态页面之间传递参数获取参数并应用的简单实例
Aug 10 Javascript
基于Bootstrap分页的实例讲解(必看篇)
Jul 04 Javascript
基于Vue实现可以拖拽的树形表格实例详解
Oct 18 Javascript
Vue项目安装插件并保存
Jan 28 Javascript
微信小程序如何实现在线客服功能
Oct 16 Javascript
在Vue 中获取下拉框的文本及选项值操作
Aug 13 Javascript
node.js 基于cheerio的爬虫工具的实现(需要登录权限的爬虫工具)
Apr 10 #Javascript
详解关于html,css,js三者的加载顺序问题
Apr 10 #Javascript
angular2 NgModel模块的具体使用方法
Apr 10 #Javascript
bootstrap tooltips在 angularJS中的使用方法
Apr 10 #Javascript
javascript判断一个变量是数组还是对象
Apr 10 #Javascript
Angular CLI 使用教程指南参考小结
Apr 10 #Javascript
基于vue开发微信小程序mpvue-docs跳转页面功能
Apr 10 #Javascript
You might like
PHP引用(&amp;)各种使用方法实例详解
2014/03/20 PHP
Kindeditor编辑器添加图片上传水印功能(php代码)
2017/08/03 PHP
Laravel 5使用Laravel Excel实现Excel/CSV文件导入导出的功能详解
2017/10/11 PHP
jquery隐藏标签和显示标签的实例
2013/11/11 Javascript
Jquery获得控件值的三种方法总结
2014/02/13 Javascript
更快的异步执行(setTimeout多浏览器)
2014/08/12 Javascript
Javascript中设置默认参数值示例
2014/09/11 Javascript
jQuery中:selected选择器用法实例
2015/01/04 Javascript
jQuery简单实现彩色云标签效果示例
2016/08/01 Javascript
详解Vue学习笔记入门篇之组件的内容分发(slot)
2017/07/17 Javascript
Vue实现点击时间获取时间段查询功能
2020/08/21 Javascript
浅谈在react中如何实现扫码枪输入
2018/07/04 Javascript
jQuery实现炫丽的3d旋转星空效果
2018/07/04 jQuery
vue实现简单的MVVM框架
2018/08/05 Javascript
微信小程序实现简易table表格
2020/06/19 Javascript
微信小程序实现日期格式化和倒计时
2020/11/01 Javascript
Node.JS枚举统计当前文件夹和子目录下所有代码文件行数
2019/08/23 Javascript
最基础的Python的socket编程入门教程
2015/04/23 Python
Python实现简单登录验证
2016/04/13 Python
TensorFlow深度学习之卷积神经网络CNN
2018/03/09 Python
一个可以套路别人的python小程序实例代码
2019/04/09 Python
pytorch+lstm实现的pos示例
2020/01/14 Python
python入门之基础语法学习笔记
2020/02/08 Python
IDLE下Python文件编辑和运行操作
2020/04/25 Python
Html5画布_动力节点Java学院整理
2017/07/13 HTML / CSS
澳大利亚百货公司:David Jones
2018/02/08 全球购物
GIVENCHY纪梵希官方旗舰店:高定彩妆与贵族护肤品
2018/04/16 全球购物
自主招生自荐信格式
2013/12/03 职场文书
运动会演讲稿
2014/05/07 职场文书
公司担保书格式范文
2014/05/12 职场文书
检讨书1000字
2014/10/11 职场文书
2015年中个人总结范文
2015/03/10 职场文书
2015年前台个人工作总结
2015/04/03 职场文书
高效课堂教学反思
2016/02/24 职场文书
让人感觉高大上的讲话稿怎么写?
2019/07/08 职场文书
Spring Cloud Netflix 套件中的负载均衡组件 Ribbon
2022/04/13 Java/Android