赞
踩
vue2 依赖于 object.defineProperty 监听对象变化更新视图
1. object.defineProperty(target,key,{}) 方法在原对象上修改或定义一个属性
第一个参数原目标对象
第二个参数原属性 或 新属性
第三个参数 属性描述 或 属性存取
2.知道方法后
2.1 创建一个文件 我这里用普通的html 文件为例 创建 vue2.html 文件
2.2 <script> </script> 标签中定义一个方法 updateView 用来模拟更新视图 ,如下我们需求是当data对象的值发生改变时触发 updateView 更新视图
- <script>
- function updateView(){
- console.log('模拟更新视图')
- }
- let data = {name:'zs'}
- data.name = 'lisi'
- </script>
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() 再次执行
- <div id="app">
- <h5>{{state.title}}</h5>
- </div>
- <script src="https://unpkg.com/vue@next"></script>
- 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'
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。