vue foreach用法_vue 随记(4):响应式的进化

9c4ac3bb577bf7418831006ffc8e32e8.png

响应式的进化

本项目涉及代码:https://github.com/dangjingtao/vue3_reactivity_simple

推荐阅读:observer-util:

https://github.com/nx-js/observer-util

1. defineProperty的缺点和vue 2的hack 方案

1.1 新属性设置不上

vue 2 的响应式已经很强大了。但是对于对象上新增的属性则有些吃力:

let vm = new Vue({  data() {    a: 1  },  watch: {    b() {      console.log('change !!')    }  }})// 没反应!

正常来说,被监听的数据在初始化时就已经被全部监听了。后续并不会再次这种时候,不得不通过vm.$set(全局 Vue.set 的别名。)来处理新增的属性。

语法:this.$set( target, key, value )

target:要更改的数据源(可以是对象或者数组);key:要设置的数据名;value :赋值。

文档地址: https://cn.vuejs.org/v2/api/#Vue-set

1.2 数组监听不上

此外对于数组也无法监听原地改动:

let obj = {}Object.defineProperty(obj, 'a', {  configurable: true,  enumerable: true,  get: () => {    console.log('get value by defineProperty')    return val  },  set: (newVal) => {    console.log('set value by defineProperty')    val = newVal  }})obj.a = [] // set value by definePropertyobj.a.push('1') // get value by definePropertyobj.a[0] = 1 // get value by definePropertyobj.a.pop(1) // get value by definePropertyobj.a = [1, 2, 3] // set value by defineProperty

上述案例中,使用push、pop、直接通过索引为数组添加元素时会触发属性a的getter,是因为与这些操作的返回值有关,以push方法为例,使用push方法为数组添加元素时,push方法返回值是添加之后数组的新长度,当访问数组新长度时就会自然而然触发属性a的getter。

vue2 的做法是干脆把数组的原型方法都劫持了,从而达到监听数组的目的:

