了解proxy
- proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。具体可以前往mdn查看
- proxy为一个构造函数使用new Proxy(obj, {})实例化,接收的参数obj为proxy包装对象,{}中主要是一些操作的处理函数
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- vue3的响应式原理主要用到了proxy中的get,set方法,get接收三个参数target,key, receiver,target参数为目标对象,key为获取的属性名,receiver为proxy对象,set参数和get参数一致但多了一个value值
- proxy中使用Reflect方法,Reflect方法不是一个构造函数,可以将其当成函数调用,Reflect所有的方法和参数都是静态的,proxy上面的方法Reflect上面都有,接收的参数也一致
export const reactive:Function = <T extends object>(target:T) => { return new Proxy(target, { get(target, key, receiver) { let result = Reflect.get(target, key, receiver) return result }, set(target, key, value, receiver) { return Reflect.set(target, key, value, receiver) }, }) }
了解vue3响应式原理与实现
- vue3通过包装一个对象对对象的操作进行劫持,取值时会触发proxy的属性读取操作捕捉器 get,存值或对值进行改动时会触发属性设置操作捕捉器set
- 在get 方法中当我们对一个对象进行取值操作时需要增加一个track方法收集依赖,set方法修改值赋值时需要增加一个trigger方法实现依赖
- 在此之前需要先通过一个effect闭包方法来收集一个副作用函数,在这个副作用函数里面渲染dom节点,流程代码如下
import { effect } from './effect.js' let obj = reactive({ name: '张三', nickname: '法外狂徒', age: 18 }) effect(() => { let vDom = { tag: 'div', children: [ { tag: 'h1', children: obj.nickname }, { tag: 'h2', children: obj.name }, { tag: 'h3', children: obj.age }, { tag: 'button', props: { onclick: () => {obj.age = 25} }, children: '改变年龄' }, ] } let app = document.querySelector('#app') app.innerHTML = "" rander(vDom, app) })
- 在写track方法前我们先需要一个全局的weakMap用来存取数据,而恰好weakMap只能接收一个对象做键,而vue3中的reactive只能接收引用类型的数据,这个waekMap以传入的对象做键,一个Map对象做值,然后这个Map对象的键是weakMap对象的键的键,值是一个Set对象,Set里面存储的是每次监听到get操作保存的副作用函数effect,而effect真正调用的是传入的渲染节点的方法,在proxy监听到get操作时按照如下结构保存值,监听到set操作时取到对应的Set对象使用forEach遍历执行其中的effect方法,具体数据结构图和代码如下
const targetMap:any = new WeakMap() export const track = <T>(target:object, key:T) => { let depsMap = targetMap.get(target) if(!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } let deps = depsMap.get(key) if(!deps) { deps = new Set() depsMap.set(key, deps) } deps.add(activeEffect) } export const trigger = <T>(target:object, key:T) => { let depsMap = targetMap.get(target) if (!depsMap) { return } let deps = depsMap.get(key) deps.forEach((fn:Function) => fn()); }
import {track, trigger} from './effect' let isObj:Function = <T>(obj:T) => { return obj != null && typeof obj == 'object' } export const reactive:Function = <T extends object>(target:T) => { return new Proxy(target, { get(target, key, receiver) { track(target, key) let res = Reflect.get(target, key, receiver) if(isObj(res)) { return reactive(res) } return res }, set(target, key, value, receiver) { let res = Reflect.set(target, key, value, receiver) trigger(target, key) return res }, }) }
遍历渲染dom树
- 上面的步骤其实已经实现了双向绑定的核心操作,接下来只需要把dom树渲染成真实dom即可完成双向绑定(博主的代码的dom更新是先注销之前根节点下的dom然后重新渲染一遍,以便演示双向绑定效果,在vue3中则是先根据模板编译出dom树(虚拟dom),对其中会动态变化的值打上标记,然后使用diff算法计算出最小性能消耗,只替换更改过的节点)
-
function isObj(obj) { let show = obj !== null && typeof obj === 'object' return show } // vnode是虚拟节点树,container是渲染的根节点 function rander(vnode, container) { let el = document.createElement(vnode.tag) //存在方法或者属性 if(vnode.props) { for (const key in vnode.props) { if (/^on/.test(key)) { //判断是否为方法 el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key]) } else { //属性 el.setAttribute(key, vnode.props[key]) } } } if(isObj(vnode.children)) { // 有子节点 if(vnode.children instanceof Array) { // 为数组遍历 vnode.children.forEach(item => { rander(item, el) }); } else{ rander(vnode.children, el) } } else { // 没有子节点 el.innerText = vnode.children } container.appendChild(el) }
完整代码
- reactive.ts
import {track, trigger} from './effect' let isObj:Function = <T>(obj:T) => { return obj != null && typeof obj == 'object' } export const reactive:Function = <T extends object>(target:T) => { return new Proxy(target, { get(target, key, receiver) { track(target, key) let res = Reflect.get(target, key, receiver) if(isObj(res)) { return reactive(res) } return res }, set(target, key, value, receiver) { let res = Reflect.set(target, key, value, receiver) trigger(target, key) return res }, }) }
effect.ts
let activeEffect:Function; export const effect = (fn:Function):void => { const _effect = () => { activeEffect = _effect fn() } _effect() } const targetMap:any = new WeakMap() export const track = <T>(target:object, key:T) => { let depsMap = targetMap.get(target) if(!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } let deps = depsMap.get(key) if(!deps) { deps = new Set() depsMap.set(key, deps) } deps.add(activeEffect) } export const trigger = <T>(target:object, key:T) => { let depsMap = targetMap.get(target) if (!depsMap) { return } let deps = depsMap.get(key) deps.forEach((fn:Function) => fn()); }
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> </div> <script type="module"> import { reactive } from './reactive.js' import { effect } from './effect.js' let obj = reactive({ name: '张三', nickname: '法外狂徒', age: 18 }) function isObj(obj) { let show = obj !== null && typeof obj === 'object' return show } // vnode是虚拟节点树,container是渲染的根节点 function rander(vnode, container) { let el = document.createElement(vnode.tag) //存在方法或者属性 if(vnode.props) { for (const key in vnode.props) { if (/^on/.test(key)) { //判断是否为方法 el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key]) } else { //属性 el.setAttribute(key, vnode.props[key]) } } } if(isObj(vnode.children)) { // 有子节点 if(vnode.children instanceof Array) { // 为数组遍历 vnode.children.forEach(item => { rander(item, el) }); } else{ rander(vnode.children, el) } } else { // 没有子节点 el.innerText = vnode.children } container.appendChild(el) } effect(() => { let vDom = { tag: 'div', children: [ { tag: 'h1', children: obj.nickname }, { tag: 'h2', children: obj.name }, { tag: 'h3', children: obj.age }, { tag: 'button', props: { onclick: () => {obj.age = 25} }, children: '改变年龄' }, ] } let app = document.querySelector('#app') app.innerHTML = "" rander(vDom, app) }) </script> </body> </html>
注:html记得引用ts编译后的js