vue 基于运行时+编译时。
两者共用:编译时将模板转换为表示虚拟dom的对象,可以分析哪些内容未来可能改变,哪些永远不会变等信息,传递给 Render 函数做运行时的优化
纯编译时:框架直接编译成可执行的 js 代码,理论上性能更好且不需要太大心智负担,但少了灵活性,用户的内容必须编译后才能用,编译也需要时间。
纯运行时:需要用户自己编写可执行的 JS 代码,心智负担大,无法分析用户提供的内容
总结:要考虑心智负担、可维护性、性能等综合要素
合适的警告信息:使用 console.warn()
1、区别开发环境和生产环境使用的代码
开发环境使用的代码,可以通过一个全局常量_DEV_
在 if 语句中做判断,开发版本中修改常量再通过摇树去除掉
2、tree shaking
js 是动态语言,有一些代码运行时才能知道是否有副作用(调用的时候会对外部产生影响),所以可以主动指示哪些代码不会造成副作用,使用/*#pure*/
来表示当前代码没有副作用,打包器可以放心摇树
vue.global.js 用于开发环境
vue.global.prod.js 用于生产环境,二者都是 IIFE 的形式
esm有两种,一种是给浏览器用的,另一种是打包工具用的。文件名带 bundler 的是给打包工具使用的 js,如果 package.json 中存在 module 字段,会优先使用这个字段的资源来代替 main 字段的资源。好处是比如上面的_DEV_
常量,esm环境中会换成 process.env.NODE_ENV!=='production'
,用户可以自行配置构建的目标环境
也应该支持 node 环境
上述文件都可以使用 rollup.js 的 output 对象中设置 format 字段来构建出,比如 iife、cjs、esm
好处:
1、可以进行摇树去掉不需要的特性
2、便于为框架添加新的特性,不用担心体积变大。框架升级时也可以选择支持旧的 API
实现方法:利用 rollup.js 的预定义常量插件,_VUE_OPTIONS_API_
就是是否支持 vue2 选项式 API 的开关
{
_FEATURE_OPTIONS_API_: isBundleESMBuild ? '_VUE_OPTIONS_API_' : true
}
代码中 if 判断中的 _FEATURE_OPTIONS_API_
会根据是否是打包工具使用来控制开关的展示
如果是打包工具环境,可以通过自定义插件 webpack.DefinePlugin 实现开关
new webpack.DefinePlugin({
_VUE_OPTIONS_API_: JSON.stringify(true) // 'true'
})
1、在工具内部定义一个外部错误处理函数的注册函数,接受用户传递的错误处理函数
2、内部封装一个统一的执行回调函数的函数,在 try catch 执行用户传入的回调函数, catch 部分执行用户注册的错误处理函数或者为空。
良好的类型支持
vue 的模板可以声明式的描述 UI,但是没有 js 对象描述来的灵活,比如选择 h1~h6 需要穷举,js 只需更改变量值即可表达。vue 的 h 函数就是方便编写虚拟 DOM 而设计的
假设我们虚拟 dom 的定义为:
const vnode = {
tag: 'div',
props: {
onClick: ()=>alert('hello')
},
children: 'click me'
}
实现渲染器 render 分三步:
1、创建元素:通过 tag
2、为元素添加属性和事件:for in 遍历 props 对象,如果以 on 开头(/^on/.text(key))说明是事件,截取掉 on 字符放入 addEventListner 中
3、处理 children:如果是数组,就递归调用我们的 render 函数。是字符串就创建文本节点
精髓是后续的更新,也就是 diff 算法找出变更点
组件是一组 DOM 元素的封装
组件可以是函数返回要渲染的内容,类似上面的 vnode 结构。也可以是一个对象,有一个 render 函数返回要渲染的内容(vue 的有状态组件[不止用于显示的组件]用的这种)
渲染器渲染组件
编译器会把模板编译成对应的渲染函数并且添加到<script>标签块的组件对象上
export default {
data(){}
methods:{handler: ()=>{}},
render(){return h('div',{onClick: handler}, 'click me')}
}
编译器在编译阶段可以根据模板语法,知道哪些数据是可能会发生变化的,从而帮助渲染器更好的跟踪可变量,提示性能。比如在虚拟 dom 对象上增加一个字段表示 class 是可变的
后者:函数的执行会直接或间接的影响其他函数,比如修改全局变量。当我们读取一个变量时,存储这个副作用函数。当其他时候修改这个变量时,取出所有的副作用函数执行一次。
这里进行了封装,我们构造一个副作用函数构造器 effect,传入的参数是我们真正的副作用函数 fn
前者:对象中的值发生改变后,副作用函数会自动执行利用最新的值
weakMap:{
obj: Map
}
Map: {
key: Set
}
weakMap :索引是对象,值是这个对象所有属性保存副作用函数的桶的 map。
Map:键是对象的字段,值是副作用函数桶 set
Set : 存储每个属性对应的副作用函数 ,一般叫做 deps(依赖集合)
假设一个副作用函数中
document.querySelector('#id').innerText = obj.ok ? obj.text : 'not'
当 ok 发生变化时,会收集这个副作用函数进入依赖集合 Set 中,但是也会同时放入 text 的依赖集合里。但是当 ok 值为 false 时,此时不会触发 text 读取,那么已经存在的依赖应该被删除,防止遗留。
这里我们应该考虑一个问题,我们怎么知道这个副作用函数被谁依赖着呢。所以在进行依赖收集时,不仅要把函数放入 Set,同时也要把这个 Set 放入副作用函数的 deps 数组里(effect.deps),这样就有双向联系
在我们每次执行 effect 前,先把这个 effect 从它所有的依赖集合里删除,再重新进行收集它,这样就把遗留的 effect 删掉啦
组件的渲染函数 render()
就是一个 effect ,组件中还可以渲染子组件,此时发生了嵌套
所以一个全局的活动函数变量 activeEffect 无法满足要求,因为外层的会被内层的函数覆盖掉,需要使用 effect 栈
我们在执行 effect 前先把它压入栈,执行完后弹出,同时 activeEffect 设置成栈顶函数
effect(()=>{
obj.foo = obj.foo + 1
})
上述代码由于读取了 foo ,所以会触发 track 操作把副作用函数加入桶中,然后又因为设置了 foo 触发 trigger 操作,从桶里把副作用函数取出执行,导致了无限递归调用自己
解决方法:判断下 trigger 的函数和自己是否相同
if(effectFn !== activeEffect){
//执行依赖集合中的副作用函数
}
可调度性:当 trigger 动作触发副作用函数执行时,可以决定执行的时机、次数、方式
前提:为 effect 构造器扩展一个参数: options 选项,其中有一个调度器函数 scheduler ,接受 effectFn 作为参数,在 effect 执行时把调度器挂载到每个 具体的 effectFn 上,此时不执行 effectFn,而是返回 effectFn,把控制权交给外面用户的变量控制
执行时机:调度器中可以设置 settimeout 推迟执行
次数:可以设置一个刷新队列 jobQueue: Set,一个刷新标志。连续多次 obj.foo++
时,会触发多次把 effectFn 放入队列,但是 Set 自动去重。通过 p = promise.resolve()
构造 promise 实例,然后在 then 中执行我们的 effectFn,由于有刷新标志的存在,副作用函数最终只会执行一次。
调度器何时执行:在触发 trigger 函数时,如果桶中的 effectFn 有调度器属性就触发各自的调度器
这个功能有点类似 vue 的连续多次修改响应式数据但是只执行一次更新,vue 内部实现了一个更加完善的调度器,思路相同。
为什么这里要用微任务呢?个人理解:因为要确保所有的副作用函数执行完后才把刷新状态重置
scheduler(fn){
jobQueue.add(fn)
flushJob() // 执行
}
在每次触发 trigger 时执行 scheduler
// 来自 mini-vue,与书上代码差不多
export function triggerEffects(dep) {
// 执行收集到的所有的 effect 的 run 方法
for (const effect of dep) {
if (effect.scheduler) {
// scheduler 可以让用户自己选择调用的时机
// 这样就可以灵活的控制调用了
// 在 runtime-core 中,就是使用了 scheduler 实现了在 next ticker 中调用的逻辑
effect.scheduler();
} else {
effect.run();
}
}
}
前置:我们需要在 options 参数中增加 lazy 属性,表示 effect 的时候不马上执行 effectFn,而是延迟执行,并且在 effectFn 中返回真正的副作用函数 fn 的结果,再返回 effectFn,这样在外面我们可以手动执行 effectFn 获取真正的副作用函数 fn 的结果
注意,只有传入 effect 的参数 fn 才是真正的副作用函数,effectFn 是我们封装的,在里面会执行 fn() 而已
function computed(getter: Function){
const effectFn = effect(getter,{
lazy: true
})
const obj = { // 访问器
get value(){
return effectFn()
}
}
return obj
}
const sum = computed(()=> obj.foo + obj.bar)
console.log(sum.value)
跟目前的 computed 很像,传入一个副作用函数,然后访问 value 的时候触发 get 执行 effectFn 获取最新的值。
缺点:没有缓存,每次读取值都会重新计算
我们使用 dirty 来判断是否已经计算过,并把值保存在 value 变量中,这样当 dirty 为 true 时我们直接返回结果就好。
但是这样有一个问题,我们修改了 obj.foo 的值后,sum 并没有变化,所以我们需要使用上面写的调度器选项,我们在调度器中把 dirty 重置,这样如果修改了依赖的变量 obj.foo,就会重置 dirty,然后再次读取的时候就会重新计算
读取计算属性的值时,手动调用 track 函数进行跟踪(此时 activeEffect 是外层的,创建了一个 WeakMap,key 是 computed 函数返回的对象 obj,值是一个 Map。Map 的 key 是 obj.value,值是 Set,里面有外层的 effect)。
当获取 value 触发执行计算函数的 effectFn 时,调度器里手动执行 trigger 函数触发响应,把外层的 effect 从 Set 拿出来也执行
在调度器中调用我们传入的 callback 函数即可
function watch(source: Object,cb){
effect(
//递归的进行读取,从而触发 get 把副作用函数和数据建立响应式关系
()=> traverse(source),
{
scheduler(){
cb()
}
}
)
}
// 对对象的所有属性进行监听
function traverse(value,seen=new Set()){
// 如果读取的字段是原始值,或者已读,直接返回
if(typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
// 递归所有字段
for(const k in value){
traverse(value[k],seen)
}
return value
}
我们不仅可以传入一个响应式数据作为监听的源,还可以传入一个 getter 函数(我现在项目里一般用的就是这种)
如果检测到参数是函数,那直接使用用户给的这个函数作为 getter 去读取数据产生联系
function watch(source: Object,cb){
let getter
if(typeof source === 'function'){
getter = source
}else {
getter = ()=> traverse(source)
}
effect(
//递归的进行读取,从而触发 get 把副作用函数和数据建立响应式关系
()=> getter(),
{
scheduler(){
cb()
}
}
)
}
需要利用 lazy(不马上执行副作用函数) 和 调度器(控制新旧值的获取)
function watch(source: Object,cb){
let getter
if(typeof source === 'function'){
getter = source
}else {
getter = ()=> traverse(source)
}
let oldValue,newValue
const effectFn = effect(
//递归的进行读取,从而触发 get 把副作用函数和数据建立响应式关系
()=> getter(),
{
lazy: true, // 新增
scheduler(){
newValue = effectFn() // 新增
cb(newValue,oldValue)
oldValue = newValue // 用新值更新旧值
}
}
)
// 手动调用副作用函数,因为是马上第一次执行的,获取的是旧值
// 当后面值发生变化时调度器里面执行的时候就是拿的新值
oldValue = effectFn()
}
watch 的本质其实是对 effect 的二次封装,想要立即执行,我们就把调度器封装成 Job 函数,在 options.immediate 为真时直接执行一次。
function watch(source: Object,cb){
let getter
if(typeof source === 'function'){
getter = source
}else {
getter = ()=> traverse(source)
}
let oldValue,newValue
const job = ()=>{
newValue = effectFn()
cb(newValue,oldValue)
oldValue = newValue
}
const effectFn = effect(
()=> getter(),
{
lazy: true,
scheduler: job // 封装
}
)
if(options.immediate){
job() // 立即执行调度器
}else {
oldValue = effectFn()
}
}
flush 功能也可以通过在调度器中进行判断从而使用 promise.then() 来指定执行时机
竞态问题:无法保证异步操作的完成会按照他们开始时同样的顺序(常见于请求)
function watch(source: Object,cb){
let getter
if(typeof source === 'function'){
getter = source
}else {
getter = ()=> traverse(source)
}
let oldValue,newValue
let cleanup // 用来保存用户注册的过期回调
function onInvalidate(fn){
cleanup = fn
}
const job = ()=>{
newValue = effectFn()
if(cleanup){
cb(newValue,oldValue,onInvalidate) // 将onInvalidate作为回调的第三个参数,让用户传递函数fn进来
}
oldValue = newValue
}
const effectFn = effect(
()=> getter(),
{
lazy: true,
scheduler: ()=>{
if(options.flush === 'post'){
const p = Promise.resolve()
p.then(job)
}else {
job()
}
}
}
)
if(options.immediate){
job()
}else {
oldValue = effectFn()
}
}
watch(obj, async(newValue, oldValue, onInvalidate) => {
let expired = false // 默认为否
onInvalidate(()=>{
expired = true
})
const res = await fetch('/request')
if(!expired){ // 通过闭包,如果这个值被改变为 true,说明已经过期,有新的请求了,数据就不要了
finalData = res
}
})
在每次执行回调函数前,判断是否有之前请求的过期回调,有过期回调就调用把上一次的闭包 expired 置为 true,这样就不会保存过期数据
Proxy 可以创建一个代理对象,能够实现对其他对象的代理,不能代理非对象
代理:拦截并重新定义一个对象的基本操作
基本操作:读取、设置等
复合操作(非基本操作):上述基本操作组合起来,比如函数调用(读取get、调用apply)
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,非构造函数不能被 new
const obj = {foo:1}
const p = new Proxy(obj, {
get(target,key){
track(target,key)
return target[key] // 1
},
set(target,key,newVal){
target[key] = newVal // 2
trigger(target,key)
}
})
上述代码是我们上节使用的追踪和触发代码,使用了最常见的对象读取设置方式
这样的代码有个问题,在下面的场景时,this 指向会错误
const obj = {
foo:1,
get bar(){
return this.foo
}
}
effect(()=>{
console.log(p.bar)
})
p.foo ++
此时副作用函数不会执行,因为 this 指向的是原始对象 obj,而不是使用了代理的 p。
改成下面的方案:
const p = new Proxy(obj, {
get(target,key){
track(target,key)
return Reflect(target,key,receiver) // 1
}
// 省略
})
receiver 表示的是我们调用这个 get 函数时候的 this。也就是在 effect 里触发追踪时,可以传入正确的 this: p,这样就可以正常触发 track 建立联系了。
一共有两种对象:常规对象、异质对象
内部方法:当我们对一个对象操作时,引擎内部使用的方法,规范中用 [[fn]] 来表示。一个对象有11种必须的内部方法,还有两个额外的必要内部方法,[[Call]] 函数调用触发,[[Construct]] 构造函数调用触发
JS 中对象的实际语义是由对象的内部方法指定的,如何判断是函数还是对象?函数有独有的 [[Call]] 内部方法
内部方法有多态性:比如普通对象和 Proxy 对象都有 [[Get]] 方法,但是内部逻辑是不同的
什么是异质对象:
对于11种内部方法:不符合ECMA规范10.1.x给出的定义实现的
对于额外的两种内部方法:不符合ECMA规范10.2.1和10.2.2给出的定义实现的
举例 Proxy 的 [[Get]] 不符合定义,所以是异质对象
每一个内部方法都有一个处理器函数一一对应,比如 get 和 [[Get]]
代理透明性质:如果我们没有指定处理器函数,那么就会使用普通对象的内部方法
注意:在我们的处理器函数中,要使用 Reflect.fn 才能操作代理对象,否则是操作原始对象
- 访问属性:obj.foo
- 判断对象或原型上是否有 key:key in obj
- for in 循环遍历对象
1)对于第一点很简单,使用 Proxy 的 get 函数即可
2)对于 in 操作符
查阅规范可知运算结果是通过 HasProperty 的抽象方法得到的 ,而抽象方法又是通过对象的内部方法 [[HasProperty]] 得到的,而 Reflect 中就有一个 has 方法提供了这个内部方法的拦截能力
const obj = {foo:1}
const p = new Proxy(obj,{
has(target,key){
track(target,key)
return Reflect.has(target,key)
}
})
3)对于 for in 循环
查阅规范可知,使用 Reflect.ownKeys(obj) 来获取只属于对象自身的键
const obj = {foo:1}
const ITERATE_KEY = Symbol()
const p = new Proxy(obj,{
ownKeys(target){ // 不相 get/set 能拿到 key
track(target,ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
但是这个方法不能跟踪到具体的属性,所以我们构造了独一的键,在触发的时候我们也需要拿出对应的副作用函数
触发 trigger 函数
const p = new Proxy(obj,{
set(target,key,newVal){
// 设置属性值
const res = Reflect.set(target,key,newVal,receiver)
trigger(target,key)
return res
}
// 其他拦截函数省略
})
function trigger(target,key){
const depsMap= bucket.get(target)
if(!depsMap) return
// 普通key的副作用函数
const effects = depsMap.get(key)
// 获得构造的key有联系的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 普通key相关的副作用函数添加到执行队列
effects && effects.forEach(effectFn =>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
// 与构造的唯一key相关的副作用函数也添加到执行队列
iterateEffects && iterateEffects.forEach(effectFn =>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
// 执行副作用函数,有调度器就传进去让用户控制执行
effectsToRun.forEach(effectFn =>{
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else {
effectFn()
}
})
}
上述方案也有问题,就是只能跟踪新增的属性,但是如果修改之前已经有的
比如 p.foo=2
不会对 for..in 产生影响,这种情况是不需要触发的,所以我们要在 set 时能够区分类型
// 代理函数
const p = new Proxy(obj,{
set(target,key,newVal){
// 如果属性不存在则是 add
const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD'
const res = Reflect.set(target,key,newVal,receiver)
trigger(target,key,type) // 类型传给trigger
return res
}
// 其他拦截函数省略
})
// trigger函数
function trigger(target,key){
//...
if(type === 'ADD'){
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effectFn =>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
}
// ...
}
除了新增、修改,当然还有删除会影响到 for...in。通过阅读规范可知拦截 deleteProperty
// 代理函数
const p = new Proxy(obj,{
deleteProperty(target,key){
const hadKey= Object.prototype.hasOwnProperty.call(target,key)
const res = Reflect.deleteProperty(target,key)
// 只有当被删除属性是自己的,且成功删除时才触发更新
if(res && hadKey){
trigger(target,key,'DELETE')
}
return res
}
// 其他拦截函数省略
})
// trigger函数
function trigger(target,key){
//...
if(type === 'ADD' || type === 'DELETE){ // 新增'DELETE'类型
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effectFn =>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
}
// ...
}
// 代理函数
const p = new Proxy(obj,{
set(target,key,newVal,receiver){
// 先获取旧值
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD't
const res = Reflect.set(target,key,newVal,receiver)
// 比较新值与旧值,只有不全等,并且不是 NaN 的时候才触发响应
if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal))
trigger(target,key,type)
return res
}
// 其他拦截函数省略
})
举个栗子:
function reactive(obj){
return new Proxy(obj,{
// 省略拦截函数
})
}
const obj = {}
const proto = {bar:1}
const child = reactive(obj)
const parent = reactive(proto)
Object.setPrototypeOf(child,parent)
effect(()=>{
console.log(child.bar); // 值是1
})
child.bar = 2 //2 但是会触发两次副作用函数
由于 child 没有 bar 这个属性,所以会到原型上找,于是触发 parent 的 get 拦截,建立响应式联系。然后在我们给 child.bar 赋值时,如果 child 没有,会调用原型上 parent 的 [[Set]] 方法。而前面说了副作用函数会被 child.bar 收集,也会被 parent 收集,所以触发两次。
解决方案:利用参数中 target 会变化,但是 receiver 永远是当前代理对象的特点,当他们统一的时候,说明没有利用原型链
// reactive函数
function reactive(obj){
return new Proxy(obj,{
get(target,key,receiver){
// 代理对象可以通过 raw 属性获取原始数据
if(key === 'raw'){
return target
}
track(target,key)
return Reflect.get(target,key,receiver)
}
// 省略拦截函数
})
}
// 代理对象
const p = new Proxy(obj,{
set(target,key,newVal,receiver){
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD't
const res = Reflect.set(target,key,newVal,receiver)
// target === receiver.raw 说明 receiver 就是target 的代理对象
if(target === receiver.raw){
if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal)){
trigger(target,key,type)
}
}
return res
}
// 其他拦截函数省略
})
这节介绍 reactive 和 shallowReactive 的区别
我们当前实现的响应只能响应第一层数据,obj.a.b
是无法响应的,因为读取时 obj.a
是一个普通对象,所以我们需要包装一下。
这里添加了一个 isShallow 参数来控制浅响应,如果选了深响应,就递归的进行包装
// createReactive函数
function createReactive(obj, isShallow = false){
return new Proxy(obj,{
get(target,key,receiver){
if(key === 'raw'){
return target
}
const res = Reflect.get(target,key,receiver)
// 浅响应直接返回原始结果
if(isShallow){
return res
}
// 深响应进行追踪并递归可能的对象
track(target,key)
if(typeof res === 'object' && res !== null){
return reactive(res) // 是对象的话包装
}
return res
}
// -----------省略拦截函数
})
}
function reactive(obj){
return createReactive(obj)
}
function shallowReactive(obj){
return createReactive(obj,true)
}
我们希望用户尝试修改只读数据时,会出现警告
新增第三个参数,对删除和设置的拦截进行修改:
// createReactive
function createReactive(obj, isShallow = false,isReadonly = false){
return new Proxy(obj,{
set(target,key,newVal,receiver){
if(isReadonly){ // 新增
console.warn(`属性${key}是只读的`)
return true
}
// 省略。。。。。
},
deleteProperty(target,key){
if(isReadonly){ // 新增
console.warn(`属性${key}是只读的`)
return true
}
// 省略。。。。。
}
// -----------省略拦截函数
})
}
因为是只读属性,所以我们不需要对字段进行追踪,节省性能
// createReactive
function createReactive(obj, isShallow = false,isReadonly = false){
return new Proxy(obj,{
get(target,key,receiver){
// 非只读的时候才需要建立联系
if(!isReadonly){
track(target,key)
}
// ...省略其他,跟上面一样
},
})
}
浅只读、深只读 包装函数:
// 包装函数
function readonly(obj){
return createReactive(obj,false,true)
}
function shallowReadonly(obj){
return createReactive(obj,true,true)
}
// createReactive
function createReactive(obj, isShallow = false,isReadonly = false){
return new Proxy(obj,{
get(target,key,receiver){
if(!isReadonly){
track(target,key)
}
if(typeof res === 'object' && res !== null){
return isReadonly ? readonly(res) : reactive(res) // 能到这一步的一定是深只读或者深响应
}
return res
}
})
}
数组是一个异质对象,[[DefineOwnProperty]] 内部方法与常规方法不同,其他的相同
数组的读取操作:
索引:arr[0]
访问长度:arr.length
数组作为对象遍历 for in
for of 遍历数组
数组原型方法
数组的设置操作:
索引修改
修改数组长度
栈方法:push、pop等
原型方法:splice fill sort
上述操作需要我们进行代理的时候做一些特殊判断
1、我们在副作用函数中访问length,然后通过索引赋值导致数组长度变化,需要重新执行副作用函数
2、我们修改length,那么此时 index>=length 的元素需要触发响应
下面代码实现了这两点
// createReactive
function createReactive(obj, isShallow = false,isReadonly = false){
return new Proxy(obj,{
set(target,key,newVal,receiver){
const oldVal = target[key]
// 判断是添加还是设置操作
const type = Array.isArray(target) ? Number(key) < target.length ? 'SET' : 'ADD' : Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD'
const res = Reflect.set(target,key,newVal, receiver)
if(target === receiver.raw){
if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal)){
trigger(target,key,type,newVal) // 这里要传 newVal,以便跟 length 对比
}
}
return res
}
})
}
// trigger
function trigger(target,key,type,newVal){
const depsMap= bucket.get(target)
if(!depsMap) return
// 省略其他
// 当操作类型为 add 且是目标时数组时,取出length属性相关联的副作用函数
if(type === 'ADD' && Array.isArray(target)){
const lengthEffects = depsMap.get('length')
lengthEffects && lengthEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
}
// 如果是数组且操作 length
if(Array.isArray(target) && key === 'length'){
// 索引大于等于新的length的元素
// 需要把所有相关联的副作用函数取出添加到队列中等待执行
depsMap.forEach((effects, key)=>{
if(key >= newVal){
effects.forEach(effectFn =>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
}
})
}
添加新元素、修改 length 会影响 for...in 结果,本质上都是修改了 length
// createReactive
function createReactive(obj, isShallow = false,isReadonly = false){
return new Proxy(obj,{
ownKeys(target) {
// 对数组使用length建立联系
track(target, Array.isArray(target) ? 'length' : ITERATE_KEY )
return Reflect.ownKeys(target)
}
})
}