var arrayProto = Array.prototypevar arrayMethods = Object.create(arrayProto);[  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse'].forEach(function(item){    Object.defineProperty(arrayMethods,item,{        value:function mutator(){            //缓存原生方法,之后调用            console.log('array被访问');            var original = arrayProto[item]                var args = Array.from(arguments)        original.apply(this,args)            // console.log(this);        },    })

2. Proxy——新时代的响应式

Proxy来说,Object.defineProperty 是ie 8 支持的方法。对比几年前,兼容性的矛盾似乎不再那么突出(vue 3 最低支持ie 11)。所以在新一代的vue演进中,响应式机制的改革被提到了一个非常重要的位置。

在前面的文章中,我们了解过defineProperty和Proxy的用法。

我们写defineProperty的时候,总是会用一层对象循环来遍历对象的属性,一个个调整其中变化:

Object.keys(data).forEach(key => {  Object.defineProperty(data, key, {    get() {      return data[key];    },    set(nick) {      // 监听点      data[key] = nick;    }  })})

而Proxy监听一个对象是这样的:

new Proxy(data, {  get(key) { },  set(key, value) { },});

可以看到Proxy的语法非常简洁,根本不需要关心具体的 key,它去拦截的是 「修改 data 上的任意 key」 和 「读取 data 上的任意 key」。所以,不管是已有的 key 还是新增的 key,都逃不过它的魔爪。

Proxy 更加强大的地方还在于 Proxy 除了 get 和 set,还可以拦截更多的操作符。Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。

所以说,前端响应式数据的新世代——Proxy,已经到来了。

3. vue3 中的响应式

可在此处克隆最新的仓库代码:https://github.com/vuejs/vue-next.git,下载下来之后运行dev命令打包:

 npm run dev

即可阅读源码。

在vue 3中负责响应式部分的仓库名为 @vue/rectivity,它不涉及 Vue 的其他的任何部分,甚至可以轻松的集成进 React[注]。是非常非常“正交” 的实现方式。

[注] https://juejin.im/post/5e70970af265da576429aada

对于vue3 的effect API,如果你了解过 React 中的 useEffect,相信你会对这个概念秒懂,Vue3 的 effect 不过就是去掉了手动声明依赖的「进化版」的 useEffect。

在vue3 中,实现数据观察是这样的:

// 定义响应式数据const data = reactive({   count: 1});// 观测变化,类似react中的useEffectconst effection = effect(() => console.log('count changed', data.count));data.count = 2; // 'count changed 2'

如果想要监听单条数据,可以用ref:

llet count = ref(0);effect(()=>{  console.log(`count被变更为${count}`)});count.value += 1;

React 中手动声明 [data.count] 这个依赖的步骤被 Vue3 内部直接做掉了,在 effect 函数内部读取到 data.count 的时候,它就已经被收集作为依赖了。少了useState,setData,看起来比react更方便了。

因此实现响应式最重要的api是:

•reactive•effect•ref

接下来我们就尝试简单实现之。

3.1 ref:监听单个变量

3.1.1 简化实现

先来实现ref(对单个数据的监听)。

新建一个Proxy.js:

let activeEffect;class Dep{  constructor(){    this.subs = new Set();  }  depend(){    // 收集依赖    if(activeEffect){      this.subs.add(activeEffect);    }  }  notify(){    // 数据变化,通知effect执行。    this.subs.forEach(effect=>effect())  }}const ref = (initVal)=>{  const dep = new Dep();  let state = {    get value(){      // 收集依赖      dep.depend();      return initVal;    },    set value(newVal){      // 修改,通知dep执行有此依赖的effect      dep.notify();      return newVal;    }  }  return state;}let state = ref(0);const effect = (fn)=>{  activeEffect = fn;  fn();}effect(()=>{  console.log(`state被变更为`,state.value)});state.value = 1;

在上面的代码中,state.value每次被设置,都会打印出变更提示。

3.1.2 源码解读

在源码中找到packages/reactivity/src/refs.ts,可以看到Ref方法是由createRef完成的。

// 源码 `packages/reactivity/src/refs.ts`export function ref(value?: unknown) {  return createRef(value)}// ... // 创建reffunction createRef(rawValue: unknown, shallow = false) {  if (isRef(rawValue)) {    return rawValue  }  let value = shallow ? rawValue : convert(rawValue)  const r = {    __v_isRef: true,    get value() {      track(r, TrackOpTypes.GET, 'value')      return value    },    set value(newVal) {      if (hasChanged(toRaw(newVal), rawValue)) {        rawValue = newVal        value = shallow ? newVal : convert(newVal)        trigger(          r,          TriggerOpTypes.SET,          'value',          __DEV__ ? { newValue: newVal } : void 0        )      }    }  }  return r}

vue3 源码中,拦截各种取值、赋值操作,依托 track 和 trigger 两个函数进行依赖收集和派发更新。分别对应简化实现中的Dep.dependDep.notify

•track 用来在读取时收集依赖。•trigger 用来在更新时触发依赖。

在vue3中,Dep不再是一个class类。而是一个非常大的Map对象。

在Proxy 第二个参数 handler 也就是陷阱操作符[注]中,拦截各种取值、赋值操作,依托 track 和 trigger 两个函数进行依赖收集和派发更新。

•track 用来在读取时收集依赖。•trigger 用来在更新时触发依赖。

[注] 陷阱操作符: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler

3.2 reactive:监听对象

现在我们想模仿vue 3 的Composition API,实现一个这样的功能:

 lang="en">   charset="UTF-8">   name="viewport" content="width=device-width, initial-scale=1.0">  Document  
id="app">
id="btn">click const root = document.querySelector('#app'); const btn = document.querySelector('#btn'); let obj = reactive({name: 'djtao', age: 18 }); let double = computed(() => obj.age * 2); effect(() => {console.log(`数据变更为`, obj); root.innerHTML = `

Name(监听属性):${obj.name}

Age(监听属性):${obj.age}double(计算属性):${double.value}`; }); btn.addEventListener('click', e => {obj.name = 'dangjingtao'; obj.age += 1; })

要求点击按钮更新监听属性及计算属性。

结合tracker和trigger的角色,接下来就实现上面代码的用法。

3.2.1 监听(响应式核心)

我们用一个cerateBaseHandler来处理生成proxy的handler,然后通过track和trigger分辨收集依赖和派发更新。

// 依赖收集const track = () => {}// 依赖派发const trigger = () => {}// 计算属性const computed = () => {}// 副作用const effect = fn =>{  fn();}const createBaseHandler = (taget) => {  return {    get(target,key){      track(target,key);      return target[key]; // 此处源码用Reflect    },    set(target,key,newValue){      const info = {        oldValue:data[key],        newValue      };      data[key] = newValue;      trigger(target,key,info);    }  }}// 对象响应式const reactive = (data) => {  const observer = new Proxy(data,createBaseHandler(data));  return observer;}

那么响应式核心就写好了。

3.2.2 track和trigger

我们把所有的依赖放到一个类似栈的数组结构中。

在做之前,应该设想下“依赖”这个对象(不妨命名为targetMap)的数据结构:首先它可能接收多个reactive的代理对象(命名为target),而每个taget都对应各自的依赖(depMap)——提示使用weakMap数据结构。这个depMap是一个Map对象。可通过属性名拿到该属性名具体要收集的依赖集合dep(这是个Set对象)。当我们拿到effect之后,把它添加到dep中。依赖收集就完成了。

5ec399e71bac4b340de33e74f7df271c.png

不妨试想收集依赖就是一个找自己位置的游戏,首先根据target在大对象中找到该数据的专属依赖。然后根据属性key,再找到这个数据,这个属性的一系列依赖,最后把副作用添加进去。

targetMap: {    { name: 'djtao',age:18 }: depMap   }  depMap:{    'name':dep}dep:Set()

在收集依赖的过程中,一律遵循“找不到就创建”的原则。

// 它用来建立 监听对象 -> 依赖 的映射let targetMap = new WeakMap();const effectStack = [];// 依赖收集const track = (target,key) => {  // reactive可能有多个,一个可能又有多个key  const effect = effectStack[effectStack.length - 1];  if(effect){    // 尝试找到属于该数据的依赖对象(depMap)    let depMap = targetMap.get(target);    // 如果不存在,则把它定义为一个Map,并添加到targetMap中    if(!depMap){      depMap = new Map();      targetMap.set(target,depMap);    }    // 找到这个对象的这个属性,获取对这个对象,这个属性对依赖。    let dep = depMap.get(key);    // 如果不存在,则初始化一个set。    if(!dep){      dep = new Set();      depMap.set(key,dep);    }    // 核心逻辑:现在拿到dep了,把副作用添加进去。    dep.add(effect);    // 此处在deps也绑定上dep    effect.deps.push(dep);  }}

trigger就好理解多了,对应的是简化实现一节代码的Dep.notify(),当数据变化时,拿出依赖,遍历执行。

const trigger = (target, key, info) => {    // 简化来说 就是通过 key 找到所有更新函数 依次执行    const depMap = targetMap.get(target);      const deps = depMap.get(key)    deps.forEach(effect => effect());}

实际上trigger需要进一步细化

3.2.3 computed和effect

接下来就是实现computed和effect副作用。

怎么理解二者的关系?简化来看,computed就是一个特殊的effect。因为它与原对象同时监听同一个或n个属性。需要一个option去配置它当它被标记为computed时,内容为true。

所以在trigger中,还需要更加健壮些:

// 依赖派发const trigger = (target,key,info) => {  // 查找对象依赖  const depMap =targetMap.get(target);  // 如果没找到副作用即可结束  if(!depMap){    return  }  // effects 和 computedRunners 用于遍历执行副作用和computed的依赖  const effects = new Set();  const computedRunners = new Set();  if(key){    let deps = depMap.get(key);    deps.forEach(effect => {      // 计算flag为true时,添加到computedRunners      if(effect().computed){        computedRunners.add(effect);      }else{        effects.add(effect);      }    });    computedRunners.forEach(computed=>computed());    effects.forEach(effect=>effect());  }}

这样trigger就相对完整了。

因为是一个特殊的effect,读取computed的value属性的时候,即可执行计算。

// 计算属性:特殊的effect,多了一个配置const computed = (fn) => {  const runner = effect(fn,{computed:true,lazy:true});  return {    effect:runner,    get value(){      return runner();    }  }}

再看effect:需要一个createReactiveEffect方法处理一下:

// 副作用const effect = (fn,options={}) =>{  let e = createReactiveEffect(fn,options);  // 惰性:首次不执行,后续更新才执行  if(!options.lazy){    e();  }  return e;}// const createReactiveEffect = (fn,options={})=>{  const effect = (...args) =>{    return run(effect,fn,args);  }  // 单纯为了后续清理,以及缓存  effect.deps = [];  effect.computed = options.computed;  effect.lazy = options.lazy;  return effect;}// 辅助方法,执行前入栈,最后执行完之后出栈// 以此保证最上面一个effect是最新的const run =(effect,fn,args)=>{  if(effectStack.indexOf(effect)===-1){    try {      effectStack.push(effect);      return fn(...args);    } finally {      effectStack.pop();    }  }}

run方法维护effectStack,并负责执行具体方法。

好了,需求就已经做完了。看看效果:

点击前:

f0e4df74152520358729deb124a89fc7.png

点击后:

769b56048773a3a979f0878b0a1899a7.png

历次打印结果:

1a265923b9cfaf9bc2484ee7f877b238.png

可以看到,响应式系统中,首先监听到初始值,点击按钮,先监听了name的变化,然后是age的变化。

自此,参照vue3源码的响应式系统完成。

3.3 源码导读

代码在packages/src/reactivity/reactive.ts

export function reactive(target: object) {  // if trying to observe a readonly proxy, return the readonly version.  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {    return target  }  return createReactiveObject(    target,    false,    mutableHandlers,    mutableCollectionHandlers  )}

createReactiveObject的关键部分:

function createReactiveObject(  target: Target,  isReadonly: boolean,  baseHandlers: ProxyHandler,  collectionHandlers: ProxyHandler) {  // ...  const observed = new Proxy(    target,    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers  )  // ...  return observed}

collectionHandlers对应的是Set,Map等数据类型,baseHandler对应普通对象。

baseHandler中也调用了trigger。

017815c5ef5313e9b72c5093a799563a.png

trigger和track的逻辑如下:

export function track(target: object, type: TrackOpTypes, key: unknown) {    // ...  let depsMap = targetMap.get(target)  if (!depsMap) {    targetMap.set(target, (depsMap = new Map()))  }  let dep = depsMap.get(key)  if (!dep) {    depsMap.set(key, (dep = new Set()))  }  if (!dep.has(activeEffect)) {    dep.add(activeEffect)    activeEffect.deps.push(dep)    // ...  }}export function trigger(  target: object,  type: TriggerOpTypes,  key?: unknown,  newValue?: unknown,  oldValue?: unknown,  oldTarget?: Map<unknown, unknown> | Set) {  const depsMap = targetMap.get(target)  if (!depsMap) {    // never been tracked    return  }  const effects = new Set<ReactiveEffect>()  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {    if (effectsToAdd) {      effectsToAdd.forEach(effect => {        if (effect !== activeEffect || !shouldTrack) {          effects.add(effect)        } else {          // the effect mutated its own dependency during its execution.          // this can be caused by operations like foo.value++          // do not trigger or we end in an infinite loop        }      })    }  }  if (type === TriggerOpTypes.CLEAR) {    // collection being cleared    // trigger all effects for target    depsMap.forEach(add)  } else if (key === 'length' && isArray(target)) {    depsMap.forEach((dep, key) => {      if (key === 'length' || key >= (newValue as number)) {        add(dep)      }    })  } else {    // schedule runs for SET | ADD | DELETE    //. ...  }  const run = (effect: ReactiveEffect) => {    if (__DEV__ && effect.options.onTrigger) {      effect.options.onTrigger({        effect,        target,        key,        type,        newValue,        oldValue,        oldTarget      })    }    if (effect.options.scheduler) {      effect.options.scheduler(effect)    } else {      effect()    }  }  effects.forEach(run)}

在computed中,也把它作为一个特殊的effect存在。

4da97d5f9906a4b89fc9497b4b7e1493.png

39f1535df255aa1bacdfb12391321608.png