数据驱动视图
把数据理解为状态,而视图就是用户可直观看到页面。
UI = render(state)
变化侦测就是追踪状态即数据的变化,变化侦测是响应式系统的核心
getter/setter 是设计对象对外暴露的计算属性用的,是对象本身有意而为之,让一个属性访问像调用一个方法一样, 对象内部能监控某个属性的访问,对外又像一个普通属性一样。
javascript对象编程
Proxy是设计模式的实现,其意图是先有一个对象,你用 proxy 在上面加了一层代理,被代理对象本身是不知情它的属性访问被人监控了
变化侦测(observer)
本文主要围绕object.defineProperty方法展开。
一开始JavaScript没有元编程能力,所以采用object.defineProperty方法,现在vue3好像已经借助Proxy实现变化侦测,简洁了许多。
侦测数据的变化,分为两种类型:推(vue),拉(React、Angular)
Angular脏检查
React虚拟DOM
vue推,粒度越细,开销越大。vue2引入虚拟DOM且粒度调整为中等粒度(组件)。即一个组件共用一个watcher
实例初始化中,调用 initState 处理部分选项数据,initData 用于处理选项 data
// initData 函数的最后一句代码:
// observe data
observe(data, true /* asRootData */)
├─dist # 项目构建后的文件,会找到不同的vue.js构建版本
├─scripts # 与项目构建相关的脚本和配置文件
├─flow # flow的类型声明文件
├─packages # vue-server-render和vue-template-compiler,作为单独的NPM包发布
└─test # 项目测试代码
├─src # 项目源代码
│ ├─complier # 与模板编译相关的代码
│ ├─core # 通用的、与运行平台无关的运行时代码
│ │ ├─observe # 实现变化侦测的代码
│ │ ├─vdom # 实现virtual dom的代码
│ │ ├─instance # Vue.js实例的构造函数和原型方法
│ │ ├─global-api # 全局api的代码
│ │ └─components # 内置组件的代码
│ ├─server # 与服务端渲染相关的代码
│ ├─platforms # 特定运行平台的代码,如weex、web
│ ├─sfc # 单文件组件的解析代码
│ └─shared # 项目公用的工具代码
├─types # TypeScript类型定义
│ ├─test # 类型定义测试
observer类位于源码的src/core/observer/index.js
// 源码位置:src/core/observer/index.js
/**
* Observer类
*/
export class Observer {
constructor (value) {
this.value = value;
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this); // 给value新增一个__ob__属性,值为该value的Observer实例
if (Array.isArray(value)) {
// 当value为数组时的逻辑
} else {//对象
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])//通过递归的方式把一个对象的所有属性都转化成可观测对象
}
}
}
如果数据有__ob__属性,表示它已经被转化成响应式的 。
从上源码可见,vue中变化侦测分别有针对数组和对象的变化侦测。
对象的变化侦测
在JavaScript中,侦测一个对象的变化有两种方法,Object.defineProperty和Proxy代理。
- Data初始化时,通过observer调用Object.defineProperty,定义了getter/setter的形式来追踪变化。
- 当外界通过Watcher读取数据时,会触发getter,两者通过(全局window.target)将Watcher添加到数据的依赖管理dep中。(计算属性等需要响应式依赖的地方生成watcher)
- 当数据发生了变化时,会触发setter,setter中调用 dep.notify(),向Dep中的每一个依赖(即Watcher)发送通知。
- Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等
不足:仅仅只能观测到object数据的取值及设置值,无法观测到向object添加、删除一对key/value。
为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete
object.defineProperty
补充:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性或者修改现有属性,并返回此对象。
注意:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。可以联系vue3代理实现数据响应式来思考
所以这里:只有当实例被创建时就已经存在于 data 中的 property 才是响应式的,所有响应式的property需要在一开始列出。
Object.defineProperty(obj, prop, descriptor)
import { def } from ‘…/util/index’
def(obj,prop,value);//后面新增属性
object.defineProperty
defineReactive 也算是闭包的一个运用
function defineReactive (obj,key,val) {
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}
依赖管理器Dep类
依赖保存在Observer实例上,因为observer在getter、setter、拦截器中都可以访问到。
// 源码位置:src/core/observer/dep.js
export default class Dep {
constructor () {
this.subs = [];//初始化了一个subs数组,用来存放依赖
}
addSub (sub) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub) {
remove(this.subs, sub)
}
// 添加一个依赖
depend () {
if (window.target) {
this.addSub(window.target)
}
}
// 通知所有依赖更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/**
* Remove an item from an array
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
Watcher类
计算属性等需要响应式依赖的地方生成watcher
计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中调用initComputed 函数完成的
谁使用了数据,谁就是依赖,为依赖创建一个Watcher实例。
Watcher把自己设置到全局(window.target),然后读取数据,触发这个数据的getter。
在getter中就会从全局(window.target)获得Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。
this.cb.call(this.vm, this.value, oldValue);实现改变视图
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get();//调用实例方法,重点
}
get () {//初始化即被调用
window.target = this;//把实例自身赋给了全局的一个唯一对象
const vm = this.vm
let value = this.getter.call(vm, vm);//获取一下被依赖的数据,同时触发该数据上面的getter(加入依赖)
// 在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中
//完毕最后将window.target释放掉。
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
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
}
}
对后文中watch方法中deep=true 深度侦测的支持,实现原理是,在释放window.target之前,遍历子值,触发收集子值依赖
数组的变化侦测
Object.defineProperty,这个方法是对象原型上的,所以Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制。
数组通过get收集依赖(同上),在拦截器中触发依赖
不足:
对于数组变化侦测是通过拦截器实现的,也就是说只要是通过数组原型上的方法对数组进行操作就都可以侦测到,但是其他数组特有的语法就会被忽略,例如通过数组的下标来操作数据、采用length来清零。(对应可以使用set、和splice方法来规避)
所以Vue增加了两个全局API:Vue.set和Vue.delete
思考下标给数组赋值的实质应该是新增键值对,对应上面对象中新增属性。
思考JavaScript中数组的初始化是怎样的?空对象,不含值属性,只是实现了属性方法。
创建拦截器:
为什么不直接覆盖Array.prototype,因为这样会污染全局的Array,而我们只希望对响应式的数组设置拦截器。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)
// 改变数组自身内容的7个方法,即变异方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 创建拦截器
*/
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]// 缓存 original 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
})
})
注意:这里通过inserted拿到新增的元素并对他进行侦测
补充:不对原数组操作的数组方法
filter、concat、slice
这里直接重写方法
为什么只对对象劫持,而要对数组进行方法重写?
因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案
挂载拦截器
‘proto’ in {}
判断了浏览器是否支持__proto__,
如果支持,把value.proto 赋值为 拦截器对象arrayMethods;
如果不支持,把拦截器中重写的7个方法循环加入到value上。
// 源码位置:/src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()//依赖挂载在observer实例上
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {//对象
this.walk(value)
}
}
/**
* 挂载拦截器在__proto__ 上,不存在则新建方法属性
*/
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])//给target新增一个key属性,属性值是src[key]
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
this.observeArray(value)
数组的深度侦测
不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化。
在Vue中,不论是Object型数据还是Array型数据所实现的数据变化侦测都是深度侦测。
是通过上面observeArray方法中的observe来实现的
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {//不支持非对象、vNode
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
响应式
当一个 Vue 实例被创建时,除了创建HTML,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
Object.freeze(),这会阻止修改现有的 property,也意味着响应系统无法再追踪变化。
值得注意的是只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。如果后来添加一个新的 property,不是响应式
在页面显示seconds,计时
var app = new Vue({
name: 'my-app',
el: '#root',
// Some data
data:{
seconds:0
},
created(){
setInterval(()=>{
this.seconds++;
},1000);
}
})
与数据相关的实例方法
$watch
expOrFn{string键路径 | 有返回值的Function}
[options]是一个对象包含{boolean} deep、{boolean} immediate
deep: true ,发现对象内部值的变化,注意监听数组的变动不需要这么做。 immediate: true
将立即以表达式的当前值触发回调:
vm.$watch( expOrFn, callback, [options] );
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
返回值:取消观察函数
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()
方法源码
Vue.prototype.$watch = function (expOrFn,cb,options) {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
$set和delete
在vue的响应中,对于object型数据,添加一对新的key/value或删除一对已有的key/value时,Vue是无法观测到的;而对于Array型数据,当我们通过数组下标修改数组中的数据时,Vue也是是无法观测到的。所以提出了全局方法
vm.$set 是全局 Vue.set 的别名,其用法相同。
参数:
{Object | Array} target 不能是vue.js实例、或者vue.js实例的根数据对象
{string | number} propertyName/index
{any} value
vm.$set( target, propertyName/index, value );//返回:设置的值。
vm.$delete( target, propertyName/index );//没有返回值
发布订阅者模式
vm.$on和emit
发布订阅模式
vm.$on( event, callback )
vm.$on('test', function (msg) {
console.log(msg)
})
vm.$emit('test', 'hi')
//该方法接收的第一个参数是要触发的事件名,之后的附加参数都会传给被触发事件的回调函数。
所有绑定在这个实例上的事件都会存储在实例的_events属性中。
vm.$off
如果没有提供参数,则移除所有的事件监听器;
如果只提供了事件,则移除该事件所有的监听器;
如果同时提供了事件与回调,则只移除这个回调的监听器。
vm.$off( [event, callback] )
vm.$once
监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
Vue.prototype.$once = function (event, fn) {
const vm: Component = this
function on () {
vm.$off(event, on);//取消监听之后执行回调
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
watch和computed
watch和computed都是以Vue的依赖追踪机制为基础的,都是希望在依赖数据发生改变的时候,被依赖的数据根据预先定义好的函数,发生“自动”的变化。可以写代码完成这一切,但可能写法混乱,代码冗余的情况。Vue为我们提供了这样一个方便的接口,统一规则(框架的魅力)
watch是监听+事件,first变化触发事件执行
watch: {
first: function (val) { this.fullName = val + this.lastName }
}
b:{//深度监听,可监听到对象、数组的变化
handler(val, oldVal){
console.log("b.c: "+val.c, oldVal.c);
},
deep:true //true 深度监听
immediate: true,//不等变化,申明后立即执行
}
computed是计算属性,事实上和和data对象里的数据属性是同一类的(使用上)
计算属性
计算属性computed介于data对象的属性和方法之间,可以像访问data对象的属性那样访问它,但仍需要以函数的方式定义它。
计算属性与方法的区别:
计算属性会被缓存,多次调用一个计算属性代码只执行一次,后面会使用缓存值(计算依赖变化,重新计算)
也可以将计算属性由函数改为带有get、set属性的对象,这样就可以设置它的值
computed:{
full: function () { return this.firstName + lastName }
}
//this.fullName取用,类似data
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
从上显然可见
watch擅长处理 :一个数据影响多个数据
computed擅长处理 :一个数据受多个数据影响
computed原理
实质是定义在实例上的一个特殊的getter方法,方法中结合watcher实现缓存和收集依赖功能。缓存是通过watcher的dirty属性实现,当计算属性中的内容变化,计算属性的watcher收到通知,将自己的dirty属性设置为true
有人提出提案,应该先对比计算属性的值再重新渲染。
watch监听一个对象
<html>
<head>
<title>VueJS</title>
</head>
<body>
<!-- Include the library in the page -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- Some HTML -->
<div id="root">
<label> <input type="text" v-model="fo.name" ></label>
</div>
<!-- Some JavaScript -->
<script>
// New VueJS instance
var app = new Vue({
el: '#root',
// Some data
data:{
fo:{
name:""
}
},
watch:{
'fo.name'(cur,old){//有.的时候用引号包围
console.log('old:', old)
console.log('cur:', cur)
}
}
})
</script>
</body>
</html>
注意,监听一个对象的时候,分两种。默认整个对象被替换时候触发,但是监听内部设置deep:true后可以深度监听,即使属性变化也会触发。