把你了解的vue响应式原理阐述一下
三个核心类
1.observer:给对象的属性添加getter和setter,用于依赖收集和派发更新
2.Dep:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例
dep.subs = watcher[].
当数据发生变更的时候,会通过dep.notify()通知各个watcher。
3.watcher: 观察者对象,render watcher,computed watcher,user watcher
依赖收集
1.initState,对computed属性初始化时,会触发computed wathcer 依赖收集。
2.initState, 对监听属性初始化的时候,触发的user watcher 依赖收集。
3.render.触发 render watcher 依赖收集
派发更新 Object.defineProperty.
1、组件中的对响应数据进行修改,会触发setter逻辑
2、dep.notify()
3.遍历所有subs,调用每一个watcher的update方法。
总结原理
当创建VUE实例时,vue 先遍历data的属性。Object.defineProperty为属性添加getter和setter对数据的读取进行劫持。
getter: 依赖收集
setter: 派发更新
每个组件的实例都会对应的watcher实例。
计算属性的实现原理
compute watcher, 计算属性的监听器。
computed watcher 持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值
当computed 的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher会将dietary 属性设为true,并对计算属性方法的调用
1、计算属性computed所谓的缓存是什么?
计算属性是基于它的响应式依赖进行缓存的, 只有依赖发生改变时才重新求值
2. 实际应用特性
比如计算属性方法非常耗时,遍历一个特别大的数组,计算一次可能非常耗时
3.一下情况, computed可以监听到数据的变化吗? 不可以
computed: {
storageMsg: function(){
return sessionStorage.getItem('xxx');
},
time: function(){
return Date.now();
}
}
created(){
sessionStorage.setItem('xxx', 111);
}
onClick(){
sessionStorage.setItem('xxx', Math.random());
}
watch与computed区别(都做响应式变化监听)
watch 用于监听到某个属性的变化后做什么动作或逻辑(复杂)才使用它,没有缓存
computed 只有在data里初始化过的数据才会被监听到,响应式更新数据,用于格式转换等简单的操作,不适合做复杂的操作。
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渲染-》宏任务
什么时候用到nextTick呢?
在数据变化后要执行某个操作,因为数据改变而改变的Dom,这个操作应该放在vue.nextTick里面
<template>
<div v-if="loaded" ref="test"></div>
</template>
async showDiv(){
this.loaded = true;
await vue.nextTick();
this.$refs.test.xxx();
}
手写简单的vue, 实现简单的响应式更新
index.html 主页面
vue.js vue主文件
compiler.js 编译模板,解析指令, v-model v-html
dep.js 收集依赖关系,存储观察者。// 以发布订阅的形式实现
oberserver.js 数据劫持
watcher.js 观察者对象类
index.html
<div id="app">
<h1>模版表达式</h1>
<h3>{{msg}}</h3>
<br />
<h1>v-text测试</h1>
<div v-text="msg"></div>
<br />
<h1>v-html测试</h1>
<div v-html="testHtml"></div>
<br />
<h1>v-model测试</h1>
<input type="text" v-model="msg"/>
<input type="text" v-model="count">
<button v-on:click="handler">按钮</button>
</div>
<script src="./index.js" type="module"></script>
index.js
import Vue from './vue.js';
const vm = new Vue({
el: '#app',
data:{
msg: 'hello vue',
name: 'sb',
count: 100,
testHtml: '<ul><li>ddddd</li></ul>',
obj: {
width: 222,
height: 111,
child: {
name: 111,
}
}
},
methods: {
handler(){
alert('111');
}
}
})
console.log(vm);
vue.js
import Observer from './observer.js';
import Compiler from './compiler.js';
/**
* 包括vue的构造函数,及接收的各种配置参数
*
*/
export default class Vue{
constructor(options = {}){
this.$options = options;
this.$data = options.data;
this.$method = options.methods;
this.initRootElement(options);
this._proxyData(this.$data);
// 实例化Object.defineProperty将data里的属性注入到vue实例中
new Observer(this.$data);
// 实例化compiler对象,解析指令和模板表达式 {{msg}}
new Compiler(this);
}
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('传入数组参数有误');
}
}
_proxyData(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){
return data[key];
},
set(newData){
if(data[key] === newData){
return ;
}
data[key] = newData;
}
})
})
}
}
observer.js
import Dep from "./dep.js";
export default class Observer{
constructor(data){
this.traverset(data);
}
// 递归 遍历data里的所有属性
traverset(data){
if(!data || typeof data !== 'object'){
return;
}
Object.keys(data).forEach(key =>{
this.defineReactive(data, key, data[key]);
})
}
// 给传入的数据设置 getter/setter
defineReactive(obj, key, val){
this.traverset(val);
const dep = new Dep();
let that = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get(){
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newValue){
if(newValue === val){
return;
}
val = newValue;
that.traverset(newValue);// 设置的时候可能设置一个对象
dep.notify();
}
})
}
}
compile.js
import Watcher from './watcher.js';
export default class Compiler{
constructor(vm){
this.el = vm.$el;
this.vue = vm;
this.methods = vm.$methods;
this.compile(vm.$el);
}
// 编译模版
compile(el){
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node=>{
// 文本节点
// 元素节点
if(this.isTxtNode(node)){
this.compileText(node)
}
if(this.isElementNode(node)){
this.compileElement(node);
}
// 子节点,递归调用
if(node.childNodes && node.childNodes.length > 0){
this.compile(node);
}
})
}
compileText(node){
// {{ msg }}
const reg = /\{\{(.+?)\}\}/;
const value = node.textContent;
if(reg.test(value)){
const key = RegExp.$1.trim(); // msg
node.textContent = value.replace(reg, this.vue[key]);
new Watcher(this.vue, key, (newValue)=>{
node.textContent = newValue;
})
}
}
compileElement(node){
if(node.attributes.length){
// 遍历元素节点的所有属性
Array.from(node.attributes).forEach(attr=>{
const attrName = attr.name;
if(this.isDirective(attrName)){
let directiveName = attrName.indexOf(':')> -1 ? attrName.substr(5) : attrName.substr(2);
let key = attr.value;
this.update(node, key, directiveName);
}
});
}
}
update(node, key, directiveName){
const updateFn = this[directiveName + 'Updater'];
// v-model , v-text, v-html, v-on:click
updateFn && updateFn.call(this, node, this.vue[key], key,directiveName);
}
textUpdater(node, value, key){
node.textContent = value;
new Watcher(this.vue, key, (newValue) =>{
node.textContent = newValue;
})
}
modelUpdater(node, value, key){
node.value = value;
new Watcher(this.vue, key, (newValue)=>{
node.value = newValue;
})
node.addEventListener('input', ()=>{
this.vue[key] = node.value;
})
}
htmlUpdater(node, value, key){
node.innerHTML = value;
new Watcher(this.vue, key, (newValue)=>{
node.innerHTML = newValue;
})
}
clickUpdater(node, value, key, directiveName){
node.addEventListener(directiveName, this.vue.$method[key])
}
/**
* 判断元素属性是否是指令
* @param {} node
*/
isDirective(attrName){
// v-mode v-html
return attrName.startsWith('v-');
}
isTxtNode(node){
return node.nodeType === 3;
}
isElementNode(node){
return node.nodeType === 1;
}
}
sub.js
/**
* 发布订阅的模式
* 存储所有观察者, watcher
* 每个watcher都有一个update
* 通知subs里的每个watcher实例,触发update方法
*/
export default class Dep{
constructor(){
// 存储所有观察者
this.subs = [];
}
// 添加观察者
addSub(watcher){
if(watcher && watcher.update){
this.subs.push(watcher)
}
}
// 通知观察者
notify(){
this.subs.forEach(watcher=>{
watcher.update();
})
}
}
// Dep 在哪里实例化, 在哪里addSub;
// Dep notify在哪里调用?
watch.js
import Dep from "./dep.js";
export default class Watcher{
// vm =》vue实例
/**
*
* @param {*} vm vue实例
* @param {*} key data中属性名
* @param {*} cb 负责更新试图的回调函数
*/
constructor(vm, key, cb){
this.vm = vm;
this.key = key;
this.cb = cb;
Dep.target = this; // 获取旧值
// 触发get方法,在get方法里会做一些操作?
this.oldValue = vm[key];// 当前的值
Dep.target = null;
}
// 当数据变化当时候更新试图
update(){
let newValue = this.vm[this.key];
if(this.oldValue === newValue){
return;
}
this.cb(newValue);
}
}
// watcher 初始化oldValue的时候,会去做一些什么操作?
// 通过vm[key]获取oldValue前,为什么要将当前的实例挂在Dep上,获取之后为什么为什么只为null
// update 在什么执行的