详解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 call apply使用 JavaScript对象的方法绑定到DOM事件后this指向问题
Sep 28 Javascript
node.js中的buffer.slice方法使用说明
Dec 10 Javascript
js实现下拉框选择要显示图片的方法
Feb 16 Javascript
理解javascript中的原型和原型链
Jul 30 Javascript
jQuery EasyUI Dialog拖不下来如何解决
Sep 28 Javascript
self.attachevent is not a function的解决方法
Apr 04 Javascript
Node.js 实现简单的接口服务器的实例代码
May 23 Javascript
jQuery条件分页 代替离线查询(附代码)
Aug 17 jQuery
js精确的加减乘除实例
Nov 14 Javascript
Node.js中sequelize时区的配置方法
Dec 10 Javascript
Js经典案例的实例代码
May 10 Javascript
vue实现的多页面项目如何优化打包的步骤详解
Jul 19 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正则校验用户名介绍
2008/07/19 PHP
解析PayPal支付接口的PHP开发方式
2010/11/28 PHP
PHP源码之explode使用说明
2011/08/05 PHP
php代码审计比较有意思的例子
2014/05/07 PHP
PHP批量检测并去除文件BOM头代码实例
2014/05/08 PHP
采用thinkphp自带方法生成静态html文件详解
2014/06/13 PHP
PHP获取短链接跳转后的真实地址和响应头信息的方法
2014/07/25 PHP
浅析PHP程序设计中的MVC编程思想
2014/07/28 PHP
ThinkPHP内置jsonRPC的缺陷分析
2014/12/18 PHP
使用composer命令加载vendor中的第三方类库 的方法
2019/07/09 PHP
jQuery中add实现同时选择两个id对象
2010/10/22 Javascript
Google 静态地图API实现代码
2010/11/19 Javascript
JavaScript控制Session操作方法
2013/01/17 Javascript
5个书写JavaScript代码的坏习惯,看看你中枪了没?
2014/11/06 Javascript
jquery实现标题字体变换的滑动门菜单效果
2015/09/07 Javascript
jquery实现全选、不选、反选的两种方法
2016/09/06 Javascript
js实现背景图自适应窗口大小
2017/01/10 Javascript
详解使用Next.js构建服务端渲染应用
2018/07/10 Javascript
使用vue-router在Vue页面之间传递数据的方法
2019/07/15 Javascript
Vue  webpack 项目自动打包压缩成zip文件的方法
2019/07/24 Javascript
JavaScript中判断为整数的多种方式及保留两位小数的方法
2019/09/09 Javascript
微信小程序实现蓝牙打印
2019/09/23 Javascript
python Django连接MySQL数据库做增删改查
2013/11/07 Python
Python 运行 shell 获取输出结果的实例
2019/01/07 Python
Python使用os.listdir()和os.walk()获取文件路径与文件下所有目录的方法
2019/04/01 Python
简单了解python高阶函数map/reduce
2019/06/28 Python
Django 全局的static和templates的使用详解
2019/07/19 Python
python 导入数据及作图的实现
2019/12/03 Python
Python3加密解密库Crypto的RSA加解密和签名/验签实现方法实例
2020/02/11 Python
业务总经理岗位职责
2014/02/03 职场文书
教学个人的自我评价分享
2014/02/16 职场文书
公司任命书范本
2014/06/04 职场文书
检讨书怎么写
2015/01/23 职场文书
2016年优秀教师先进事迹材料
2016/02/26 职场文书
MySQL性能压力基准测试工具sysbench的使用简介
2021/04/21 MySQL
自己搭建resnet18网络并加载torchvision自带权重的操作
2021/05/13 Python