把你了解的vue响应式的原理阐述一下?
vue中有三个核心类
首先了解vue中的三个核心类
- Observer: 用来给对象的属性添加getter和setter,用来***依赖收集***和***派发更新***。
- Dep:用来收集当前响应式对象的依赖关系,每一个响应式对象都有一个dep实例,dep.subs = watcher[].是一个watcher数组。当数据发生变更的时候,会通知dep.notify()通知各个watcher。
- Watcher:观察者对象, render watcher 渲染,computed watcher 计算, user watcher监听
- 依赖收集
- initState,初始化vue组件实例的时候,对computed属性初始化时,会触发computed watcher 依赖收集。
- initState,对监听属性初始化的时候,触发的user watcher依赖收集。
- render,渲染的过程,触发render watcher依赖收集。
- 派发更新
Object.defineProperty
- 组件中对相应的数据进行了修改,或触发setter逻辑。
- dep.notify()
- 遍历所有subs,调用每一个watcher的update方法(更新视图)。
总结原理:
当创建vue实例的时候,vue先遍历data里面的属性。data初始化属性。Object.definProperty.为属性添加getter和setter对数据的读取进行劫持。
getter:依赖收集
setter:派发更新
每一个组件实例都会有对应的watcher实例。
计算属性的实现原理
data 是属性
computed : { // 都是一个个的方法,重新调用求值
test() {
{
}
computed watcher,计算属性的监听器。
computed watcher 持有一个dep实例,通过dirty属性编辑属性是否需要重新求值。
当computed的依赖值改变后,就会通知订阅的wathcer进行更新,对于computed watcher 会将 dirty 属性设置 true,并且进行计算属性方法的调用。
- computed 所谓的缓存指的是什么?
计算属性是基于他的响应式依赖进行缓存的,只有雨来大声改变时才会重新求值。
- 实际应用中怎么用的?真实项目中使用过吗?那computed缓存存在的意义是什么?或者你经常在什么时候使用?
比如计算属性的方发内部操作非常的耗时,遍历一个极大的数组,计算一次需要一秒或者更多。
const largeArray = [
{ ... },
{ ... },
{ ... }
] // 10w 条
data: {
id: 1
}
computed: {
currentItem: function() {
return largeArray.find(item => item.id === this.id);
},
stringId: function () {
return String(this.id);
}
}
- 一下情况,computed 可以监听到数据的变化吗?(比如 : 格式化转化,有缓存)
watcher 没有缓存,变化之后接下来做什么动作,调用什么函数
computed 简单的转换操作,有缓存
template
{{ storageMsg }}
computed: {
storageMsg: function(){
return sessionStorage.getItem('xxx'); // 获取不到数据,没有响应的更新上去就不会计算
},
time: function(){
return Date.now(); // 也不可以,没有响应的更新上去就不会计算
}
}
created() {
sessionStorage.setItem('xxx', 1111)
}
onClick() {
sessionStorage.setItem('xxx', Math.random())
}
vue.nextTick()
Vue.nextTick(() => {
})
await Vue.nextTick();
vue 是异步执行 dom 更新的,一旦观察到数据的变化,不会立即执行,开启异步队列,把同一个event loop 中观察数据变化的watcher推送到这个队列中。去除掉重复的事件操作。
在下一次事件循环时,vue清空异步队列,进行dom更新。
怎么样开启异步队列?
Promise.then > MutationObserver -> setImmediate > setTimeout …
vm.someData = ’ new value ’ dom 并不会立即更新,而是在异步队列被清空时才会更新dom。
里面的函数在下一轮事件队列中执行
红任务 -> 微任务队列 -> ui render(只有浏览器把元素渲染到页面,之前已经拿到新的dom,只是没有展现到浏览器上)
什么时候用到nextTick?
- 在数据变化后,要执行某个操作,而这个依赖因数据改变而改变的dom,这个操作就应该被放到vue.nextTick回调中。
<template>
<div v-if="loaded" ref="test"></div>
</template>
async showDiv() {
this.loaded = true;
// this.$refs.test // 同步获取不到test
await Vue.nextTick(); // 这个操作之后,才可以获取到test
this.$refs.test.xxx();
}
手写一个vue,实现响应式的更新。
- 首先新建一个目录
- index.html 主页面
- vue.js vue主文件
- compiler.js 编译模板,解析指令, v-model v-html
- dep.js 收集依赖关系,存储观察者
- watcher.js 观察者对象类
- index.html 内容
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ir=edge">
<title>My Vue</title>
</head>
<body>
<div></div>
</body>
</html>
- 初始化Vue class
/**
*包括vue构造函数,接受各种配置参数等等
*/
export default class Vue {
// 构造函数
constructor (options ={}) {
this.$options = options;
this.$data = options.data;
this.$methods = options.methods;
this.initRootElement(options);
}
/**
*获取根元素,并存储到vue实例。简答检查一下传入的是否合规。
*/
initRootElement(options){
if(typeof options.el === 'string') {
this.$el = document.querySelector(options.el);
} else if (options.el instanceof HTMLElement) {
this.$el = options.el;
}
if(!this.$el){
throw new Error('传入的el不合法,请传入css selector 或者 htmlelement')
}
}
}
- 验证一下,新建一个index.js
import Vue from 'OpenCourse/myvue/vue.js'
const vm = new Vue({
el: '#app',
data: {
msg: 'hello lubai',
},
methods: {
handler() {
alert(999);
}
}
})
- 输入一个错误的el
- 输入一个htmlelement
- vue里可以通过this来获取data
_proxyData(data){
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
get() {
if(data[key] === mewValue){
return;
}
data[key] = newValue;
}
})
})
}
- 接下来,先把几个核心声明好
具体的实现先不管
写好注释,对于这种外界可能引用到的方法,最好用jsDoc来注释。
- dep.js
export default class Dep {
constructor() {
// 存储所有的观察者
rhis.subs = [];
}
/**
*添加观察者
*/
addSub(watcher){
}
/**
* 发送通知
*/
notify (watcher) {
}
}
- observer.js
export default class Observer{
constructor(data){
this.traverse(data);
}
/** 递归遍历data里的所有属性 */
traverse(data){
}
/** 给传入的数据设置 getter 和 setter */
defineReactive(obj, key, val) {
// TODO 递归遍历
}
}