vue源码限于水平,看的乱七八糟,只是看了一些文章解析有些收获,遂记。
现在vue3已经出来了,是不是研究vue2.x过时了?非也非也。
抛去大部分vue使用者还在用的情况,vue3的很多思路都可以在vue2中得到体现,所以学习还是有必要的。
这篇文章就聊聊到响应式原理,从百草园到三味书屋,哦不,从 Vue2 到 Vue3。 聊Vue2响应式就必须聊聊 Objet.defineProperty?…
talk is cheap…
Objet.defineProperty
用过 vue 的都知道,vue 如果有个数据 firstName 改变了,那么相应用到 firstName 的地方都得改变, 比如 计算属性(computed )
computed: {
fullName: () => this.firstName + this.lastName
}
数据监听(watch )
watch: {
firstName: function (val, oldVal) {
}
}
模板渲染(render )
<span>姓: {{ firstName }}<span>
这三个地方就相当于 firstName 的依赖(Watcher ),后面细讲。
另外 Vue 中数据定义是这样的
new Vue({
el: '#app',
data: { firstName: '王' }
})
那么怎么才能监听到 fisrtName 的改变并更新相应的数据呢?
在 Object.defineProperty 之前, 是 MutationObserver -MDN ?,他的作用如下
MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品
这是已经淘汰的东西,不多做介绍, vue2.x 做法就是利用 Objet.defineProperty 监听 data 的改变从而去触发各个部分的更新。
做个简单的介绍,可以猛戳 这里? 了解更多. 以上面为例
const data = { firstName: '李' };
const vm = {};
Object.defineProperty(vm, "firstName", {
enumerable: true,
configurable: true,
get() { console.log('我被获取了!'); return data.firstName; },
set(newValue) {
data.firstName = newValue;
console.log('我被改变了, 这是新值:', newValue)
},
});
vm.firstName = '李';
console.log(vm.firstName)
上面给vm 定义一个 响应式 firstName 属性,并且这个属性可枚举(enumerable : true)、可配置(configurable : true)。firstName 改变就就会触发 set 方法,获取 firstName 就会触发 get 方法。
把响应式核心 实现方法说了,再来理一理 vue 响应式实现原理。
vue响应式原理
先上一张图,来自 掘金小册(推荐!) 剖析 Vue.js 内部运行机制?
上面的图基本就很经典的概括了 Vue 的生命周期。包含 初始化、编译、挂载、更新 。。。而Vue的响应式也基本贯穿了整个周期。
另外注意:
- 上图
watcher 不是代码中的 vue 中的 watch ,而是依赖。 - watcher(依赖) 有三类:
computed watcher 、watch watcher 、render watcher ,这里提一嘴,后面再说。
关于响应式原理这张图概括的比较全了👍(看完手写,建议回头再来看看这张图)。但凡看着好的我都采用拿来主义🤪 图来自 图解 Vue 响应式原理?
手写一个vue2.x响应式
本节主要参考 (摘抄)😅 手写一个简易vue响应式带你了解响应式原理 ?
效果 如下(v-model, v-show, v-text {{}})
原版实现
首先回忆一下 Vue 的使用
new Vue({
el: '#app',
data: {
name: 'ethan',
text: 'text',
}
})
让我们由 Vue 类切入
Vue
首先需要定义一个 Vue 类, 需要完成以下几点功能:
- 将 data 变为响应式。
- 能编译模板,识别其中绑定的数据。
对于第一点 利用 Observer 将数据变为响应式,另外为了 在模板 template 中 使用 {{ firstName }} 而不是 {{ data.firsName }} , 那么需要将 data 的属性映射到 Vue 中方便直接调用,利用 _proxyData 方法实现。 对于第二点 专门写个 Compiler 实现。
class Vue {
constructor(options) {
this.$options = options || {}
this.$el = typeof options.el === 'string' ?
document.querySelector(options.el) : options.el;
this.$data = options.data;
this._proxyData(this.$data);
new Observer(this.$data)
new Compiler(this)
}
_proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){ return data[key] },
set (newValue) {
if(newValue === data[key]) return;
data[key] = newValue;
}
})
})
}
}
Observer
再来实现 Observer , 它作用如下:
- 将对象数据每个属性及其子属性变为响应式,
对于这一点, 遍历每个属性将其变为响应式,利用 方法 walk 实现.对于单个属性的响应式,利用 Object.defineProperty 处理, 封装 为 defineReactive 方法。
注意:
- 利用 walk 递归,使data的每一个属性都具有响应性,包含新更改的值
- 每次在获取的时候添加依赖.
- 每次数据更新时通知
Dep 通知(notify )更新
于是有下面代码
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
if(!data || typeof data != 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, value) {
this.walk(value);
const self= this;
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
Dep.target && dep.addSub(Dep.target)
return value
},
set (newValue) {
if(newValue === obj[key]) return;
value = newValue;
self.walk(newValue)
dep.notify()
}
})
}
}
Compiler
接下来 是 模板编译 Compiler 的实现,作用如下
- 解析
Dom 中绑定的数据 - 解析
Dom 中的指令(v-model , v-text 等)
实现上面两点,只需要 把 dom 当成一个字符串 利用正则解析即可。
注意
- 文本节点,元素节点有不同的解析方法,针对性处理
- 对于指令,通过获取 js 获取 Dom 自定义属性即可解决
- 对于input节点需要监听改变。利用
change 事件
其实除了文本节点、元素节点还有很多节点需要针对性处理,想要了解更多,建议查看 vue-design—渲染器之挂载?
下面是 Compiler 的实现
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.compile(this.el)
}
compile(el) {
let childrenNodes = [...el.childNodes]
childrenNodes.forEach(node => {
if(this.isTextNode(node)){
this.compileText(node)
}else if(this.isElementNode(node)) {
this.compileElement(node)
}
if(node.childNodes && node.childNodes.length) this.compile(node)
})
}
compileText(node){
let reg = /\{\{(.+?)\}\}/
let val = node.textContent
if(reg.test(val)){
let key = RegExp.$1.trim()
const value = this.vm[key];
node.textContent = val.replace(reg, value)
new Watcher(this.vm, key, (newVal) => {
node.textContent = newVal
})
}
}
compileElement(node) {
![...node.attributes].forEach(attr => {
let attrName = attr.name
if(this.isDirective(attrName)){
attrName = attrName.substring(2);
let key = attr.value;
this.update(node, key, attrName)
}
})
}
update(node, key, attrName) {
let updateFn = this[attrName+'Update']
updateFn && updateFn.call(this, node, key, this.vm[key])
}
textUpdate(node, key, content ){
node.textContent = content
new Watcher(this.vm, key, newVal => { node.textContent = newVal })
}
modelUpdate(node, key, value) {
const typeAttr = node.getAttribute('type')
if(typeAttr == "text") {
node.value = value;
new Watcher(this.vm, key, newVal => { node.value = newVal})
node.addEventListener('keyup', () => {
this.vm.$data[key] = node.value
})
}
}
isDirective(attr) {
return attr.startsWith('v-')
}
isTextNode(node){
return node.nodeType === 3
}
isElementNode(node) {
return node.nodeType === 1
}
}
可以看到,上面三处添加了 Watcher (Watcher 的实现后面会讲)
- 文本节点更新时(
compileText ), 如 <span>{{ name }}</span> - 自定义指令 v-text 更新时(
textUpdate ), 如<span v-text="name"></span> - 自定义指令 v-modal 更新时(
modelUpdate ), 如<input v-modal="name"></input>
因为在上述三种情况会产生name的依赖(前面提到的 三种Watcher 中的一种 render Watcher )。
Dep
再来说说 Dep 的实现,主要作用
主要是暴露方法 addSub 和 notify 。请看下面👇
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
最后说说 Watcher , 文中说的最多的 依赖 就是它!作用是什么呢?
- 更新 Dep 的 target(值为 Watcher 本身)
- 利用 回调函数
cb 触发更新
代码如下
class Watcher {
constructor(vm, key, cb){
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
this.oldVal = vm[key]
Dep.target = null
}
update(){
let newValue = this.vm.$data[this.key]
if(newValue === this.oldVal) return;
this.cb(newValue)
}
}
还记得吗 ? 在 前面 Observe 定义响应时有这一段代码👇
class Observer{
get(){
Dep.target && dep.addSub(Dep.target)
return value
},
}
每次在实例化 Watcher 的时候就为 Dep 添加 target , 每次执行 data[prop] 的时候 就会进行依赖收集!
好了,终于都写的差不多了。测试一下看看?
测试一下
麻雀虽小,五脏俱全了,不过他们的执行顺序需要做些调整:
<body>
<div id="app">
<input type="text" v-model="name" /><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
<script>
class Dep {
class Watcher {
class Compiler {
class Observer {
class Vue {
new Vue({
el: '#app',
data: {
name: 'ethan',
sex: '男',
text: 'text',
}
})
</script>
</body>
当然,v-text 也是双向绑定,主要是 compileText 的作用。改成 name 试一下
<body>
<div id="app">
<input type="text" v-model="name" /><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="name"></div>
</div>
<script>
</script>
</body>
好像可以哎~~~,哇哦🤸?♂?🤸?♂?🤸?♂?,不得不说我抄写代码有点🤏东西,家人们,点个赞吧🤡 什么?不给?没事😁请继续看下去。
修复原版bug和扩展v-show
Bug发现
上面实现了,我就有个想法,性别通过选择更改可以不可以呢🤷?♂?? 说干就干,首先就要监听选择更改的值,然后将 data.sex 赋值为新值不就好了。
当时心想怪简单的,结果我折腾了好一会🤣,都忘完了。
html
<div id="app">
<input type="text" v-model="name" /><br/>
<input id="male" name="sex" type="radio" v-model="sex" value="男">
<label for="male"> 男 </label>
</input>
<input id="female" name="sex" type="radio" v-model="sex" value="女">
<label for="female"> 女 </label>
</input><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
js
class Compiler {
constructor(vm) {}
compile(el) {}
compileText(node){}
compileElement(node) {}
update(node, key, attrName) {
let updateFn = this[attrName+'Update']
updateFn && updateFn.call(this, node, key, this.vm[key])
}
textUpdate(node, key, content ){}
modelUpdate(node, key, value) {
const typeAttr = node.getAttribute('type')
if(typeAttr == "text") {
}
else if(typeAttr === "radio") {
const nameAttr = node.getAttribute('name')
node.addEventListener('change', (ev) => {
this.vm.$data[key] = ev.target.value
})
}
}
isDirective(attr) {}
isTextNode(node){}
isElementNode(node) {}
}
可以看到,第一次切换 "女" 时更新,后面的切换都不更新了?为啥,各位同志们可以先想想💭。
不卖关子了,原因就是 旧值 this.oldVal 是在 new Watcher 时确定,后面在更新"男"=> "女" 时,自然更新 ;当再选择 "男" ,上次选择 "女" 时Watcher 中的 this.oldVal 并没有更新,所以值相同不会触发 Watcher ,具体代码在 Watcher 的 update 方法内体现。
当再选择 "女" 虽然触发了 Watcher , 但是看起来也没有改变(原来的值就为 "女" )
找到原因就好改了。在 notify 中将旧值传入,在 Watcher 的 update 中接收并更新 this.oldVal ,具体就是
class Dep {
notify(oldValue){
this.subs.forEach(sub => {
sub.update(oldValue)
})
}
}
class Watcher {
update(oldValue){
this.oldVal = oldValue;
let newValue = this.vm.$data[this.key]
if(newValue === this.oldVal) return;
this.cb(newValue)
}
}
class Observer {
defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
set (newValue) {
const oldval = obj[key]
if(newValue === obj[key]) return;
value = newValue;
self.walk(newValue)
dep.notify(oldval)
}
})
}
}
这样就可以了。
扩展v-show
有了前面的实现, v-show 实现起来就简单了,主要监听输入,更新值即可。
css
<style>
.show-txt {
opacity: 1;
transition: all .5s linear;
}
.show-txt.hidden {
opacity: 0;
color: #eee;
}
</style>
html
<div id="app">
<input type="text" v-model="name" /><br/>
<input id="male" name="sex" type="radio" v-model="sex" value="男">
<label for="male"> 男 </label>
</input>
<input id="female" name="sex" type="radio" v-model="sex" value="女">
<label for="female"> 女 </label>
</input><br/>
<input name="show" type="checkbox" v-model="show" checked>是否展示</input>
<div class="show-txt" v-show="show">展示文本示例</div><br/>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
js
class Compiler {
modelUpdate(node, key, value) {
const typeAttr = node.getAttribute('type')
if(typeAttr == "text") {}
else if(typeAttr === "radio") {}
else if(typeAttr === 'checkbox') {
node.addEventListener('change', (ev) => {
this.vm.$data[key] = ev.target.checked
})
}
}
showUpdate(node, key, value){
const change = (val) => {
const operate = !!val ? 'remove' : 'add';
node.classList[operate]('hidden')
}
change(value);
new Watcher(this.vm, key, (newVal) => { change(newVal) })
}
}
new Vue({
el: '#app',
data: {
name: 'ethan',
sex: '男',
text: 'text',
show: true,
}
})
好了目前就实现到到这里了,Vue2的响应式原理手写实现告一段落。如果你更倾向源码去寻找答案 推荐 Vue 源码解读(3)—— 响应式原理 ?
当然上面只是对象属性的更新,Vue源码对数组的更新检测利用重写原型的方法。下面稍作介绍。
数组检测
以下八种数组方法,vue是可以检测其改变的,
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
其做法就是在数组原型拦截这些方法,可从源码窥其一二。
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
ob.dep.notify();
return result
});
});
扩展:Vue3的响应式实现
Vue3对Vue2来说大大的升级了,本文只探究响应式原理,这里大概总结一下。
Vue3响应式原理
先来个对比。
Vue2.x
- 基于
Object.defineProperty ,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。 Object.defineProperty 无法检测到对象属性的添加和删除 。- 由于Vue会在初始化实例时对属性执行
getter/setter 转化,所有属性必须在 data 对象上存在才能让 Vue 将它转换为响应式。 - 深度监听需要一次性递归,对性能影响比较大。
Vue3 - 基于 Proxy 和 Reflect ,可以原生监听数组,可以监听对象属性的添加和删除。 - 不需要一次性遍历 data 的属性,可以显著提高性能。 - 因为 Proxy 是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。
对于Vue3的响应式,网上?找到一张图,总结比较贴切。
关于 Proxy
相比 Object.defineProperty ,Proxy? 支持的对象操作十分全面:get 、set 、has 、deleteProperty 、ownKeys 、defineProperty …等等
掘金不少讲的好的文章 🏸- proxy,不再赘述,给一个例子,可以带着好奇去对 Proxy 扫盲。
let data = [1, 2, 3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
Reflect.set(target, key, value, receiver)
return true
}
})
p.length = 4
console.log(data)
p.shift()
p.push(4)
p
p[3] = 0
手写响应式
我要开始动手了!什么?动手?不是不是,亲爱读者们,我是说我要动手开代码了
主要的函数如下:
仅涉及 Composition API 响应式原理实现,没有涉及到编译部分(Dom部分)
function track(target, key){}
function trigger(target, key){}
function reactive(obj){}
function effect(cb){}
限于篇幅且代码也不多,这里就直接给代码了,请参考注释阅读。
代码来自(建议精读): 林三心画了8张图,最通俗易懂的Vue3响应式核心原理解析?
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
dep.add(activeEffect)
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(receiver, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key)
}
}
return new Proxy(target, handler)
}
let activeEffect = null
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null
}
上面代码如果理解比较吃力,建议查看上面提到 原文 👆以及文章 Vue3响应式原理 + 手写reactive?(看完记得回来,我后面有一些高质量扩展给到大家🤡),讲的比较细。
主要用了 Proxy 劫持数据(替代 Object.defineProperty), 其他方面比如依赖收集、触犯更新实现和 Vue2 基本一样。
测试一下
const data = { name: '二蛋' }
const rData = reactive(data)
effect(() => {
console.log('我依赖二蛋', rData.name);
})
rData.name = '王二蛋'
除了上面的 reactive ,还有个 ref , 是针对单个变量响应式设计的。另外 reactive 实现了,ref 其实是 基于reactive 的扩展, 仅 属性 value 为响应式!
function ref(initValue) {
return reactive({
value: initValue
})
}
基于上面代码,其它的响应式 API? computed 、 watchEffect 、 watch 也可以实现了。
function watchEffect(fn) {
effect(() => fn())
}
const eData = ref(5);
watchEffect(() => { console.log('effect测试: ', eData.value) })
eData.value = 666
}
function computed(fn) {
const result = ref()
effect(() => result.value = fn())
return result
}
const ref1 = ref(5);
const cRef1 = computed(() => {
console.log('computed测试: ', ref1);
return ref1.value
});
ref1.value = 666;
console.log('last: ', ref1, cRef1)
支持监听单个值和多个值,并且新旧值都要回传,另外要注意的是 监听多个源时 某一个 更新,要触发回调 fn 从而精确更新(map实现)。当然前面的代码要稍作修改,具体如下:
function track(target, key) {
if (!activeEffect.fn) return
dep.add(activeEffect.fn)
activeEffect.key = dep;
}
function trigger(target, key, { oldValue }) {
dep.forEach(effect => effect(oldValue, dep))
}
function reactive(target) {
const handler = {
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
if(value === oldValue) return;
const result = Reflect.set(target, key, value, receiver)
trigger(receiver, key, { newValue: value, oldValue})
return result
}
}
return new Proxy(target, handler)
}
let activeEffect = {fn: null, key: null}
function effect(fn) {
activeEffect.fn = fn
activeEffect.fn()
activeEffect = {fn: null, key: null}
}
function watch(source, fn) {
let oldValues = new Map(), newValues = new Map(), isArray = false;
function handleEffects(rValue){
console.log('start values: ', newValues.values(), oldValues.values());
effect((oldValue, upKey = null) => {
console.log('~~~~', rValue);
const newValue = typeof rValue === 'function' ? rValue().value : rValue.value;
console.log('values: ', newValue, oldValue, newValues.values(), oldValues.values(), activeEffect.key);
if(activeEffect.fn) {
oldValues.set(activeEffect.key, newValue)
newValues.set(activeEffect.key, newValue)
}else {
console.log(newValue, oldValue);
oldValues.set(upKey, oldValue)
newValues.set(upKey, newValue)
console.log('last values: ', newValues.values(), oldValues.values());
isArray
? fn([[...newValues.values()], [...oldValues.values()]])
: fn([...newValues.values()][0], [...oldValues.values()][0]);
}
})
}
if(Array.isArray(source)) {
isArray = true;
source.forEach(rValue => {
handleEffects(rValue)
})
}
else
handleEffects(source)
}
测试一下
const wRef = ref(5);
watch(wRef, (value, preValue) => {
console.log('watch监听单源测试:', value, preValue)
})
wRef.value = 66
const wRef = ref(1);
const wRef1 = ref(2);
const wRef2 = ref(3);
watch([wRef, () => wRef1, wRef2], (values, preValues) => {
console.log('watch监听多源测试:', values, preValues)
})
wRef.value = 11;
wRef1.value = 22;
wRef2.value = 33
wRef.value = 111
wRef2.value = 333
简版 watch 基本就实现了,仅支持 ref , reactive 的数据会有问题,各位可以想想哪里有问题。
总结
vue2 和vue3 响应基本思路都差不多。收集依赖 ,数据源改变时更新依赖 。
写完感觉自己越来越无知,吾生也有涯,而知也无涯。
另外看起来可能好理解,建议还是动动手吧。
纸上得来终觉浅,绝知此事要躬行!
限于技术和写文章水平,行文有误欢迎👐大家指出。
参考
站在别人肩膀上才能看的更远,感谢 💖💖💖 文章中所有参考链接文章作者的分享。
🔗 | 掘金小册 - 剖析 Vue.js 内部运行机制 🔗 | 手写一个简易vue响应式带你了解响应式原理 🔗 | VUE源码相关面试题汇总 🔗 | Vue3为什么选择Proxy做双向绑定? 🔗 | Vue 3 响应式原理及实现 🔗 | 浅析Vue3中的响应式原理
|