#013#vue3的响应式原理

文章探讨了Vue2中通过`defineProperty`实现响应式以及Vue3如何使用Proxy改进这一机制。Vue2的响应式依赖于对象属性的getter和setter,对于数组和复杂对象需要递归处理。Vue3则利用Proxy直接代理对象,提供更全面的拦截能力。
摘要由CSDN通过智能技术生成

前言

日志,各位看官就当乐子看吧。

正经人谁写日记啊?!! ——鹅城县长

当提及Vue时,会说起什么呢?

  1. 基础:指令的区别或者原理,如v-showv-if
  2. 解析过程,即生命周期;
  3. 它的特性的原理:如何实现响应式,这其中就包括refreactive
  4. 组件
  5. vue3的区别

今天看看如何实现响应式的,还有vue2是如何实现的。

vue3和vue2的区别

Proxy和defineProperty

原文:https://vue3js.cn/interview/vue3/proxy.html

defineProperty

vue2 使用defineProperty生成响应式对象。下面通过代码看看如何实现的:

function update(value){
    app.innerText=obj.foo
}
function defineReactive(obj, key, val){
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}: ${val}`)
            return val;
        },
        set(newVal){
            if(newVal !== val){
                val=newVal
                update()
            }
        }
    })
}

调用defineProperty实现obj.foo的数据响应式,

const obj={}
defineProperty(obj, 'foo', '')
setTimeout(()=>{
    obj.foo = new Date().toLocaleTimeString()
},1000)

1秒后,数据发生变化,update方法会被触发。当存在多个key时,需要利用遍历,给obj中的每个key设置getset方法

function observe(obj) {
    if (typeof obj !== 'object' || obj==null){
        return;
    }
    Object.keys(obj).forEach(k => {
        defineReactive(obj, k, obj[k])
    })
}

如果value也是个对象,那么还需要在defineProperty中利用递归,

// defineReactive 函数
observe(val)    // 递归设置val
Object.defineProperty(obj, key, {
    get() {
        console.log(`get ${key}: ${val}`)
        return val;
    },
    set(newVal){
        if(newVal !== val){
            val=newVal
            update()
        }
    }
})

当赋的是一个对象时,set同样需要递归

set(newVal) {
    if(newVal !== val){
        observe(newVal)
        notifyUpdate()
    }
}

基本的对象响应式完整代码:

function update(value){
    app.innerText=obj.foo
}
function defineReactive(obj, key, val){
    observe(val)
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}: ${val}`)
            return val;
        },
        set(newVal) {
            if(newVal !== val){
                observe(newVal)
                notifyUpdate()
            }
        }
    })
}
function observe(obj) {
    if (typeof obj !== 'object' || obj==null){
        return;
    }
    Object.keys(obj).forEach(k => {
        defineReactive(obj, k, obj[k])
    })
}

// 使用
const obj={
    foo: 'foo',
    bar: 'bar'
}
observe(obj)

但是,其中还存在较多问题:

  1. 无法劫持删除添加操作
  2. 数组的监听,并不好用,对数组的api(push,pop等)无法劫持
  3. 可以看到当数据是嵌套对象时,需要递归,会对性能产生影响(这个问题 vue3 似乎也存在)

对上述1,2两个问题,vue2的解决方案:

// 数组重写
const originalProto=Array.prototype
const arrayProto=Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
    arrayProto[method] = function(){
        originalProto(method).apply(this.arguments);
        dep.notice();
    }
})

Proxy

Proxy可以代理对象的默认行为,我们可以改变默认行为,实现自定义。能代理的行为有:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截Object.keys(proxy)、for…in等循环,返回一个数组
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
  • 其他:preventExtensions(target)getPrototypeOf(target)isExtensible(target)setPrototypeOf(target, proto)apply(target, object, args)construct(target, args)

Reflect对象,用于调用对象原先的默认行为,上面提到的所有内容。

补充
通过Proxy.revocable(target, handler);可以创建一个可撤销的代理对象。调用revoke()方法即可撤销。

使用Proxy实现一个响应式方法reactive

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            // 如果是嵌套对象,使用递归
            return isObject(res) ? reactive(res) : res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

defineProperty不同,Proxy直接劫持对象,所以不需要通过遍历劫持对象属性了。

挂载时发生了什么

// App.vue
<div id="app"></div>
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

1. createApp

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ...
});

2. ensureRenderer

function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}

3. createRenderer

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

4. baseCreateRenderer

这个函数中,实现了vnodediffpatch,很大很重要,目前只关注其返回值。

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options

  // ....此处省略两千行,我们先不管

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

5. createAppAPI

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }

    // 创建默认APP配置
    const context = createAppContext()
    ...

    const app: App = {
        ...
        // 都是一些眼熟的方法
        use() {},
        mixin() {},
        component() {},
        directive() {},

        // mount 我们拎出来讲
        mount() {},
        unmount() {},
        // ...
    }

    return app
  }
}

6. createAppContext

export function createAppContext(): AppContext {
  return {
    config: {
      isNativeTag: NO,
      devtools: true,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

OK,结束了,虽然有很多东西没看到:如何实现响应式的、如何构建VNode的,之后再看吧!

彩蛋

今天去了我这的市图书馆,环境还不错哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值