vue数据绑定源码
思路分析
数据的双向绑定,就是数据变化了自动更新视图,视图变化了自动更新数据,实际上视图变化更新数据只要通过事件监听就可以实现了,并不是数据双向绑定的关键点。关键还是数据变化了驱动视图自动更新。
所有接下来,我们详细了解下数据如何驱动视图更新的。
数据驱动视图更新的重点就是,如何知道数据更新了,或者说数据更新了要如何主动的告诉我们。可能大家都听过,vue的数据双向绑定原理是Object.defineProperty( )对属性设置一个set/get,是这样的没错,其实get/set只是可以做到对数据的读取进行劫持,就可以让我们知道数据更新了。但是你详细的了解整个过程吗?
介绍数据驱动更新之前,先介绍下面4个类和方法,然后从数据的入口initState开始按顺序介绍,以下类和方法是如何协作,达到数据驱动更新的。
defineReactive
这个方法,用处可就大了。
我们看到他是给对象的键值添加get/set
方法,也就是对属性的取值和赋值都加了拦截,同时用闭包给每个属性都保存了一个Dep
对象。
当读取该值的时候,就把当前这个watcher
(Dep.target
)添加进他的dep里的观察者列表,这个watcher
也会把这个dep
添加进他的依赖列表。
当给设置值的时候,就让这个闭包保存的dep
去通知他的观察者列表的每一个watcher
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
if (!getter && arguments.length === 2) {
val = obj[key]
}
const setter = property && property.set
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
Observer
什么是可观察者对象呢?
简单来说:就是数据变更时可以通知所有观察他的观察者。
1、取值的时候,能把要取值的watcher(观察者对象)加入它的dep(依赖,也可叫观察者管理器)管理的subs列表里(即观察者列表);
2、设置值的时候,有了变化,所有依赖于它的对象(即它的dep里收集到的观察者watcher)都得到通知。
这个类功能就是把数据转化成可观察对象。针对Object类型就调用defineReactive方法循环把每一个键值都转化。针对Array,首先是对Array经过特殊处理,使它可以监控到数组发生了变化,然后对数组的每一项递归调用Observer进行转化。
对于Array是如何处理的呢?这个放在下面单独说。
export class Observer {
/**
*如果是对象就循环把对象的每一个键值都转化成可观察者对象
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* 如果是数组就对数组的每一项做转化
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Dep
这个类功能简单来说就是管理数据的观察者的。当有观察者读取数据时,保存观察者到subs,以便当数据变化了的时候,可以通知所有的观察者去update,也可以删除subs里的某个观察者。
export default class Dep {
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 这个方法非常绕,Dep.target就是一个Watcher对象,Watcher把这个依赖加进他的依赖列表里,然后调用dep.addSub再把这个Watcher加入到他的观察者列表里。
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省去了初始化各种属性和option
this.dirty = this.lazy // for lazy watchers
// 解析expOrFn,赋值给this.getter
// expOrFn也要明白他是什么?
// 当是渲染watcher时,expOrFn是updateComponent,即重新渲染执行render
// 当是计算watcher时,expOrFn是计算属性的计算方法
// 当是侦听器watcher时,expOrFn是watch属性的取值表达式,可以去读取要watch的数据,this.cb就是watch的handler属性
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 执行this.getter,同时重新进行依赖收集
*/
get () {
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
if (this.deep) {
// 对于deep的watch属性,处理的很巧妙,traverse就是去递归读取value的值,
// 就会调用他们的get方法,进行了依赖收集
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
/**
* 不重复的把当前watcher添加进依赖的观察者列表里
*/
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)
}
}
}
/**
* 清理依赖列表:当前的依赖列表和新的依赖列表比对,存在于this.deps里面,
* 却不存在于this.newDeps里面,说明这个watcher已经不再观察这个依赖了,所以
* 要让个依赖从他的观察者列表里删除自己,以免造成不必要的watcher更新。然后
* 把this.newDeps的值赋给this.deps,再把this.newDeps清空
*/
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
*/
update () {
if (this.lazy) {
// 对于计算watcher时,不需要立即执行计算方法,只要设置dirty,意味着
// 数据不是最新的了,使用时需要重新计算
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 调度watcher执行计算。
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
this.cb.call(this.vm, value, oldValue)
}
}
}
/**
* 对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
* 说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* 把这个watcher所观察的所有依赖都传给Dep.target,即给Dep.target收集
* 这些依赖。
* 举个例子:具体可以看state.js里的createComputedGetter这个方法
* 当render里依赖了计算属性a,当渲染watcher在执行render时就会去
* 读取a,而a会去重新计算,计算完了渲染watcher出栈,赋值给Dep.target
* 然后执行watcher.depend,就是把这个计算watcher的所有依赖也加入给渲染watcher
* 这样,即使data.b没有被直接用在render上,也通过计算属性a被间接的是用了
* 当data.b发生改变时,也就可以触发渲染更新了
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
queueWatcher
下面是Watcher的update方法,可以看的除了是计算属性和标记了是同步的情况以外,全部都是推入观察者队列中,下一个tick时调用。也就是数据变化不是立即就去更新的,而是异步批量去更新的。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
下面来看看queueWatcher方法
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
这里使用了一个 has 的哈希map用来检查是否当前watcher的id是否存在,若已存在则跳过,不存在则就push到queue,队列中并标记哈希表has,用于下次检验,防止重复添加。因为执行更新队列时,是每个watcher都被执行run,如果是相同的watcher没必要重复执行,这样就算同步修改了一百次视图中用到的data,异步更新计算的时候也只会更新最后一次修改。
nextTick(flushSchedulerQueue)
把回调方法flushSchedulerQueue传递给nextTick,一次异步更新,只要传递一次异步回调函数就可以了,在这个异步回调里统一批量的处理queue中的watcher,进行更新。
function flushSchedulerQueue () {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
resetSchedulerState()
}
每次执行异步回调更新,就是循环执行队列里的watcher.run方法。
在循环队列之前对队列进行了一次排序:
- 组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
- 一个组件的user watchers(侦听器watcher)比render watcher先运行,因为user watchers往往比render watcher更早创建
- 如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过
nextTick
export function nextTick (cb?: Function, ctx?: Object) {
// 这个方法里,我把关于不写回调,使用promise的情况处理去掉了,把trycatch都去掉了。
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
setTimeout(flushCallbacks, 0) // 异步任务进行了简化
}
}
下面是异步的回调方法flushCallbacks,遍历执行callbacks里的方法,也就是遍历执行调用nextTick时传入的回调方法。
你可能就要问了,queueWatcher的时候不是控制了只会调用一次nextTick吗,为啥要用callbacks数组来存储呢。举个例子:
你写了一堆同步语句,改变了data等,然后又调用了一个this.$nextTick来做个异步回调,这个时候不就又会向callbacks数组里push了一个回调方法吗。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
如何把数组处理成可观察对象
不考虑兼容处理
本质就是改写数组的原型方法。当数组调用methodsToPatch这些方法时,就意味者数组发生了变化,需要通知所有观察者update。
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 保存数组的原始原型方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
参考文章
vue.js源码解读系列 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定
原文地址:https://segmentfault.com/a/1190000016922527
vue数据绑定源码的更多相关文章
- 从Vue.js源码角度再看数据绑定
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
- 从template到DOM(Vue.js源码角度看内部运行机制)
写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ...
- Vue.js 源码分析(二十二) 指令篇 v-model指令详解
Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的. v-model本质上不过是语 ...
- vue.js源码精析
MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...
- vue.js使用webpack发布,部署到服务器上之后在浏览器中可以查看到vue文件源码
webpack+vue 2.0打包发布之后,将发布的文件部署到服务器中之后,浏览器中访问的时候会出现一个webpack文件夹,里边会显示vue文件源码 如果不想让vue源文件显示出来,可以在confi ...
- Vue.js源码——事件机制
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
- vue源码分析—Vue.js 源码构建
Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...
- 2018-11-23 手工翻译Vue.js源码:尝试重命名标识符与文本
续前文: 手工翻译Vue.js源码第一步:14个文件重命名 对core/instance/索引中的变量, 方法进行重命名如下(题图): import { 混入初始化 } from './初始化' im ...
- 【转】从Vue.js源码看异步更新DOM策略及nextTick
在使用vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样: <template> <div> <div ref="test" ...
随机推荐
- Codeforces Round #329 (Div. 2)B. Anton and Lines 贪心
B. Anton and Lines The teacher gave Anton a large geometry homework, but he didn't do it (as usual ...
- FileZilla文件下载的目录
连接上ftp服务器之后,在remote site那边邮件选中了目录下载文件,但是下载完成之后. 不知道下载到哪里了,用search everything软件搜了一下,发现就在D盘的根目录. 所以,下载 ...
- Web Tab, Project Properties
https://msdn.microsoft.com/en-us/library/aa983445(v=vs.100).aspx The Web tab of the project Properti ...
- 【Bzoj2456】mode
Position: http://www.lydsy.com/JudgeOnline/problem.php?id=2456 List Bzoj2456 mode List Description S ...
- bzoj 3312 No Change
题目大意: 到商场购物,他的钱包里有K个硬币 想按顺序买 N个物品,第i个物品需要花费c(i)块钱 在依次进行的购买N个物品的过程中,可以随时停下来付款,每次付款只用一个硬币 支付购买的内容是从上一次 ...
- Olddriver’s books
Olddriver 的书多的吓人,什么算法导论,组合数学英 文版(orz)......他把 n 本书都放在身后的桌子上, 每本书有一定的面积,并且书可以覆盖,求 n 本书覆盖桌面 的面积 输入格式: ...
- VPS主机
腾讯云企业认证 备案域名要是顶级域名格式,所以不能是www.blueheartzf.com,而要是blueheartzf.com
- Mybatis 代码自动生成(generatorConfig.xml配置)
博客推荐: Mybatis最入门---代码自动生成(generatorConfig.xml配置) MyBatis Generator generatorConfig.xml配置详解 pom.xml&l ...
- zookeeper单机安装
安装zookeeper步骤: 1,下载zookeeper http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.14/ 2,放到合适目录,解压 ...
- PCB Genesis脚本 C#调用Python
在PCB行业,Genesis的二次开发的编程脚本越来越丰富了啊,从一开始进入眼界的Genesis脚本语言是很少的,CSH,PERL, 再后来慢慢发展,VB,易语言,VB.NET,C#,Java,TCL ...