Vue3.0 响应式系统源码逐行分析讲解_vue.js_脚本之

来源:http://www.chinese-glasses.com 作者:Web前端 人气:113 发布时间:2020-04-15
摘要:时间: 2019-10-11阅读: 153标签: vue3 前言 在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020年第一季度才有可能发布 3.0 正式版。可以直接看 href="-next"github源码。 关于响应式原

时间: 2019-10-11阅读: 153标签: vue3

前言

在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。可以直接看 href="-next"github源码。

关于响应式原理想必大家都很清楚了,下面我将会根据响应式API来具体讲解Vue3.0中的实现原理, 另外我只会针对get,set进行深入分析,本文包含以下API实现,推荐大家顺序阅读

新版Vue 3.0计划并已实现的主要架构改进和新功能:

effect reactive readonly computed ref

编译器(Compiler)使用模块化架构优化 "Block tree"更激进的 static tree hoisting 功能 (检测静态语法,进行提升)支持 Source map内置标识符前缀(又名"stripWith")内置整齐打印(pretty-printing)功能移除 Source map 和标识符前缀功能后,使用 Brotli 压缩的浏览器版本精简了大约10KB运行时(Runtime)速度显著提升同时支持 Composition API 和 Options API,以及 typings基于 Proxy 实现的数据变更检测支持 Fragments (允许组件有从多个根结点)支持 Portals (允许在DOM的其它位置进行渲染)支持 Suspense w/ async setup()目前不支持IE111.剖析Vue Composition API

对了,大家一定要先知道怎么用哦~

可以去看官方地址

引子

Vue 3 使用ts实现了类型推断,新版api全部采用普通函数,在编写代码时可以享受完整的类型推断(避免使用装饰器)解决了多组件间逻辑重用问题 (解决:高阶组件、mixin、作用域插槽)Composition API 使用简单

先来段代码,大家可以直接复制哦,注意引用的文件

先尝鲜Vue3.0看看效果

 Document    const { reactive, computed, effect, watch, createApp } = Vue const App = { template: ` <div > <button @click="increment">{{ state.count }}</button> </div> `, setup() { const state = reactive function increment { state.count++ } effect => { console.log('count改变', state.count); }) return { state, increment } } } createApp 
