详解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 相关文章推荐
用JavaScript 处理 URL 的两个函数代码
Aug 13 Javascript
前端开发部分总结[兼容性、DOM操作、跨域等](持续更新)
Mar 04 Javascript
Jquery index()方法 获取相应元素索引值
Oct 12 Javascript
JavaScript获取图片真实大小代码实例
Sep 24 Javascript
有关JavaScript中call()和apply() 的一些理解
May 20 Javascript
jQuery+CSS3文字跑马灯特效的简单实现
Jun 25 Javascript
轻松掌握JavaScript单例模式
Aug 25 Javascript
使用JQuery中的trim()方法去掉前后空格
Sep 16 Javascript
小程序点赞收藏功能的实现代码示例
Sep 07 Javascript
React通过redux-persist持久化数据存储的方法示例
Feb 14 Javascript
ES6 Proxy实现Vue的变化检测问题
Jun 11 Javascript
Vue基本指令实例图文讲解
Feb 25 Vue.js
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
深入for,while,foreach遍历时间比较的详解
2013/06/08 PHP
php+mysql删除指定编号员工信息的方法
2015/01/14 PHP
LAMP环境使用Composer安装Laravel的方法
2017/03/25 PHP
PHP使用ActiveMQ实现消息队列的方法详解
2019/05/31 PHP
fckeditor 获取文本框值的实现代码
2009/02/09 Javascript
加载远程图片时,经常因为缓存而得不到更新的解决方法(分享)
2013/06/26 Javascript
js 时间格式与时间戳的相互转换示例代码
2013/12/25 Javascript
js获取光标位置和设置文本框光标位置示例代码
2014/01/09 Javascript
原生javascript实现无间缝滚动示例
2014/01/28 Javascript
javascript实现当前页导航激活的方法
2015/02/27 Javascript
javascript实现的淘宝旅行通用日历组件用法实例
2015/08/03 Javascript
jquery ajax分页插件的简单实现
2016/01/27 Javascript
Jquery通过ajax请求NodeJS返回json数据实例
2016/11/08 NodeJs
jQuery文字轮播特效
2017/02/12 Javascript
微信小程序实现瀑布流布局与无限加载的方法详解
2017/05/12 Javascript
Bootstrap fileinput文件上传组件使用详解
2017/06/06 Javascript
Node.js 回调函数实例详解
2017/07/06 Javascript
微信小程序文章详情页跳转案例详解
2019/07/09 Javascript
[18:32]DOTA2 HEROS教学视频教你分分钟做大人-谜团
2014/06/12 DOTA
[03:09]DOTA2亚洲邀请赛 LGD战队出场宣传片
2015/02/07 DOTA
[06:45]DOTA2-DPC中国联赛 正赛 Magma vs LBZS 选手采访
2021/03/11 DOTA
python中使用百度音乐搜索的api下载指定歌曲的lrc歌词
2014/07/18 Python
Python的类实例属性访问规则探讨
2015/01/30 Python
在Python中编写数据库模块的教程
2015/04/29 Python
wxPython的安装图文教程(Windows)
2017/12/28 Python
python中的随机函数小结
2018/01/27 Python
Python实现检测文件MD5值的方法示例
2018/04/11 Python
详解TensorFlow查看ckpt中变量的几种方法
2018/06/19 Python
css3+jq创作含苞待放的荷花
2014/02/20 HTML / CSS
微信小程序canvas实现水平、垂直居中效果
2020/02/05 HTML / CSS
印度首选时尚目的地:Reliance Trends
2018/01/17 全球购物
自荐信要包含哪些内容
2013/11/06 职场文书
党员群众路线对照检查材料思想汇报
2014/09/17 职场文书
天坛导游词
2015/02/02 职场文书
新学期小学班主任工作计划
2019/06/21 职场文书
mysql如何配置白名单访问
2021/06/30 MySQL