本文章脱胎于x站的2021上半年各大厂web核心面试题解析(第二期)-路白,视频更加的通俗易懂!
看了关于vue响应式的原理,以这篇笔记记录下来,来加深对于vue响应式原理的印象
在解析vue2的响应式原理之前,首先我们得明白几个概念,或者说几个核心类他们分别的作用
- Observe类,对data函数里的变量属性添加getter/setter,进行数据劫持以及派发更新。
- Watcher类,Watcher类有多种,比如computed watcher,user watcher(自己在watch里定义的需要监听数据变化的watcher)。
- Dep类, 用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs=watcher[],是当前响应式对象dep实例收集的watcher依赖。当数据发生改变的时候,就会触发dep.notify方法,该方法会遍历subs数组,调用每一个watcher的update方法,进行响应式的更新。
好,简单了解完各个核心类的作用,接下来就来逐个的实现,文件结构如下。
index.html -------- html文件,提供根元素
index.js -------- 引入vue.js,添加参数
watch.js -------- watcher类
dep.js -------- dep类
observe.js -------- 观察者类
compiler.js -------- 用于编译指令,如v-html,v-model,v-text
vue.js -------- 主文件
第一步 首先我们先将index.html与index.js文件的初始代码搞定
//index.js
import Vue from './vue.js'
const vm=new Vue({
el:'#app',
data:{
msg:'hello word',
text:'<div>哈哈哈哈</div>',
html:'<h1>变大了</h1>'
},
methods:{
fn(){
alert("111");
}
}
})
console.log(vm);
//index.html
<body>
<div id="app">
{{msg}}
<input type="text" v-model='msg'>
<button type="button" v-on:click='fn'>按钮</button>
<div v-text='text'></div>
<div v-html='html'></div>
</div>
<script src="./index.js" type="module"></script>
</body>
- 我们知道Vue是new出来的,所以可以知道Vue是用class来声明的类。
- html的js引入写上了type='module',意思是告诉浏览器我们使用的是ESM语法,主流浏览器支持这样的声明。
第二步 将vue.js的主体代码写出,有几点是需要实现的
- 将传入的options及相关属性绑定到vue实例上
- 对传入的el属性进行判定
- 通过对vue的vm实例可以知道,vue将data里的属性绑定了一份到vue实例上
import Observe from "./observe.js";
import Complier from "./compiler.js";
export default class Vue{
constructor(options){
this.$options=options;
this.$data=options.data;
this.$methods=options.methods;
//初始化el,对传进来的根元素进行判定
this.initElement(options);
//将data元素绑定到vue实例上
this._proxyData (this.$data);
//数据劫持
new Observe(this.$data);
//编译模板
new Complier(this);
}
initElement(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('请传入css selector或者HTMLElement');
}
}
_proxyData(data){
//通过Object.defineProperty将data里的属性绑定到vue实例上
Object.keys(data).forEach(key=>{
Object.defineProperty(this, key, {
//表示可以被枚举,也就是可以被循环出来
enumerable:true,
//表示可以进行相应配置
configurable:true,
get(){
return data[key];
},
set(newValue){
if(newValue===data[key]){
return
}
data[key]=newValue;
}
})
})
}
}
_proxyData函数用于将属性绑定在vue实例上,而不是用于进行依赖的收集和派发更新,所以不用递归的对每一个值进行劫持。
第三步 梳理架构
这个时候我们先不急着把其余的类实现,而是先将架构先写出来,能够更好地梳理我们的知识点,当然在写架构的时候注释要加上,不然等到写的时候,就忘记了这个是用来干什么的了,得不偿失。
- dep类
//dep类是用来保存watch类的,所以初始的时候就有一个保存watch的数组
export default class Dep{
constructor(){
//watch数组
this.subs=[];
}
//要想放进subs数组,就需要一个add方法
addSubs(watcher){
}
//用于派发更新
notify(){
}
- watch类
export default class Watcher{
constructor(vm, key, cb){
}
update(){
}
}
这里需要三个参数,分别是vm的实例,data的属性名,回调函数
- observe类
export default class Observe{
constructor(data){
}
transver(data){
}
}
这里需要一个data参数,也就是传进来的data,使用Object.defineProperty将data里的每一个属性进行数据的劫持,加上getter/setter,而我们的依赖收集和派发更新就是在这里实现
- complier类
export default class Complier{
constructor(vm){
}
//遍历元素,对模板进行编辑
compile(el){
}
}
就这样就完成了四大类的基本构架,在此基础上添加相应的代码,距离完成又近了一步,是不是看起来还挺简单的。
第四步 dep类的整体实现
//dep类是用来保存watch类的,所以初始的时候就有一个保存watch的数组
export default class Dep{
constructor(){
//watch数组
this.subs=[];
}
//要想保存进subs数组,就需要一个add方法
addSubs(watcher){
if(watcher&&watcher.update){
this.subs.push(watcher);
}
}
//用于派发更新
notify(){
//遍历subs数组,调用每一个watcher的updatae方法
console.log(this.subs);
this.subs.forEach(watcher=>{
watcher.update();
})
}
}
看起来是不是很简单
不过可以先思考两个问题。
- dep类在什么时候实例化?在哪里addSubs?
- dep类在什么时候调用notify方法?
第五步 watcher类的整体实现
import Dep from "./dep.js";
export default class Watcher{
/**
*
* @param {*} vm vm的实例,拿到所需值
* @param {*} key data的属性名,拿到新值,旧值
* @param {*} cb 更新时调用回调函数
*/
constructor(vm, key, cb){
this.vm=vm;
this.key=key;
this.cb=cb;
//为什么要往dep.target上添加watcher实例
Dep.target=this;
//拿到旧值
//同时注意一点,在这里会触发变量的get方法
this.oldValue=vm[key];
Dep.target=null;
}
update(){
//因为数据已经更新,所以拿到的就是最新的值
let newValue=this.vm[this.key];
if(newValue===this.oldValue){
return;
}
this.cb(newValue);
}
}
是不是看起来也很简单,别着急,真正复杂的还在compiler模块中,在这里也有个问题。
- 为什么要往dep.target上添加watcher实例?是为了能够将在同一时间只维持一份watcher,因为在computed里,watch里用到时,会添加多个watcher,容易造成数据紊乱,所以在一个时间里只有一个watcher,保证watcher是正常添加的。
第六步 compiler的整体实现
这一步会比较复杂,主要就是对于元素的节点进行分析,遍历。代码很长,不要惊慌。
import Watcher from "./watch.js";
export default class Complier{
constructor(vm){
this.vm=vm;
this.compile(vm.$el);
}
//遍历元素,对模板进行替换
compile(el){
//拿到元素的子节点,一个类数组
const childNodes=el.childNodes;
Array.from(childNodes).forEach(node=>{
//判断节点类型
if(this.isTextNode(node)){
//文本节点
this.compileText(node);
}else if(this.isElementType(node)){
//元素节点
this.compileElement(node);
}
if(node.childNodes&&node.childNodes.length>0){
this.compile(node);
}
})
}
//判断文本节点
isTextNode(node){
return node.nodeType===3;
}
//判断元素节点
isElementType(node){
return node.nodeType===1;
}
//文本节点编译
compileText(node){
let reg=/\{\{(.+?)\}\}/;
let value=node.textContent;
if(reg.test(value)){
const key=RegExp.$1.trim();//拿到msg
node.textContent=value.replace(reg, this.vm[key]);//替换成功
//数据需要动态改变,所以需要依赖收集
new Watcher(this.vm, key, (newValue)=>{
node.textContent=value.replace(reg, newValue);;
})
}
}
//元素节点编译
compileElement(node){
if(node.attributes.length>0){
Array.from(node.attributes).forEach(attr=>{
//属性名
const attrName=attr.name;
//判断是否v-开头
if(this.isVStartsWith(attrName)){
//判断是否有:号,例如v-on:click v-model
const directiveName=attrName.indexOf(':')>-1?attrName.substr(5):attrName.substr(2);
//拿到值,例如v-model='msg'的msg
let key=attr.value;
this.update(node, key, directiveName);
}
})
}
}
//判断v-开头
isVStartsWith(attr){
return attr.startsWith('v-');
}
//进行拼接,使之对应函数
update(node, key, directiveName){
const updaterFn=this[directiveName+'Updater'];
updaterFn && updaterFn.call(this, node, this.vm[key], key, directiveName);
}
//v-model
modelUpdater(node, value, key){
//对应于input
node.value=value;
new Watcher(this.vm, key, (newValue)=>{
node.value=newValue;
})
node.addEventListener('input', ()=>{
//会触发setter
this.vm[key]=node.value;
})
}
//v-text
textUpdater(node, value, key){
node.textContent=value;
new Watcher(this.vm, key, (newValue)=>{
node.textContent=newValue;
})
}
//v-html
htmlUpdater(node, value, key){
node.innerHTML=value;
new Watcher(this.vm, key, (newValue)=>{
node.innerHTML=newValue;
})
}
//v-on:click
clickUpdater(node, value, key, directiveName){
node.addEventListener(directiveName, this.vm.$methods[key]);
}
}
啊,搞定了这一步,接下来就是最后一步了,加油,快成功了!!!
第七步 observe类的实现
import Dep from './dep.js'
export default class Observe{
constructor(data){
this.transver(data);
}
transver(obj){
if(!obj || typeof obj!=='object'){
return
}
//遍历监听数据
Object.keys(obj).forEach(data=>{
this.defineReactive(obj, data, obj[data]);
})
}
defineReactive(obj, data, value){
//可能又是一个object
this.transver(value);
//在这里实例化dep
let dep=new Dep();
//保存this
const that=this;
Object.defineProperty(obj, data, {
enumerable:true,
configurable:true,
get(){
//在这里添加依赖,拿到绑定在Dep身上的watcher
Dep.target && dep.addSubs(Dep.target);
//注意,这一步不能够用obj[data],会造成循环的get这个值
return value;
},
set(newValue){
if(value===newValue){
return;
}
value=newValue;
//可能是一个object
that.transver(newValue);
//在这里派发更新
dep.notify();
}
})
}
}
至此,大功造成,能完整的看下来真的是毅力可嘉,不妨跟着视频手写一遍,记忆会更加深刻。