当学习成为了习惯,常识也就变成了常识。 感激各位的 点赞、珍藏和评论。
新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。
前言
上一篇文章 Vue 源码解读(5)—— 全局 API 具体介绍了 Vue 的各个全局 API 的实现原理,本篇文章将会具体介绍各个实例办法的实现原理。
指标
深刻了解以下实例办法的实现原理。
- vm.$set
- vm.$delete
- vm.$watch
- vm.$on
- vm.$emit
- vm.$off
- vm.$once
- vm._update
- vm.$forceUpdate
- vm.$destroy
- vm.$nextTick
- vm._render
源码解读
入口
/src/core/instance/index.js
该文件是 Vue 实例的入口文件,包含 Vue 构造函数的定义、各个实例办法的初始化。
// Vue 的构造函数function Vue (options) { // 调用 Vue.prototype._init 办法,该办法是在 initMixin 中定义的 this._init(options)}// 定义 Vue.prototype._init 办法initMixin(Vue)/** * 定义: * Vue.prototype.$data * Vue.prototype.$props * Vue.prototype.$set * Vue.prototype.$delete * Vue.prototype.$watch */stateMixin(Vue)/** * 定义 事件相干的 办法: * Vue.prototype.$on * Vue.prototype.$once * Vue.prototype.$off * Vue.prototype.$emit */eventsMixin(Vue)/** * 定义: * Vue.prototype._update * Vue.prototype.$forceUpdate * Vue.prototype.$destroy */lifecycleMixin(Vue)/** * 执行 installRenderHelpers,在 Vue.prototype 对象上装置运行时便当程序 * * 定义: * Vue.prototype.$nextTick * Vue.prototype._render */renderMixin(Vue)
vm.$data、vm.$props
src/core/instance/state.js
这是两个实例属性,不是实例办法,这里简略介绍以下,当然其自身实现也很简略
// dataconst dataDef = {}dataDef.get = function () { return this._data }// propsconst propsDef = {}propsDef.get = function () { return this._props }// 将 data 属性和 props 属性挂载到 Vue.prototype 对象上// 这样在程序中就能够通过 this.$data 和 this.$props 来拜访 data 和 props 对象了Object.defineProperty(Vue.prototype, '$data', dataDef)Object.defineProperty(Vue.prototype, '$props', propsDef)
vm.$set
/src/core/instance/state.js
Vue.prototype.$set = set
set
/src/core/observer/index.js
/** * 通过 Vue.set 或者 this.$set 办法给 target 的指定 key 设置值 val * 如果 target 是对象,并且 key 本来不存在,则为新 key 设置响应式,而后执行依赖告诉 */export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 更新数组指定下标的元素,Vue.set(array, idx, val),通过 splice 办法实现响应式更新 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } // 更新对象已有属性,Vue.set(obj, key, val),执行更新即可 if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ // 不能向 Vue 实例或者 $data 增加动静增加响应式属性,vmCount 的用途之一, // this.$data 的 ob.vmCount = 1,示意根组件,其它子组件的 vm.vmCount 都是 0 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // target 不是响应式对象,新属性会被设置,然而不会做响应式解决 if (!ob) { target[key] = val return val } // 给对象定义新属性,通过 defineReactive 办法设置响应式,并触发依赖更新 defineReactive(ob.value, key, val) ob.dep.notify() return val}
vm.$delete
/src/core/instance/state.js
Vue.prototype.$delete = del
del
/src/core/observer/index.js
/** * 通过 Vue.delete 或者 vm.$delete 删除 target 对象的指定 key * 数组通过 splice 办法实现,对象则通过 delete 运算符删除指定 key,并执行依赖告诉 */export function del (target: Array<any> | Object, key: any) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } // target 为数组,则通过 splice 办法删除指定下标的元素 if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target: any).__ob__ // 防止删除 Vue 实例的属性或者 $data 的数据 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } // 如果属性不存在间接完结 if (!hasOwn(target, key)) { return } // 通过 delete 运算符删除对象的属性 delete target[key] if (!ob) { return } // 执行依赖告诉 ob.dep.notify()}
vm.$watch
/src/core/instance/state.js
/** * 创立 watcher,返回 unwatch,共实现如下 5 件事: * 1、兼容性解决,保障最初 new Watcher 时的 cb 为函数 * 2、标示用户 watcher * 3、创立 watcher 实例 * 4、如果设置了 immediate,则立刻执行一次 cb * 5、返回 unwatch * @param {*} expOrFn key * @param {*} cb 回调函数 * @param {*} options 配置项,用户间接调用 this.$watch 时可能会传递一个 配置项 * @returns 返回 unwatch 函数,用于勾销 watch 监听 */Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object): Function { const vm: Component = this // 兼容性解决,因为用户调用 vm.$watch 时设置的 cb 可能是对象 if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } // options.user 示意用户 watcher,还有渲染 watcher,即 updateComponent 办法中实例化的 watcher options = options || {} options.user = true // 创立 watcher const watcher = new Watcher(vm, expOrFn, cb, options) // 如果用户设置了 immediate 为 true,则立刻执行一次回调函数 if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } // 返回一个 unwatch 函数,用于解除监听 return function unwatchFn() { watcher.teardown() }}
vm.$on
/src/core/instance/events.js
const hookRE = /^hook://** * 监听实例上的自定义事件,vm._event = { eventName: [fn1, ...], ... } * @param {*} event 单个的事件名称或者有多个事件名组成的数组 * @param {*} fn 当 event 被触发时执行的回调函数 * @returns */Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { const vm: Component = this if (Array.isArray(event)) { // event 是有多个事件名组成的数组,则遍历这些事件,顺次递归调用 $on for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { // 将注册的事件和回调以键值对的模式存储到 vm._event 对象中 vm._event = { eventName: [fn1, ...] } (vm._events[event] || (vm._events[event] = [])).push(fn) // hookEvent,提供从内部为组件实例注入申明周期办法的机会 // 比方从组件内部为组件的 mounted 办法注入额定的逻辑 // 该能力是联合 callhook 办法实现的 if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm}
对于 hookEvent,下一篇文章会具体介绍。
vm.$emit
/src/core/instance/events.js
/** * 触发实例上的指定事件,vm._event[event] => cbs => loop cbs => cb(args) * @param {*} event 事件名 * @returns */Vue.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { // 将事件名转换为小些 const lowerCaseEvent = event.toLowerCase() // 意思是说,HTML 属性不辨别大小写,所以你不能应用 v-on 监听小驼峰模式的事件名(eventName),而应该应用连字符模式的事件名(event-name) if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( `Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".` ) } } // 从 vm._event 对象上拿到以后事件的回调函数数组,并一次调用数组中的回调函数,并且传递提供的参数 let cbs = vm._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) const info = `event handler for "${event}"` for (let i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info) } } return vm}
vm.$off
/src/core/instance/events.js
/** * 移除自定义事件监听器,即从 vm._event 对象中找到对应的事件,移除所有事件 或者 移除指定事件的回调函数 * @param {*} event * @param {*} fn * @returns */Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // vm.$off() 移除实例上的所有监听器 => vm._events = {} if (!arguments.length) { vm._events = Object.create(null) return vm } // 移除一些事件 event = [event1, ...],遍历 event 数组,递归调用 vm.$off if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // 除了 vm.$off() 之外,最终都会走到这里,移除指定事件 const cbs = vm._events[event] if (!cbs) { // 示意没有注册过该事件 return vm } if (!fn) { // 没有提供 fn 回调函数,则移除该事件的所有回调函数,vm._event[event] = null vm._events[event] = null return vm } // 移除指定事件的指定回调函数,就是从事件的回调数组中找到该回调函数,而后删除 let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm}
vm.$once
/src/core/instance/events.js
/** * 监听一个自定义事件,然而只触发一次。一旦触发之后,监听器就会被移除 * vm.$on + vm.$off * @param {*} event * @param {*} fn * @returns */Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this // 调用 $on,只是 $on 的回调函数被非凡解决了,触发时,执行回调函数,先移除事件监听,而后执行你设置的回调函数 function on() { vm.$off(event, on) fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) return vm}
vm._update
/src/core/instance/lifecycle.js
/** * 负责更新页面,页面首次渲染和后续更新的入口地位,也是 patch 的入口地位 */Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // 首次渲染,即初始化页面时走这里 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 响应式数据更新时,即更新页面时走这里 vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook.}
vm.$forceUpdate
/src/core/instance/lifecycle.js
/** * 间接调用 watcher.update 办法,迫使组件从新渲染。 * 它仅仅影响实例自身和插入插槽内容的子组件,而不是所有子组件 */Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() }}
vm.$destroy
/src/core/instance/lifecycle.js
/** * 齐全销毁一个实例。清理它与其它实例的连贯,解绑它的全副指令及事件监听器。 */Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { // 示意实例曾经销毁 return } // 调用 beforeDestroy 钩子 callHook(vm, 'beforeDestroy') // 标识实例曾经销毁 vm._isBeingDestroyed = true // 把本人从老爹($parent)的肚子里($children)移除 const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // 移除依赖监听 if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } // call the last hook... vm._isDestroyed = true // 调用 __patch__,销毁节点 vm.__patch__(vm._vnode, null) // 调用 destroyed 钩子 callHook(vm, 'destroyed') // 敞开实例的所有事件监听 vm.$off() // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null }}
vm.$nextTick
/src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this)}
nextTick
/src/core/util/next-tick.js
const callbacks = []/** * 实现两件事: * 1、用 try catch 包装 flushSchedulerQueue 函数,而后将其放入 callbacks 数组 * 2、如果 pending 为 false,示意当初浏览器的工作队列中没有 flushCallbacks 函数 * 如果 pending 为 true,则示意浏览器的工作队列中曾经被放入了 flushCallbacks 函数, * 待执行 flushCallbacks 函数时,pending 会被再次置为 false,示意下一个 flushCallbacks 函数能够进入 * 浏览器的工作队列了 * pending 的作用:保障在同一时刻,浏览器的工作队列中只有一个 flushCallbacks 函数 * @param {*} cb 接管一个回调函数 => flushSchedulerQueue * @param {*} ctx 上下文 * @returns */export function nextTick (cb?: Function, ctx?: Object) { let _resolve // 用 callbacks 数组存储通过包装的 cb 函数 callbacks.push(() => { if (cb) { // 用 try catch 包装回调函数,便于谬误捕捉 try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true // 执行 timerFunc,在浏览器的工作队列中(首选微工作队列)放入 flushCallbacks 函数 timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }}
vm._render
/src/core/instance/render.js
/** * 通过执行 render 函数生成 VNode * 不过外面加了大量的异样解决代码 */Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } // 设置父 vnode。这使得渲染函数能够拜访占位符节点上的数据。 vm.$vnode = _parentVnode // render self let vnode try { currentRenderingInstance = vm // 执行 render 函数,生成 vnode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // 到这儿,阐明执行 render 函数时出错了 // 开发环境渲染错误信息,生产环境返回之前的 vnode,以避免渲染谬误导致组件空白 /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } finally { currentRenderingInstance = null } // 如果返回的 vnode 是数组,并且只蕴含了一个元素,则间接打平 if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // render 函数出错时,返回一个空的 vnode if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode}
installRenderHelpers
src/core/instance/render-helpers/index.js
该办法负责在实例上装置大量和渲染相干的简写的工具函数,这些工具函数用在编译器生成的渲染函数中,比方 v-for 编译后的 vm._l,还有大家最相熟的 h 函数(vm._c),不过它没在这里申明,是在 initRender 函数中申明的。
installRenderHelpers 办法是在 renderMixin 中被调用的。
/** * 在实例上挂载简写的渲染工具函数 * @param {*} target Vue 实例 */export function installRenderHelpers (target: any) { target._o = markOnce target._n = toNumber target._s = toString target._l = renderList target._t = renderSlot target._q = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners target._d = bindDynamicKeys target._p = prependModifier}
如果对某个办法感兴趣,能够自行深究。
总结
面试官 问:vm.$set(obj, key, val) 做了什么?
答:
vm.$set 用于向响应式对象增加一个新的 property,并确保这个新的 property 同样是响应式的,并触发视图更新。因为 Vue 无奈探测对象新增属性或者通过索引为数组新增一个元素,比方:
this.obj.newProperty = 'val'
、this.arr[3] = 'val'
。所以这才有了 vm.$set,它是 Vue.set 的别名。- 为对象增加一个新的响应式数据:调用 defineReactive 办法为对象减少响应式数据,而后执行 dep.notify 进行依赖告诉,更新视图
- 为数组增加一个新的响应式数据:通过 splice 办法实现
面试官 问:vm.$delete(obj, key) 做了什么?
答:
vm.$delete 用于删除对象上的属性。如果对象是响应式的,且能确保能触发视图更新。该办法次要用于避开 Vue 不能检测属性被删除的状况。它是 Vue.delete 的别名。
- 删除数组指定下标的元素,外部通过 splice 办法来实现
- 删除对象上的指定属性,则是先通过 delete 运算符删除该属性,而后执行 dep.notify 进行依赖告诉,更新视图
面试官 问:vm.$watch(expOrFn, callback, [options]) 做了什么?
答:
vm.$watch 负责察看 Vue 实例上的一个表达式或者一个函数计算结果的变动。当其发生变化时,回调函数就会被执行,并为回调函数传递两个参数,第一个为更新后的新值,第二个为老值。
这里须要 留神 一点的是:如果察看的是一个对象,比方:数组,当你用数组办法,比方 push 为数组新增一个元素时,回调函数被触发时传递的新值和老值雷同,因为它们指向同一个援用,所以在察看一个对象并且在回调函数中有新老值是否相等的判断时须要留神。
vm.$watch 的第一个参数只接管简略的响应式数据的键门路,对于更简单的表达式倡议应用函数作为第一个参数。
至于 vm.$watch 的外部原理是:
- 设置 options.user = true,标记是一个用户 watcher
- 实例化一个 Watcher 实例,当检测到数据更新时,通过 watcher 去触发回调函数的执行,并传递新老值作为回调函数的参数
- 返回一个 unwatch 函数,用于勾销察看
面试官 问:vm.$on(event, callback) 做了什么?
答:
监听以后实例上的自定义事件,事件可由 vm.$emit 触发,回调函数会接管所有传入事件触发函数(vm.$emit)的额定参数。
vm.$on 的原理很简略,就是解决传递的 event 和 callback 两个参数,将注册的事件和回调函数以键值对的模式存储到 vm._event 对象中,vm._events = { eventName: [cb1, cb2, ...], ... }。
面试官 问:vm.$emit(eventName, [...args]) 做了什么?
答:
触发以后实例上的指定事件,附加参数都会传递给事件的回调函数。
其外部原理就是执行
vm._events[eventName]
中所有的回调函数。备注:从 $on 和 $emit 的实现原理也能看出,组件的自定义事件其实是谁触发谁监听,所以在这会儿再回头看 Vue 源码解读(2)—— Vue 初始化过程 中对于 initEvent 的解释就会明确在说什么,因为组件自定义事件的解决外部用的就是 vm.$on、vm.$emit。
面试官 问:vm.$off([event, callback]) 做了什么?
答:
移除自定义事件监听器,即移除 vm._events 对象上相干数据。
- 如果没有提供参数,则移除实例的所有事件监听
- 如果只提供了 event 参数,则移除实例上该事件的所有监听器
- 如果两个参数都提供了,则移除实例上该事件对应的监听器
面试官 问:vm.$once(event, callback) 做了什么?
答:
监听一个自定义事件,然而该事件只会被触发一次。一旦触发当前监听器就会被移除。
其外部的实现原理是:
- 包装用户传递的回调函数,当包装函数执行的时候,除了会执行用户回调函数之外还会执行
vm.$off(event, 包装函数)
移除该事件 - 用
vm.$on(event, 包装函数)
注册事件
- 包装用户传递的回调函数,当包装函数执行的时候,除了会执行用户回调函数之外还会执行
面试官 问:vm._update(vnode, hydrating) 做了什么?
答:
官网文档没有阐明该 API,这是一个用于源码外部的实例办法,负责更新页面,是页面渲染的入口,其外部依据是否存在 prevVnode 来决定是首次渲染,还是页面更新,从而在调用 \_\_patch\_\_ 函数时传递不同的参数。该办法在业务开发中不会用到。
面试官 问:vm.$forceUpdate() 做了什么?
答:
迫使 Vue 实例从新渲染,它仅仅影响组件实例自身和插入插槽内容的子组件,而不是所有子组件。其外部原理到也简略,就是间接调用
vm._watcher.update()
,它就是watcher.update()
办法,执行该办法触发组件更新。
面试官 问:vm.$destroy() 做了什么?
答:
负责齐全销毁一个实例。清理它与其它实例的连贯,解绑它的全副指令和事件监听器。在执行过程中会调用
beforeDestroy
和destroy
两个钩子函数。在大多数业务开发场景下用不到该办法,个别都通过 v-if 指令来操作。其外部原理是:- 调用 beforeDestroy 钩子函数
- 将本人从老爹肚子里($parent)移除,从而销毁和老爹的关系
- 通过 watcher.teardown() 来移除依赖监听
- 通过 vm.\_\_patch\_\_(vnode, null) 办法来销毁节点
- 调用 destroyed 钩子函数
- 通过
vm.$off
办法移除所有的事件监听
面试官 问:vm.$nextTick(cb) 做了什么?
答:
vm.$nextTick 是 Vue.nextTick 的别名,其作用是提早回调函数 cb 的执行,个别用于
this.key = newVal
更改数据后,想立刻获取更改过后的 DOM 数据:this.key = 'new val'Vue.nextTick(function() { // DOM 更新了})
其外部的执行过程是:
this.key = 'new val'
,触发依赖告诉更新,将负责更新的 watcher 放入 watcher 队列- 将刷新 watcher 队列的函数放到 callbacks 数组中
- 在浏览器的异步工作队列中放入一个刷新 callbacks 数组的函数
- vm.$nextTick(cb) 来插队,间接将 cb 函数放入 callbacks 数组
- 待未来的某个时刻执行刷新 callbacks 数组的函数
- 而后执行 callbacks 数组中的泛滥函数,触发 watcher.run 的执行,更新 DOM
- 因为 cb 函数是在前面放到 callbacks 数组,所以这就保障了先实现的 DOM 更新,再执行 cb 函数
面试官 问:vm._render 做了什么?
答:
官网文档没有提供该办法,它是一个用于源码外部的实例办法,负责生成 vnode。其要害代码就一行,执行 render 函数生成 vnode。不过其中加了大量的异样解决代码。
链接
- 配套视频,微信公众号回复:"精通 Vue 技术栈源码原理视频版" 获取
- 精通 Vue 技术栈源码原理 专栏
- github 仓库 liyongning/Vue 欢送 Star
感激各位的:点赞、珍藏和评论,咱们下期见。
当学习成为了习惯,常识也就变成了常识。 感激各位的 点赞、珍藏和评论。
新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。