详解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 相关文章推荐
让人印象深刻的10个jQuery手风琴效果应用
May 08 Javascript
JS中判断null、undefined与NaN的方法
Mar 26 Javascript
网页从弹窗页面单选框传值至父页面代码分享
Sep 29 Javascript
javascript实现2016新年版日历
Jan 25 Javascript
JavaScript中ES6字符串扩展方法
Aug 26 Javascript
简单分析javascript中的函数
Sep 10 Javascript
浅析location.href跨窗口调用函数
Nov 22 Javascript
AngularJS的ng-repeat指令与scope继承关系实例详解
Jan 21 Javascript
Angular2学习教程之ng中变更检测问题详解
May 28 Javascript
jQuery实现常见的隐藏与展示列表效果示例
Jun 04 jQuery
JS实现数组去重,显示重复元素及个数的方法示例
Jan 21 Javascript
vue2.0 获取从http接口中获取数据,组件开发,路由配置方式
Nov 04 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
非常不错的MySQL优化的8条经验
2008/03/24 PHP
php安全之直接用$获取值而不$_GET 字符转义
2012/06/03 PHP
thinkphp的c方法使用示例
2014/02/24 PHP
C#静态方法与非静态方法实例分析
2014/09/22 PHP
php和editplus正则表达式去除空白行
2015/04/17 PHP
thinkphp5 加载静态资源路径与常量的方法
2017/12/24 PHP
PHP whois查询类定义与用法示例
2019/04/03 PHP
php操作redis命令及代码实例大全
2020/11/19 PHP
修改jquery里的dialog对话框插件为框架页(iframe) 的方法
2010/09/14 Javascript
javascript学习笔记(二) js一些基本概念
2012/06/18 Javascript
Javascript浅谈之引用类型
2013/12/18 Javascript
javascript弹出窗口实现代码
2015/11/12 Javascript
Angular ng-class详解及实例代码
2016/09/19 Javascript
详解vue.js移动端导航navigationbar的封装
2017/07/05 Javascript
浅谈es6语法 (Proxy和Reflect的对比)
2017/10/24 Javascript
详解vue+css3做交互特效的方法
2017/11/20 Javascript
VueJs监听window.resize方法示例
2018/01/17 Javascript
微信小程序中使用ECharts 异步加载数据的方法
2018/06/27 Javascript
VUE项目axios请求头更改Content-Type操作
2020/07/24 Javascript
[50:17]Newbee vs Serenity 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
Python迭代用法实例教程
2014/09/08 Python
Python模糊查询本地文件夹去除文件后缀的实例(7行代码)
2017/11/09 Python
Python机器学习之决策树算法实例详解
2017/12/06 Python
python psutil库安装教程
2018/03/19 Python
python os用法总结
2018/06/08 Python
在python中创建指定大小的多维数组方式
2019/11/28 Python
Django中文件上传和文件访问微项目的方法
2020/04/27 Python
Python爬虫scrapy框架Cookie池(微博Cookie池)的使用
2021/01/13 Python
荷兰皇家航空公司官方网站:KLM Royal Dutch Airlines
2017/12/07 全球购物
日本必酷网络直营店:Biccamera
2019/03/23 全球购物
写一个函数,求一个字符串的长度。在main函数中输入字符串,并输出其长度
2015/11/18 面试题
教师个人剖析材料
2014/02/05 职场文书
客户答谢会活动方案
2014/08/31 职场文书
最新最全的手机号验证正则表达式
2022/02/24 Javascript
Redis监控工具RedisInsight安装与使用
2022/03/21 Redis
Java设计模式之代理模式
2022/04/22 Java/Android