原文链接:https://ssshooter.com/2021-07-15-how-does-vue-work-1/
几年来看了不少 vue 原理的文章,在这些文章的帮助下,我也多次尝试自己理解 Vue 的源码,终于,我觉得是时候自己输出一下内容了,希望可以从不同于其他文章的角度带大家熟悉 Vue。
这个专题自然是分多个部分讲解 Vue 源码,第一篇就先讲最最经典的 Vue 响应式原理吧!
在正式讲原理之前,我觉得应该首先讲明白下面几个概念吧。
var Dep = function Dep() {
this.id = uid++
this.subs = []
}
Dep 的含义,自然就是 dependency(也就是依赖,一个计算机领域的名词)。
就像编写 node.js 程序,常会使用 npm 仓库的依赖。在 Vue 中,依赖具体指的是响应式处理后的数据。后面会提到,响应式处理的关键函数之一是在很多 Vue 原理文章都会提到的 definereactive。
Dep 与每个响应式数据绑定后,该响应式数据就会成为一个依赖(名词),下面介绍 Watcher 时会提到,响应式数据可能被 watch、computed、在模板中使用 3 种情况依赖(动词)。
Dep 对象下有一个 subs 属性,是一个数组,很容易猜出,就是 subscriber(订阅者)列表的意思咯。订阅者可能是 watch 函数、computed 函数、视图更新函数。
Watcher 是 Dep 里提到的订阅者(不要和后面的 Observer 观察者搞混)。
因为 Watcher 的功能在于及时响应 Dep 的更新,就像一些 App 的订阅推送,你(Watcher)订阅了某些资讯(Dep),资讯更新时会提醒你阅读。
与 Dep 拥有 subs 属性类似,Watcher 对象也有 deps 属性。这样构成了 Watcher 和 Dep 就是一个多对多的关系,互相记录的原因是当一方被清除的时候可以及时更新相关对象。
上面多次提到的 watch、computed、渲染模板产生 Watcher,在 Vue 源码里都有简明易懂的体现:
Observer 是观察者,他负责递归地观察(或者说是处理)响应式对象(或数组)。在打印出的实例里,可以注意到响应式的对象都会带着一个 __ob__,这是已经被观察的证明。观察者没有上面的 Dep 和 Watcher 重要,稍微了解下就可以了。
Observer.prototype.walk 是 Observer 初始化时递归处理的核心方法,不过此方法用于处理对象,另外还有 Observer.prototype.observeArray 处理数组。
按照上面几个概念的关系,如何搭配,该如何实现数据响应式更新?
首先定下我们的目标:自然是在数据更新时,自动刷新视图,显示最新的数据。
这就是上面提到的 Dep 和 Watcher 的关系,数据是 Dep,而 Watcher 触发的是页面渲染函数(这是最重要的 watcher)。
但是新问题随之而来,Dep 怎么知道有什么 Watcher 依赖于他?
Vue 采用了一个很有意思的方法:
上述逻辑就在 defineReactive 函数中。这个函数入口不少,这里先讲比较重要的 observe 函数。
在 observe 函数中会 new Observer 对象,其中使用 Observer.prototype.walk 对对象中的值进行逐个响应式处理,使用的就是 defineReactive 函数。
因为 defineReactive 函数太重要了,而且也不长,所以直接贴到这边讲比较方便。
function defineReactive(obj, key, val, customSetter, shallow) {
var dep = new Dep()
depsArray.push({ dep, obj, key })
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val
// 后半部分诡异的条件是用于判断新旧值都是 NaN 的情况
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// customSetter 用于提醒你设置的值可能存在问题
if ('development' !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
},
})
}
首先每个响应式的值都是一个“依赖",所以第一步我们先借闭包的能力给每个值造一个 Dep。(到 Vue 3 就不需要闭包啦)
接着看核心的三个参数:
这个值还可能之前就定义了自己的 getter、setter,所以在做 Vue 的响应式处理时先处理原本的 getter、setter。
上面在核心流程中提到在 getter 函数会建立 Dep 和 Watcher 的关系,具体来说依靠的是 dep.depend()。
下面贴一下 Dep 和 Watcher 互相调用的几个方法:
Dep.prototype.depend = function depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub)
}
通过这几个函数,可以领略到了 Dep 和 Watcher 错综复杂的关系……不过看起来迂回,简单来说,其实做的就是上面说的互相添加到多对多列表。
你可以在 Dep 的 subs 找到所有订阅同一个 Dep 的 Watcher,也可以在 Watcher 的 deps 找到所有该 Watcher 订阅的所有 Dep。
但是里面还有一个隐藏问题,就是 Dep.target 怎么来呢?先放一放,后会作出解答。先接着看看 setter 函数,其中的关键是 dep.notify()。
Dep.prototype.notify = function notify() {
// stabilize the subscriber list first
var subs = this.subs.slice()
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
不难理解,就是 Dep 提醒他的订阅者列表(subs)里的所有人更新,所谓订阅者都是 Watcher,subs[i].update() 调用的也就是 Watcher.prototype.update。
那么来看一下 Watcher 的 update 做了什么——
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
在这里我觉得有两个点比较值得展开,所以挖点坑
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(
e,
this.vm,
'callback for watcher "' + this.expression + '"'
)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
这段代码的重点在于需要现在 get 方法中对 Dep.target 进行了设置。
因为只有 Dep.target 存在,之后在回调函数 cb(例如页面渲染函数就是一个典型的 Watcher cb)调用时,Dep.prototype.depend 才能真正生效。再之后的逻辑,就回到使用响应式数据的取值,一切都连起来了!形成闭环(滑稽)!这就是上面 depend() 遗留问题的答案。
虽说粗略来说这个算法并不难理解,但实际上还有许多其他机制与这个算法一起协作,组成完整的 Vue。例如上面挖的坑:更新队列和组件更新的函数本身的实现,都值得学习。
VueJS 实际开发中会遇到的问题,主要写一些 官方手册 上没有写,但是实际开发中会遇到的问题,需要一定知识基础。
Vue.js是一套构建用户界面的渐进式的前端框架。 vueJS与后台交互数据的方法我所了解的有以下几种
Vue是一套构建用户界面的JS渐进式框架。 Vue 只关注视图层, 采用自底向上增量开发的设计。讲解js高级之响应式、过渡效果、过渡状态。
深入理解Vue.js响应式原理。Vue教程有关的视频都讲到,我习惯响应式开发,在更早的Angular1时代,我们叫它:数据绑定(Data Binding)。你只需要在Vue实例的 data() 块中定义一些数据,并绑定到HTML
在vue组件中,为了使样式私有化(模块化),不对全局造成污染,可以在style标签上添加scoped属性以表示它的只属于当下的模块,这是一个非常好的举措,但是为什么要慎用呢?因为scoped往往会造成我们在修改公共组件(三方库或者项目定制的组件)的样式困难,需要增加额外的工作量
vue现在使用的人越来越多了,这篇文章主要整理一些比较优秀的移动端ui框架,推荐给大家,例如:mint UI、vux、vonic、vant、cube-ui、Muse-ui、Vue-Carbon、YDUI等
webpack是开发Vue单页应用必不可少的工具,它能管理复杂的构建步骤,并且优化你的应用大小和性能, 使你的开发工作流更加简单。在这篇文章中,我将解释使用webpack提升你的Vue应用的4种方式,包括:单文件组件、优化Vue构建过程、浏览器缓存管理、代码分离
Vue-Access-Control是一套基于Vue/Vue-Router/axios 实现的前端用户权限控制解决方案,通过对路由、视图、请求三个层面的控制,使开发者可以实现任意颗粒度的用户权限控制。
Web 中的组件其实就是页面组成的一部分,具有高内聚性,低耦合度,互冲突等特点,有利于提高开发效率,方便重复使用,简化调试步骤等。vue 中的组件是一个自定义标签形式,扩展原生的html元素,封装可重用的代码。
Vue的实例是Vue框架的入口,其实也就是前端的ViewModel,它包含了页面中的业务逻辑处理、数据模型等,当然它也有自己的一系列的生命周期的事件钩子,辅助我们进行对整个Vue实例生成、编译、挂着、销毁等过程进行js控制。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!