vue 源码自问自答-响应式原理
最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然。
所以打算把自己掌握的知识,试着组织成自己的语言表达出来
不打算平铺直叙的写清楚 vue 源码的前因后果和全部细节,而是以自问自答的形式,回答我自己之前的疑惑,
如果有错误的地方,欢迎指正哈~
Vue 的双向数据绑定原理
Vue 实现响应式的核心 API 是 ES5 的 Object.defineProperty(obj,key,descriptor),Vue 的「响应式」和「依赖收集」都依靠这个 API
它接受 3 个参数,分别是 obj / key / 描述符,返回的是一个包装后的对象
它的作用就是,用这个 API 包装过后的对象可以拥有 getter 和 setter 函数。
getter 会在对象的这个 key 被获取时触发,setter 会在这个对象的 key 被修改时触发。
一个 Vue 项目的开始, 通常是从 Vue 构造函数的实例化开始的。
new Vue()的时候会执行一个_init()方法,会初始化属性,比如 props/event/生命周期钩子,也包括 data 对象的初始化。
Vue 在初始化时,将 data 对象上的所有 key,都包装成拥有 getter 和 setter 的属性。
- 渲染页面时,会执行 render function(无论是用 template 还是 render 最终都会生成 render 函数)
- 执行 render function 会获取 data 对象上的属性,所以会触发对应属性的 getter 函数
- getter 触发时,主要就做 2 个事情。 1.把值返回 2.依赖收集,也就是讲 watcher 存放到 Dep 实例的一个队列里。
- 当修改对象的值触发 setter,setter 同样是做 2 个事情。1.把 newVal 设置好 2.用一个循环通知之前存放在 dep 实例中 watcher 对象们,watcher 对象调用各自的 update 方法来更新视图
实现双向数据绑定的demo1 - 忽略「收集依赖」的版本
function cb() {
console.log("更新视图");
}
function defineReactve(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log("触发了getter");
return val;
},
set: newVal => {
console.log("触发了setter");
if (newVal === val) return;
val = newVal;
cb()
}
});
}
function observe(data) {
function walk(data) {
Object.keys(data).forEach(key => {
if (typeof data[key] === "object") {
walk(data[key]);
} else {
defineReactve(data, key, data[key]);
}
});
}
walk(data);
}
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data);
}
}
var vm = new Vue({
data: {
msg: "test",
person: {
name: "ziwei",
age: 18
}
}
});
vm._data.person.name = 'hello'
// 触发setter和cb函数,从而视图更新
实现双向数据绑定的demo2 - 「收集依赖」的版本
function defineReactve( obj, key, val ) {
const dep = new Dep()
Object.defineProperty( obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log( "触发了getter" );
dep.addSub(Dep.target)
return val;
},
set: newVal => {
console.log( "触发了setter" );
if ( newVal === val ) return;
val = newVal;
dep.notify() // 通知队列的wather去update视图
}
} );
}
function observe( data ) {
function walk( data ) {
Object.keys( data ).forEach( key => {
if ( typeof data[ key ] === "object" ) {
walk( data[ key ] );
} else {
defineReactve( data, key, data[ key ] );
}
} );
}
walk( data );
}
class Dep{
constructor(){
this.subs = []
}
addSub(){
this.subs.push(Dep.target)
}
notify(){
this.subs.forEach(sub => {
sub.update()
})
}
}
Dep.target = null
class Watcher{
constructor(){
Dep.target = this
}
update(){
console.log('update更新视图啦~')
}
}
class Vue {
constructor( options ) {
this._data = options.data;
observe( this._data );
new Watcher() // 模拟页面渲染,触发getter,依赖收集的效果
this._data.person.name
}
}
var vm = new Vue( {
data: {
msg: "test",
person: {
name: "ziwei",
age: 18
}
}
} );
vm._data.person.name = 'hello'
一些省略掉的环节
这样就是 Vue 响应式的一个基本原理,不过我描述的过程中,也省略了很多环节,比如
- Vue 是如何实现给 data 对象上的属性都拥有 getter 和 setter 的
- 为什么要进行「依赖收集」,
- 如何避免重复「收集依赖」
- watcher 调用 update,也并不是直接更新视图。实现上中间还有 patch 的过程以及使用队列来异步更新的策略。
Vue 是如何实现给 data 对象上的属性都拥有 getter 和 setter 的
通过循环data对象,给对象的每一个key,用Object.defineProperty包装
遍历时,如果发现data[key]也是对象的话,需要用递归
为什么要进行「依赖收集」?
举2个场景的栗子🌰
- 1.如果我们不进行依赖收集,那页面里有2处使用了data上的数据,当这个数据发生变化时,会触发setter,然后setter当中进行视图更新。
但问题在于,你并不知道谁依赖我?我应该更新哪几个地方
- 2.如果没有依赖收集,我们修改了data中的msg,按说我们要触发setter更新视图,但是如果视图里并没有用到data.msg的话,实际上不应该更新的。
如何避免重复「收集依赖」
在往Dep中放wather对象是,实际上wather的update方法时,会把其放入queue队列中,会通过watch.id判断是否重复了,重复的wather就不会被推入队列
watcher 调用 update,也并不是直接更新视图。实现上中间还有 patch 的过程以及使用队列来异步更新的策略。
异步更新的策略,类似于setTimeout(fn,0) ,目的就是为了避免频繁的更新dom,让页面的渲染的性能更好。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。