vue2 依赖于 object.defineProperty 监听对象变化更新视图
1. object.defineProperty(target,key,{}) 方法在原对象上修改或定义一个属性
第一个参数原目标对象
第二个参数原属性 或 新属性
第三个参数 属性描述 或 属性存取
2.知道方法后
2.1 创建一个文件 我这里用普通的html 文件为例 创建 vue2.html 文件
2.2 标签中定义一个方法 updateView 用来模拟更新视图 ,如下我们需求是当data对象的值发生改变时触发 updateView 更新视图
2.3 思考:我们需要改变对象属性的时候触发 更新视图 方法 先拆解问题
2.3.1 先判断监听的属性必须是对象属性,并且给对象中的属性添加监听
// 定义一个function 作用就是判断当前是否为对象 如果是对象则添加属性监听
// 如果不是对象则返回原数据
function observer(target){
if(typeof target !== 'object' || target === null ){
retrun target
}
for( let key in target ){
defineReactive(target,key,target[key])
}
}
// 定义一个function 作用只对对象添加属性监听方法
function defineReactive(target,key,value){
object.defineProperty(target,key,{
get(){
retrun value
},
set(newValue){
updateView() // 当新的值储存时更新视图
value = newValue
}
})
}
2.3.2 如果赋的值是个对象 获取初始化的值是个对象则
let data = {name:'zs',age:{n:18}}
data.age = {num:20}添加对象监听方法 再执行一次 observer(value)
function defineReactive(target,key,value){
+++ observer(value)
object.defineProperty(target,key,{
get(){
retrun value
},
set(newValue){
+++ observer(newValue)
updateView()
value = newValue
}
})
}
其实本质还是个递归
重点:由此可见vue2响应式一个缺陷
1.未定义的变量无法进行响应式的绑定
2.如果对象层级过深每次递归都往对象深层添加监听很消耗性能
2.3.3 监听时进行数组操作
let data = {name:'zs',age:[1,2,3]}
data.push(4)
数组操作的时候我们也需要更新页面
方法:在数组原型链上重写原型链方法添加重新刷新方法
Array.prototype 获取数组原型链方法
如果直接循环Array.prototype 添加方法会使数组的所有方法都将更新渲染页面,我们只需要特定方法执行 如 :push unshift shift 等
let oldArrayProtoType = Array.prototype // 获取原数组原型链
let proto = Object.create(oldArrayPrototype); //创建一个新的对象继承原数组原型链方法
['push','unshift','shift'].forEach(method=>{ // 将新的原型链中添加执行更新视图的方法并将当前this指针及参数传递
protp[method] = function(){
updateView()
oldArrayProtoType[method].call(this,arguments)
}
})
2.3.4 类型判断方法 observer 中添加数组判断并将新的原型链替换老原型链
function observer(target){
if(typeof target !== 'object' && target === null ){
retrun target
}
+++ if(Array.isArray(target)){
+++ Object.setPrototypeOf(targer,proto)
+++ }
for( let key in target ){
defineReactive(target,key,target[key])
}
}
data.push(4) 就可以触发更新视图方法了
vue2响应式的缺陷已经知道了那么vue3新解决方案 new Proxy()
vue3 中如果需要将属性变更为响应式属性需要使用到 reactive() 数据更新触发effect() 再次执行
{{state.title}}
import {reactive,effect,createApp} from 'Vue'
const app = createApp({
setup(){
let proxy = reactive({name:'zs'})
effect(()=>{
console.log('通知视图更新',proxy.title)
})
proxy.name = 'ls'
retrun {
state:proxy
}
}
})
app.mount('#app')
// effect 会执行两次 第一次是在页面初次渲染执行 第二次是数据发生变化后执行
1. 首先创建一个reactive 方法需要传入一个普通对象获得一个响应式对象
1.1 reactive 返回 createReactiveObject方法
// 这个方法需要返回的是个响应式对象
function reactive(target){
retrun createReactiveObject(target)
}
// 创建响应式对象方法
createReactiveObject(target){
}
1.2
创建响应式方法思路
1.2.1
首先方法参数必须是一个对象 创建一个函数 isObject 专门用来判断值是否为对象
function isObject(val){
retrun typeof val === 'object' && val !== null
}
// 如果是不是对象则直接返回
// 如果是对象新增proxy事件监听
createReactiveObject(target){
if(!isObject(target)){
return target
}
let baseHandle = {
// target 原对象 key当前的变更或获取的key receiver 代理后的proxy对象
get(target,key,receiver){
let res = Reflect.get(target,key,receiver)
console.log('获取')
// 返回值如果是个对象那么再次执行 reactive(res)
return isObject(res)?reactive(res):res
//return res
},
set(target,key,value,receiver){
let res = Reflect.set(target,key,value,receiver)
console.log('写入')
return res
},
deleteProperty(target,key,receiver){
let res = Reflect.deleteProperty(target,key)
console.log('删除')
return res
}
}
let observer = new Proxy(target,baseHandle)
return observer
}
/*
Reflect es6 方法对象的操作方法不修改原对象,返回处理过后的新对象
Reflect.set 方法返回的是Boolean值
*/
运行结果
proxy.name = 'ls' proxy.name = [1,2,3] proxy.name.push(4)
console.log(proxy.name) // 写入方法执行 获取方法执行 可以成功打印修改后的值
无论字符还是数组方法都可以触发!
当数组执行
proxy.name = [1,2,3] proxy.name.push(4)
// 会发现会触发两次写入方法
// 分析原因 首次写入的时候是给数组添加了一个新成员4 第二次写入是重写了数组的length
// 解决方法 判断对象中是否有这个属性,有就是修改 没有就是添加 并且添加的值如果和原值一致那就不修改 修改无意义
// 判断当前对象是是否包含了此属性
function hasOwn(target,key){
return target.hasOwnProperty(key)
}
set(target,key,value,receiver){
let res = Reflect.set(target,key,value,receiver)
let oldValue = target[key]
let hadkey = hasOwn(target,key)
if(!hadkey){
console.log('写入')
}else if(oldValue !== value){
console.log('修改')
}
retrun res
},
1.2.2
为防止已经代理过的方法再次代理 createReactiveObject 方法中增加判断限制
let toProxy = new WeakMap();
let toRaw = new WeakMap();
function createReactiveObject(target){
// 防止多次代理找到后直接返回
let proxy = toProxy.get(target);
if(proxy){
return proxy
}
if(toRaw.has(target)){
return target;
}
...
// 在retrun之前储存代理过后的值
let observed = new Proxy(target,baseHandle);
toProxy.set(target,observed);
toRaw.set(observed,target);
return observed
}
// 防止如下情况产生
情况1
let proxy = reactive({name:1})
reactive(proxy)
情况2
let proxy = reactive({name:1})
reactive({name:1})
1.3
effect 驱动页面进行更新
effect 首次加载执行一次 如果数据更新再执行一次
// effect 执行方法
let obj = reactive({name:'zs'})
effect(()=>{
console.log('驱动执行',obj.name)
})
obj.name = 'ls'
1.3.1 effect参数是个回调函数 并默认执行一次那么先创建effect方法
// effect方法中 创建动态驱动方法并默认执行一次
function effect(fn){
let effect = createReactiveEffect(fn)
effect()
}
// 创建驱动方法
function createReactiveEffect(fn){
let effect = function(){
run(fn)
}
retrun effect
}
// 方法执行
funtion run(fn){
fn()
}
1.3.2
栈储存effect执行的方法
重点: effect更新时需要重新触发之前执行过的方法
如果effect多次调用每个储存的方法都要执行一次
思路: 每个方法用数组储存起来,当数据更新时从数组中取出并执行,遵循先进后出的原 则
// 用来存储effect中的方法 栈
let activeEffectStacks = [];
1.3.3
储存effect执行的方法
// 创建驱动方法
function createReactiveEffect(fn){
let effect = function(){
+++ run(effect,fn) // 将effect执行的方法传入到run方法中 并在执行时储存
}
retrun effect
}
// 方法执行
funtion run(effect,fn){
+++ activeEffectStacks.push(effect); // 将effect中的参数方法储存
fn()
}
1.3.4
由于数据变化时 effect 执行的一定是 第一次执行时初始化 需要获取的数据,所以get方法一定会执行
所以在get 方法中 收集依赖 也就是将栈中的方法关联在对应的监听参数中
get(target,key,receiver){
let res = Reflect.get(target,key,receiver)
console.log('获取')
// 收集依赖把当前的key和effect对应起来
+++ track(target,key); // 如果目标上的key变化了重新让数组中的effect执行即可
return isObject(res)?reactive(res):result // 设置递归
},
let targetsMap = new WeakMap(); // 集合和hash表
// 当前的key和effect对应起来
// targetsMap 中储存格式 key 为当前的原对象 value 为一个新的Map
// Map 中 key为当前 get中指定的key value为 一个新的Set
// 利用Set 只能唯一 判断如果Set中没有当前effect栈的方法就添加进去
// 格式为
//'{name:'zs'}':{
// name:[()=>{console.log('驱动执行',obj.name)}]
// }
function track(target,key){
let effect = activeEffectStacks[activeEffectStacks.length-1]; //栈取最后一个
if(effect){
let depsMap = targetsMap.get(target)
if(!depsMap){
targetsMap.set(target,depsMap = new Map)
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,deps = new Set())
}
if(!deps.has(effect)){
deps.add(effect);
}
}
}
set 方法中当值发生改变时驱动储存的方法执行
// set 中
if(!hadKey){
+++ trigger(target,'add',key);
console.log('新增属性')
}else if(oldValue !== value){
+++ trigger(target,'set',key);
console.log('修改属性')
}
// trigger 方法中查找
function trigger(target,type,key){
// 根据原对象找到对应的map
// 根据set当前的key取出Set() 中的方法 并依次执行
let depsMap = targetsMap.get(target);
if(depsMap){
let deps = depsMap.get(key);
if(deps){ // 将当前可以对应的effect方法依次执行
deps.forEach(effect => {
effect();
});
}
}
}
1.3.5
run方法中储存方法 由于默认执行一次之后 栈中储存的方法也已经无用,方法已经绑定在了监听对象上,于是将run中储存的方法删除
// 防止方法储存执行出错新增了try finally 清除方法必定会执行
function run (effect,fn){
try{
activeEffectStacks.push(effect);
fn();
}finally{
activeEffectStacks.pop();
}
}
最后运行 值修改过后effect将会再次执行
let obj = reactive({name:'zs'});
effect(()=>{
console.log('执行次数',obj.name);
})
obj.name = 'ls'