script src="vue.global.js"/scriptdiv /divscript function usePosition(){ // 实时获取鼠标位置 let state = Vue.reactive({x:0,y:0}); function update(e) { state.x= e.pageX state.y = e.pageY } Vue.onMounted(() = { window.addEventListener('mousemove', update) }) Vue.onUnmounted(() = { window.removeEventListener('mousemove', update) }) return Vue.toRefs(state); } const App = { setup(){ // Composition API 使用的入口 const state = Vue.reactive({name:'youxuan'}); // 定义响应数据 const {x,y} = usePosition(); // 使用公共逻辑 Vue.onMounted(()={ console.log('当组挂载完成') }); Vue.onUpdated(()={ console.log('数据发生更新') }); Vue.onUnmounted(()={ console.log('组件将要卸载') }) function changeName(){ state.name = 'webyouxuan'; } return { // 返回上下文,可以在模板中使用 state, changeName, x, y } }, template:`button @click="changeName"{{state.name}} 鼠标x: {{x}} 鼠标: {{y}}/button` } Vue.createApp().mount(App,container);/script

这段代码,想必大家都看得懂,点击后count增加,视图也随之更新,effect监听了count改变,那么为什么effect能观察到count变化呢,还有为什么reactive可以实现响应式?

到这里你会发现响应式才是Vue的灵魂2.源码目录剖析

effect

packages目录中包含着Vue3.0所有功能

为什么要先说这个函数呢,因为它和其他函数都息息相关,只有先了解它才能更好的理解其他响应式API

├── packages│ ├── compiler-core # 所有平台的编译器│ ├── compiler-dom # 针对浏览器而写的编译器│ ├── reactivity # 数据响应式系统│ ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API│ ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。│ ├── runtime-test # 专门为测试写的runtime│ ├── server-renderer # 用于SSR│ ├── shared # 帮助方法│ ├── template-explorer│ └── vue # 构建vue runtime + compiler
export function effect( fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect { if (.isEffect) { fn = .raw } const effect = createReactiveEffect if  { effect() } return effect}

compilercompiler-core主要功能是暴露编译相关的API以及baseCompile方法compiler-dom基于compiler-core封装针对浏览器的compiler(对浏览器标签进行处理)

if判断,判断如果传入的fn函数,它已经是effect了,也就是一个标识,直接获取该函数上的raw属性,这个属性后面会讲到

runtimeruntime-core虚拟 DOM 渲染器、Vue 组件和 Vue 的各种APIruntime-test将DOM结构格式化成对象,方便测试runtime-dom基于runtime-core编写的浏览器的runtime(增加了节点的增删改查,样式处理等),返回render、createApp方法

调用createReactiveEffect

reactivity单独的数据响应式系统,核心方法reactive、effect、ref、computed

如果options中有lazy,就会立即调用effect,其实本质上调用的还是传入的fn函数

vue整合compiler+runtime

// 了解一下options有哪些{ lazy?: boolean // 是否立即调用fn computed?: boolean // 是否是computed scheduler?:  => void // 在调用fn之前执行 onTrack?:  => void // 在依赖收集完成之后调用 onTrigger?:  => void // 在调用fn之前执行,源码上来看和scheduler调用时机一样,只是传入参数不同 onStop?: () => void // 清除依赖完成后调用}

到此我们解析了Vue3.0结构目录,整体来看整个项目还是非常清晰的

createReactiveEffect

再来尝尝鲜:我们可以根据官方的测试用例来看下如何使用Vue3.0

上面提到了createReactiveEffect函数,我们来看看它的实现

const app = { template:`div{{count}}/div`, data(){ return {count:100} },}let proxy = Vue.createApp().mount(app,container);setTimeout(()={ proxy.count = 200;},2000)
function createReactiveEffect( fn: Function, options: ReactiveEffectOptions): ReactiveEffect { // 又包装了一层函数 const effect = function effect: any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect effect.isEffect = true // 标识effect effect.active = true // 如果active effect.raw = fn // 传入的回调 effect.scheduler = options.scheduler effect.onTrack = options.onTrack effect.onTrigger = options.onTrigger effect.onStop = options.onStop effect.computed = options.computed effect.deps = [] // 用于收集依赖 return effect}

接下来我们来对比 Vue 2 和 Vue 3 中的响应式原理区别

注意,敲黑板,这里有个run函数,很重要,因为它保存了依赖

3.Vue2.0响应式原理机制 - defineProperty

function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if  { return fn } if (activeReactiveEffectStack.indexOf { cleanup try { activeReactiveEffectStack.push return fn } finally { activeReactiveEffectStack.pop() } }}

这个原理老生常谈了,就是拦截对象,给对象的属性增加set和get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截

他把依赖存储在了一个全局的数组中activeReactiveEffectStack, 他以栈的形式存储,调用完依赖后,会弹出,大家要留意一下这里,后面会用到

3.1 对对象进行拦截

怎么样,是不是很简单~

function observer(target){ // 如果不是对象数据类型直接返回即可 if(typeof target !== 'object'){ return target } // 重新定义key for(let key in target){ defineReactive(target,key,target[key]) }}function update(){ console.log('update view')}function defineReactive(obj,key,value){ observer(value); // 有可能对象类型是多层,递归劫持 Object.defineProperty(obj,key,{ get(){ // 在get 方法中收集依赖 return value }, set(newVal){ if(newVal !== value){ observer(value); update(); // 在set方法中触发更新 } } })}let obj = {name:'youxuan'}observer(obj);obj.name = 'webyouxuan';

reactive

3.2 数组方法劫持

export function reactive { // 如果target是已经被readonly对象,那么直接返回对应的proxy对象 if (readonlyToRaw.has { return target } // 如果target是已经被readonly对象,那么直接返回对应的真实对象 if (readonlyValues.has { return readonly } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers )}
let oldProtoMehtods = Array.prototype;let proto = Object.create(oldProtoMehtods);['push','pop','shift','unshift'].forEach(method={ Object.defineProperty(proto,method,{ get(){ update(); oldProtoMehtods[method].call(this,...arguments) } })})function observer(target){ if(typeof target !== 'object'){ return target } // 如果不是对象数据类型直接返回即可 if(Array.isArray(target)){ Object.setPrototypeOf(target,proto); // 给数组中的每一项进行observr for(let i = 0 ; i  target.length;i++){ observer(target[i]) } return }; // 重新定义key for(let key in target){ defineReactive(target,key,target[key]) }}

前两个if是用来处理这种情况的

测试

// 情况一const state1 = readonlyconst state2 = reactive// 情况二const obj = { count: 0 }const state1 = readonlyconst state2 = reactive可以看到reactive它的参数是被readonly的对象,reactive不会对它再次创建响应式,而是通过Map映射,拿到对应的对象,即Proxy <==> Object的相互转换。createReactiveObject创建响应式对象,注意它的参数createReactiveObject( target, rawToReactive, // Object ==> Proxy reactiveToRaw, // Proxy ==> Object mutableHandlers, // get set has ... mutableCollectionHandlers // 很少会用,不讲了~)
let obj = {hobby:[{name:'youxuan'},'喝']}observer(obj)obj.hobby[0].name = 'webyouxuan'; // 更改数组中的对象也会触发试图更新console.log(obj)

以上就是reative一开始所做的一些事情,下面继续分析createReactiveObject

这里依赖收集的过程就不详细描述了,我们把焦点放在Vue3.0上

createReactiveObject

Object.defineProperty缺点无法监听数组的变化需要深度遍历,浪费内存4.Vue3.0数据响应机制

function createReactiveObject( target: any, toProxy: WeakMap, toRaw: WeakMap, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler) { // 如果不是对象,在开发环境报出警告 if  { if  { console.warn(`value cannot be made reactive: ${String } return target } let observed = toProxy.get // 如果目标对象已经有proxy对象,直接返回 if  { return observed } // 如果目标对象是proxy的对象,并且有对应的真实对象,那么也直接返回 if  { return target } // 如果它是vnode或者vue,则不能被观测 if  { return target } // 判断被观测的对象是否是set,weakSet,map,weakMap,根据情况使用对应proxy的,配置对象 const handlers = collectionTypes.has ? collectionHandlers : baseHandlers observed = new Proxy toProxy.set toRaw.set if (!targetMap.has { targetMap.set } return observed}
  • Proxy

第一个if,判断是否是对象,否则报出警告

在学习Vue3.0之前,你必须要先熟练掌握ES6中的Proxy、Reflect及 ES6中为我们提供的Map、Set两种数据结构

toProxy拿到观测对象的Proxy对象,如果存在直接返回

先应用再说原理:

// 这种情况const obj = { count: 0 }const state1 = reativeconst state2 = reative
let p = Vue.reactive({name:'youxuan'});Vue.effect(()={ // effect方法会立即被触发 console.log(p.name);})p.name = 'webyouxuan';; // 修改属性后会再次触发effect方法

toRaw拿到Proxy对象对应的真实对象,如果存在直接返回

源码是采用ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!

// 这种情况const obj = { count: 0 }const state1 = reativeconst state2 = reative

4.1 reactive方法实现

有些情况无法被观测,则直接返回观测对象本身

通过proxy 自定义获取、增加、删除等行为

const canObserve = : boolean => { return ( !value._isVue && !value._isVNode && observableValueRE.test && !nonReactiveValues.has}
function reactive(target){ // 创建响应式对象 return createReactiveObject(target);}function isObject(target){ return typeof target === 'object'  target!== null;}function createReactiveObject(target){ // 判断target是不是对象,不是对象不必继续 if(!isObject(target)){ return target; } const handlers = { get(target,key,receiver){ // 取值 console.log('获取') let res = Reflect.get(target,key,receiver); return res; }, set(target,key,value,receiver){ // 更改 、 新增属性 console.log('设置') let result = Reflect.set(target,key,value,receiver); return result; }, deleteProperty(target,key){ // 删除属性 console.log('删除') const result = Reflect.deleteProperty(target,key); return result; } } // 开始代理 observed = new Proxy(target,handlers); return observed;}let p = reactive({name:'youxuan'});console.log(p.name); // 获取p.name = 'webyouxuan'; // 设置delete p.name; // 删除

设置handlers,即get,set等属性访问器, 注意:collectionHandlers是用来处理观测对象为Set,Map等情况,很少见,这里就不讲了

我们继续考虑多层对象如何实现代理

 const handlers = collectionTypes.has ? collectionHandlers : baseHandlers
let p = reactive({ name: "youxuan", age: { num: 10 } });p.age.num = 11

然后创建了Proxy对象,并把观测对象和Proxy对象,分别做映射

由于我们只代理了第一层对象,所以对age对象进行更改是不会触发set方法的,但是却触发了get方法,这是由于p.age会造成get操作

 observed = new Proxy toProxy.set toRaw.set
get(target, key, receiver) { // 取值 console.log("获取"); let res = Reflect.get(target, key, receiver); return isObject(res) // 懒代理,只有当取值时再次做代理,vue2.0中一上来就会全部递归增加getter,setter ? reactive(res) : res;}

然后在targetMap做了target ==> Map的映射,这又是干嘛,注意:targetMap是全局的

这里我们将p.age取到的对象再次进行代理,这样在去更改值即可触发set方法

export const targetMap: WeakMap = new WeakMap() if (!targetMap.has { targetMap.set }

我们继续考虑数组问题我们可以发现Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化

在这里先给大家卖个关子,targetMap非常重要,是用来保存依赖的地方

let p = reactive([1,2,3,4]);p.push(5);

讲完了reactive,可以回到一开始的引子

但是这样会触发两次set方法,第一次更新的是数组中的第4项,第二次更新的是数组的length

依赖收集

我们来屏蔽掉多次触发,更新操作

说到依赖收集,不得不提到,依赖的创建,那么Vue3.0是在哪里创建了渲染依赖呢,大家可以找到下面这段代码以及文件

set(target, key, value, receiver) { // 更改、新增属性 let oldValue = target[key]; // 获取上次的值 let hadKey = hasOwn(target,key); // 看这个属性是否存在 let result = Reflect.set(target, key, value, receiver); if(!hadKey){ // 新增属性 console.log('更新 添加') }else if(oldValue !== value){ // 修改存在的属性 console.log('更新 修改') } // 当调用push 方法第一次修改时数组长度已经发生变化 // 如果这次的值和上次的值一样则不触发更新 return result;}
// vue-nextpackagesruntime-coresrccreateRenderer.ts function setupRenderEffect( instance: ComponentInternalInstance, parentSuspense: HostSuspsenseBoundary | null, initialVNode: HostVNode, container: HostElement, anchor: HostNode | null, isSVG: boolean ) { // create reactive effect for rendering let mounted = false instance.update = effect(function componentEffect() { // ... }, __DEV__ ? createDevEffectOptions : prodEffectOptions) }

解决重复使用reactive情况

代码特别长,我剪掉了中间部分,大家还记得effect有个选项lazy吗,没错,它默认是false,也就会立即调用传入的componentEffect回调,在它内部调用了patch实现了组件的挂载。

// 情况1.多次代理同一个对象let arr = [1,2,3,4];let p = reactive(arr);reactive(arr);// 情况2.将代理后的结果继续代理let p = reactive([1,2,3,4]);reactive(p);

敲黑板,关键来了,还记得effect调用,内部会调用run方法吗

通过hash表的方式来解决重复代理的情况

function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if  { return fn } if (activeReactiveEffectStack.indexOf { cleanup try { activeReactiveEffectStack.push return fn } finally { activeReactiveEffectStack.pop() } }}
const toProxy = new WeakMap(); // 存放被代理过的对象const toRaw = new WeakMap(); // 存放已经代理过的对象function reactive(target) { // 创建响应式对象 return createReactiveObject(target);}function isObject(target) { return typeof target === "object"  target !== null;}function hasOwn(target,key){ return target.hasOwnProperty(key);}function createReactiveObject(target) { if (!isObject(target)) { return target; } let observed = toProxy.get(target); if(observed){ // 判断是否被代理过 return observed; } if(toRaw.has(target)){ // 判断是否要重复代理 return target; } const handlers = { get(target, key, receiver) { // 取值 console.log("获取"); let res = Reflect.get(target, key, receiver); return isObject(res) ? reactive(res) : res; }, set(target, key, value, receiver) { let oldValue = target[key]; let hadKey = hasOwn(target,key); let result = Reflect.set(target, key, value, receiver); if(!hadKey){ console.log('更新 添加') }else if(oldValue !== value){ console.log('更新 修改') } return result; }, deleteProperty(target, key) { console.log("删除"); const result = Reflect.deleteProperty(target, key); return result; } }; // 开始代理 observed = new Proxy(target, handlers); toProxy.set(target,observed); toRaw.set(observed,target); // 做映射表 return observed;}

这里进行了第一步的依赖收集,保存在全局数组中,为了方便触发get的对象,将依赖收集到自己的deps中然后就是调用patch,进行组件挂载

到这里reactive方法基本实现完毕,接下来就是与Vue2中的逻辑一样实现依赖收集和触发更新

if  { const subTree = (instance.subTree = renderComponentRoot // beforeMount hook if  { invokeHooks } patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) initialVNode.el = subTree.el // mounted hook if  { queuePostRenderEffect(instance.m, parentSuspense) } mounted = true}
get(target, key, receiver) { let res = Reflect.get(target, key, receiver);+ track(target,'get',key); // 依赖收集 return isObject(res) ?reactive(res):res;},set(target, key, value, receiver) { let oldValue = target[key]; let hadKey = hasOwn(target,key); let result = Reflect.set(target, key, value, receiver); if(!hadKey){+ trigger(target,'add',key); // 触发添加 }else if(oldValue !== value){+ trigger(target,'set',key); // 触发修改 } return result;}

至于它内部实现,我就不讲了,不是本文重点,然后我们去编译的地方看看

track的作用是依赖收集,收集的主要是effect,我们先来实现effect原理,之后再完善track和trigger方法

//vue-nextpackagesruntime-coresrccomponent.tsfunction finishComponentSetup( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null) { const Component = instance.type as ComponentOptions if  { if (Component.template && !Component.render) { if  { Component.render = compile(Component.template, { onError } else if  { warn( `Component provides template but the build of Vue you are running ` + `does not support on-the-fly template compilation. Either use the ` + `full build or pre-compile the template using Vue CLI.` ) } } if (__DEV__ && !Component.render) { warn( `Component is missing render function. Either provide a template or ` + `return a render function from setup } instance.render = (Component.render || NOOP) as RenderFunction } // ...其他}

4.2 effect实现

上面的代码是编译部分,我们来看看例子中编译后是什么样

effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。

 {const _Vue = Vueconst _createVNode = Vue.createVNodeconst _hoisted_1 = { id: "box" }return function render { const { toString: _toString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue return , _createBlock("div", _hoisted_1, [ _createVNode("button", { onClick: increment }, _toString, 9 /* TEXT, PROPS */, ["onClick"]) ])) }}})
let school = {name:'youxuan'}let p = reactive(school);effect(()={ console.log(p.name); // youxuan})

可以看到,编译的代码中,有使用到state.count,那么就会触发get访问器,从而收集依赖,至于为什么能直接访问到属性,原因是由于with设置了上下文,下面我们具体分析get

10bet,我们来实现effect方法,我们需要将effect方法包装成响应式effect。

get

function effect(fn) { const effect = createReactiveEffect(fn); // 创建响应式的effect effect(); // 先执行一次 return effect;}const activeReactiveEffectStack = []; // 存放响应式effectfunction createReactiveEffect(fn) { const effect = function() { // 响应式的effect return run(effect, fn); }; return effect;}function run(effect, fn) { try { activeReactiveEffectStack.push(effect); return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性 } finally { activeReactiveEffectStack.pop(effect); }}
// vue-nextpackagesreactivitysrcbaseHandlers.tsfunction createGetter { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (typeof key === 'symbol' && builtInSymbols.has { return res } // _isRef if  { return res.value } track(target, OperationTypes.GET, key) // 如果该属性对应的值还是对象,就继续递归创建响应式 return isObject ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly : res }}

当调用fn()时可能会触发get方法,此时会触发track

调用Reflect.get获取属性值

const targetMap = new WeakMap();function track(target,type,key){ // 查看是否有effect const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1]; if(effect){ let depsMap = targetMap.get(target); if(!depsMap){ // 不存在map targetMap.set(target,depsMap = new Map()); } let dep = depsMap.get(target); if(!dep){ // 不存在set depsMap.set(key,(dep = new Set())); } if(!dep.has(effect)){ dep.add(effect); // 将effect添加到依赖中 } }}

如果key是symbol并且是Symbol的一个属性,就直接返回该值

当更新属性时会触发trigger执行,找到对应的存储集合拿出effect依次执行

// 这种情况const key = Symbolconst state = reative({ [key]: 'symbol value'})state[key]
function trigger(target,type,key){ const depsMap = targetMap.get(target); if(!depsMap){ return } let effects = depsMap.get(key); if(effects){ effects.forEach(effect={ effect(); }) }}

如果值为Ref返回该值的value,看到这里如果大家有了解过ref api的话就知道了,由于ref它自己实现了自己的get,set,所以不再需要执行后面的逻辑,这个在后面会讲

我们发现如下问题

递归深度观测,使整个对象都为响应式

本文由10bet发布于Web前端,转载请注明出处:Vue3.0 响应式系统源码逐行分析讲解_vue.js_脚本之

关键词:

上一篇:前端渲染和后端渲染的区别10bet

下一篇:没有了

最火资讯