这是我参与更文挑战的第 11 天,活动详情查看: 更文挑战
2021-06-11 总结 TANGJIE Vue2 手写vue
宝刀未老,凿石开路识vue2(一)
1. 对象的响应式原理(vue2)
1.1 理解对象
在js中一般会使用内部的一些特性来描述对象的属性特性,属性特征分为两种——数据属性和访问器属性
-
数据属性:
Configurable、Enumerable、Writable、ValueConfigurable:表示数据是否能被delete操作符删除并重新定义,是否能修改它的特性,以及是否可以把它改为访问器属性,默认为true;Enumerable:表示可被枚举,即可以for-in循环返回,默认为true;Writable:表示属性值可被修改,默认为true;Value:表示属性值,默认为:undefined;
-
访问器属性:
getter(get()函数)、setter(set()函数)
对象的描述属性是不能直接被定义访问的,必须要使用Object.defineProperty();
1.2 基于Object.defineProperty()的对象响应式原理
vue2的响应式函数其实就是基于Object.defineProperty()实现的,借助了对象的属性特征
const getDataType = function (val = 0) {
let type = typeof val
// object需要使用Object.prototype.toString.call判断
if (type === 'object') {
let typeStr = Object.prototype.toString.call(val)
// 解析[object String]
typeStr = typeStr.split(' ')[1]
type = typeStr.substring(0, typeStr.length - 1)
}
return type.toLowerCase()
}
// 对象的响应式原理
function defineReactive( obj,key,value){ // 看到这个入参模式,有没有想起Vue.set/ vm.$set();
objserve(value);
Object.defineProperty(obj,key,{
get(){
console.log(value,'get')
return value
},
set(newValue){
if( newValue !== value){
// 触发视图更新
console.log('触发视图更新','set')
objserve(newValue);
value = newValue;
}
}
})
}
// 对象的响应式处理
function objserve(obj){
if(getDataType(obj) !=='object' ){
return
}
Object.keys( obj ).forEach( key => defineReactive(obj, key, obj[key]))
}
2. Vue2的响应式
2.1 基本分析
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中;【Observer执行数据的响应化】- 对模板执行编译,找到其中的动态绑定的数据,从data中获取并初始化视图,这个过程发生在
Compile中;【Compile,编译模板,初始化视图,收集依赖】 - 定义了一个更新函数和
Watcher,将来对应数据变化时Watcher会调用更新函数;【Watcher,执行更新函数,更新dom】 - 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家dep来管理多个
Watcher; - 将来的data中数据一旦发生变化,会首先找到对应的
Dep,通知所有的Watcher执行更新函数;【Dep管理多个Watcher,批量更新】
如图:
2.2 vue响应式数据的基本实现
- 构建Vue类,初始化选项
const proxy = (vm)=>{
Object.keys( vm.$data ).forEach( key=>{
Object.defineProperty(vm,key,{
get(){
return vm.$data[key]
},
set(v){
vm.$data[key] = v
}
})
} )
}
class Vue {
constructor(options){
// 保存选项 跟着文档学的
// Vue的文档说用$options能获取到Vue实例初始化选项,因此就这样先保存一下
this.$options = options;
// vm.$data,Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。
this.$data = options.data;
// 响应化处理
objserve( this.$data )
// 访问data,不是用this.$data.xxx 而是直接用的this.xxx 因此这里我们需要对$data上的属性做一次代理
// 使得它成为vue实例上面的一个属性
proxy(this)
new Compile(this.$options.el,this)
}
}
- vue响应化函数 因为vue的$data是响应式的,所以需要一个响应式函数来解决该问题,同时结合了第一大点讲的对象响应式原理,于是有
const getDataType = function (val = 0) {
let type = typeof val
// object需要使用Object.prototype.toString.call判断
if (type === 'object') {
let typeStr = Object.prototype.toString.call(val)
// 解析[object String]
typeStr = typeStr.split(' ')[1]
type = typeStr.substring(0, typeStr.length - 1)
}
return type.toLowerCase()
}
// 对象的响应式原理
function defineReactive( obj,key,value){ // 看到这个入参模式,有没有想起Vue.set/ vm.$set();
objserve(value);
Object.defineProperty(obj,key,{
get(){
console.log(value,'get')
return value
},
set(newValue){
if( newValue !== value){
// 触发视图更新
console.log('触发视图更新','set')
objserve(newValue);
value = newValue
}
}
})
}
// 对象的响应式处理
function objserve(obj){
if(getDataType(obj) !=='object' ){
return
}
new Observer(obj);
}
// 观测数据响应变化,每一个响应式对象,就伴生一个Observer实例
class Observer{
constructor(value){
this.value = value;
this.walk(value);
}
walk(obj){
Object.keys( obj ).forEach( key => defineReactive(obj, key, obj[key]))
}
}
2.3 vue编译的基本实现
在vue中编译是很重要的一步,一个简单的编译思想设计如图:
这样我们会有如下的代码:
class Compile {
constructor(el,vm){
this.$vm = vm;
this.$el = document.querySelector(el);
if( this.$el ){
this.compile(this.$el)
}
}
compile(el){
// 递归遍历el,判断其类型
const childNodes = el.childNodes;
Array.from(childNodes).forEach( node => {
if( this.isElement(node)){
console.log('编译元素',node.nodeName)
this.compileElement(node)
}else if (this.isInter(node)){
console.log('编译插值表达式',node.textContent);
this.compileText(node);
}
if(node.childNodes && node.childNodes.length > 0){
this.compile(node)
}
})
}
isElement(node){
return node.nodeType === 1
}
isInter(node){
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
compileText(node){
// node.textContent = this.$vm[RegExp.$1];
this.update(node,RegExp.$1,'text')
}
// 编译元素
compileElement(node){
let nodeAttrs = node.attributes;
Array.from( nodeAttrs ).forEach( attr=>{
let attrName = attr.name;
let exp = attr.value;
console.log(attrName ,exp,'读取属性')
if(attrName === 'v-html'){
const temp = attrName.split('-')[1];
if(this[temp]){
this[temp](node,exp)
}else{
throw new Error('没有该指令')
}
}
// 绑定方法
if(attrName === '@click'){
node.addEventListener('click',this.$vm.methods[exp])
}
})
}
// html方法
html(node,exp){
// console.log(this.$vm[exp],exp,'xxasdfas')
node.innerHTML = this.$vm[exp];
}
// 所有动态绑定(如指令,插值语法,这里为了简单只对插值语法做更新)
// 都需要创建更新函数以及对应的watcher实例
update(node,exp,dir){
const fn = this[dir+'Updater'];
// 初始化
fn && fn(node,this.$vm[exp]);
}
textUpdater(node,value){
node.textContent = value;
}
}
2.4 vue的监听
监听是需要我们给对于的key一个监听函数,来监听其改变,于是有
class Watcher {
constructor(vm,key,updateFn){
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
}
update(){
this.updateFn.call(this.vm,this.vm[this.key])
}
}
又因为这个监听,是要监听data数据的改变,更新视图 因此他初始化被添加的时机是在编译函数的更新视图的模块中,为此在Compile类中的update方法去进行监听,对更新函数进行收集
class Compile{
...
update(node,exp,dir){
new Watcher(this.$vm,exp,function(val){
fn && fn(node,val)
})
}
...
}
2.5 声明dep,集中管理
为什么要这个东西呢?例如vue的响应式数据this.content,它可能被用于{{content}},也可能被用于v-if="content",很显然这两种更新策略是不一样的,但是他们的依赖源又是相同的,所以需要dep进行管理,当this.content发生改变,会首先找到对应的Dep,通知所有的Watcher执行更新函数
class Dep{
constructor(){
this.deps = []
}
addDep(dep){
this.deps.push(dep)
}
notify(){
this.deps.forEach( dep =>{
dep.update()
})
}
}
正因为当this.content发生改变,会首先找到对应的Dep,通知所有的Watcher执行更新函数
因此他被实例化的时机是在对对象进行响应式处理的时候
因此需要在defineReactive函数里面进行实例化,改写后如下:
function defineReactive( obj,key,value){
objserve(value);
const dep = new Dep(); // 给每一个对象属性一个收集
Object.defineProperty(obj,key,{
get(){
console.log(value,'get')
Dep.target && dep.addDep(Dep.target)
return value
},
set(newValue){
if( newValue !== value){
// 触发视图更新
console.log('触发视图更新','set')
dep.notify();
value = newValue
}
}
})
}
因为要通知到Watcher,所以addDep的时候要正确的吧当前的Watcher实例放进去,于是乎Watcher类就变成了这样
class Watcher {
constructor(vm,key,updateFn){
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update(){
this.updateFn.call(this.vm,this.vm[this.key])
}
}
为什么要Dep.target = this;this.vm[this.key];Dep.target = null;,其实里就是巧妙的应用到了对象响应式get属性方法;能够在Dep实例化的时候正确的对相应的Watcher进行收集管理
3. 简单vue实现代码
<div id="app">
<div>{{counter}}</div>
<div v-html="counter" @click="toAction"></div>
</div>
const getDataType = function (val = 0) {
let type = typeof val
// object需要使用Object.prototype.toString.call判断
if (type === 'object') {
let typeStr = Object.prototype.toString.call(val)
// 解析[object String]
typeStr = typeStr.split(' ')[1]
type = typeStr.substring(0, typeStr.length - 1)
}
return type.toLowerCase()
}
// 对象的响应式原理
function defineReactive( obj,key,value){ // 看到这个入参模式,有没有想起Vue.set/ vm.$set();
objserve(value);
const dep = new Dep(); // 给每一个对象属性一个收集
Object.defineProperty(obj,key,{
get(){
console.log(value,'get')
Dep.target && dep.addDep(Dep.target)
return value
},
set(newValue){
if( newValue !== value){
// 触发视图更新
console.log('触发视图更新','set')
dep.notify();
value = newValue
}
}
})
}
// 对象的响应式处理
function objserve(obj){
if(getDataType(obj) !=='object' ){
return
}
new Observer(obj);
}
const proxy = (vm)=>{
Object.keys( vm.$data ).forEach( key=>{
Object.defineProperty(vm,key,{
get(){
return vm.$data[key]
},
set(v){
vm.$data[key] = v
}
})
} )
}
class Vue {
constructor(options){
// 保存选项 跟着文档学的
// Vue的文档说用$options能获取到Vue实例初始化选项,因此就这样先保存一下
this.$options = options;
// vm.$data,Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。
this.$data = options.data;
this.methods = options.methods;
// 响应化处理
objserve( this.$data )
// 访问data,不是用this.$data.xxx 而是直接用的this.xxx 因此这里我们需要对$data上的属性做一次代理
// 使得它成为vue实例上面的一个属性
proxy(this)
new Compile(this.$options.el,this)
}
}
// 观测数据响应变化,每一个响应式对象,就伴生一个Observer实例
class Observer{
constructor(value){
this.value = value;
this.walk(value);
}
walk(obj){
Object.keys( obj ).forEach( key => defineReactive(obj, key, obj[key]))
}
}
// Vue的模板编译
class Compile {
constructor(el,vm){
this.$vm = vm;
this.$el = document.querySelector(el);
if( this.$el ){
this.compile(this.$el)
}
}
compile(el){
// 递归遍历el,判断其类型
const childNodes = el.childNodes;
Array.from(childNodes).forEach( node => {
if( this.isElement(node)){
console.log('编译元素',node.nodeName)
this.compileElement(node)
}else if (this.isInter(node)){
console.log('编译插值表达式',node.textContent);
this.compileText(node);
}
if(node.childNodes && node.childNodes.length > 0){
this.compile(node)
}
})
}
isElement(node){
return node.nodeType === 1
}
isInter(node){
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
compileText(node){
// node.textContent = this.$vm[RegExp.$1];
this.update(node,RegExp.$1,'text')
}
// 编译元素
compileElement(node){
let nodeAttrs = node.attributes;
Array.from( nodeAttrs ).forEach( attr=>{
let attrName = attr.name;
let exp = attr.value;
console.log(attrName ,exp,'读取属性')
if(attrName === 'v-html'){
const temp = attrName.split('-')[1];
if(this[temp]){
this[temp](node,exp)
}else{
throw new Error('没有该指令')
}
}
if(attrName === '@click'){
node.addEventListener('click',this.$vm.methods[exp])
}
})
}
// html方法
html(node,exp){
// console.log(this.$vm[exp],exp,'xxasdfas')
node.innerHTML = this.$vm[exp];
}
// 所有动态绑定(如指令,插值语法,这里为了简单只对插值语法做更新)
// 都需要创建更新函数以及对应的watcher实例
update(node,exp,dir){
const fn = this[dir+'Updater'];
// 初始化
fn && fn(node,this.$vm[exp]);
// 更新函数被收集
new Watcher(this.$vm,exp,function(val){
fn && fn(node,val)
})
}
textUpdater(node,value){
node.textContent = value;
}
}
// 做依赖收集
class Watcher {
constructor(vm,key,updateFn){
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update(){
this.updateFn.call(this.vm,this.vm[this.key])
}
}
// 声明dep
class Dep{
constructor(){
this.deps = []
}
addDep(dep){
this.deps.push(dep)
}
notify(){
this.deps.forEach( dep =>{
dep.update()
})
}
}
// 使用
const app = new Vue({
el:'#app',
data:{
counter:1
},
methods: {
toAction:()=>{
alert('触发事件')
}
},
})
setInterval(()=>{
app.counter ++
},1000)