这两天面试,或多或少都问些Vue响应式原理,问的我头皮发麻。虽然我手写过,懂和能回答还是两回事。所以
手写响应式的完整代码
Vue2.x
接着上篇 你这手写vue2.x/3.x的响应式原理保熟吗?? 继续做下扩展。
上篇手写实现了
这里给下完整vue的简单手写响应式代码吧
效果
<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>Vue2.x</title>
<style>
.show-txt {
opacity: 1;
transition: all .5s linear;
}
.show-txt.hidden {
opacity: 0;
color: #eee;
}
</style>
</head>
<body>
<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>
<b>姓名:</b><span>{{ name }}</span> <br/>
<b>性别:</b><span>{{ sex }}</span>
<hr />
<div v-text="text"></div>
</div>
<script>
class Dep {
constructor (){
this.subs = []
}
addSub (sub){
if(sub && sub.update) this.subs.push(sub)
}
notify(oldValue){
this.subs.forEach(sub => {
sub.update(oldValue)
})
}
}
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(oldValue){
this.oldVal = oldValue;
let newValue = this.vm.$data[this.key]
if(newValue === this.oldVal) return;
this.cb(newValue)
}
}
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
})
}
else if(typeAttr === "radio") {
new Watcher(this.vm, key, newVal => { node.classList.add('class-'+newVal)})
const nameAttr = node.getAttribute('name')
node.addEventListener('change', (ev) => {
this.vm.$data[key] = ev.target.value
})
}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) })
}
isDirective(attr) {
return attr.startsWith('v-')
}
isTextNode(node){
return node.nodeType === 3
}
isElementNode(node) {
return node.nodeType === 1
}
}
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) {
const oldval = obj[key]
if(newValue === obj[key]) return;
value = newValue;
self.walk(newValue)
dep.notify(oldval)
}
})
}
}
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;
}
})
})
}
}
new Vue({
el: '#app',
data: {
name: 'ethan',
sex: '男',
text: 'text',
show: true,
}
})
</script>
</body>
Vue3.x
<body>
<script>
f1()
function f1() {
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect.fn) 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.fn)
activeEffect.key = dep;
}
function trigger(target, key, { oldValue }) {
let depsMap = targetMap.get(target)
console.log('depsMap: ', depsMap);
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect(oldValue, dep))
}
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log('get: ', target, key, receiver)
track(receiver, key)
return Reflect.get(target, key, receiver)
},
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, { 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 t00() {
const data = { name: '二蛋' }
const rData = reactive(data)
effect(() => {
console.log('我依赖二蛋', rData.name);
})
rData.name = '王二蛋'
}
function ref(initValue) {
return reactive({
value: initValue
})
}
function watchEffect(fn) {
effect(() => fn())
}
function computed(fn) {
const result = ref()
effect(() => result.value = fn())
return result
}
function watch(source, fn) {
let oldValues = new Map(), newValues = new Map(), isArray = false;
function handleEffects(rValue){
effect((oldValue, upKey = null) => {
const newValue = typeof rValue === 'function' ? rValue().value : rValue.value;
if(activeEffect.fn) {
oldValues.set(activeEffect.key, newValue)
newValues.set(activeEffect.key, newValue)
}else {
oldValues.set(upKey, oldValue)
newValues.set(upKey, newValue)
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)
}
function t0(){
const eData = ref(5);
watchEffect(() => { console.log('effect测试: ', eData.value) })
eData.value = 666
}
function t01(){
const wRef = ref(5);
watch(wRef, (value, preValue) => {
console.log('watch监听单源测试:', value, preValue)
})
wRef.value = 66
}
t02()
function t02(){
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
}
function t1(){
const ref1 = ref(5);
const cRef1 = computed(() => {
console.log('computed测试: ', ref1);
return ref1.value
});
ref1.value = 666;
console.log('last: ', ref1, cRef1)
}
}
</script>
</body>
扩展
主要参考 Vue的MVVM实现原理 ?, 原文还配了视频,针不错📌(我改了一下旧值不跟新的问题,代码结构是基于我之前手写的版本)
主要扩展了
再次给一下 vue2.x 响应式的图(来自参考原文),这张图我认为描述的很清晰
为了读起来不那么吃力(大佬请绕路),建议读懂我上一篇文章
思路我就不再细说了,直接给代码,结合注释看吧
效果
全部代码
<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>
<style>
.show-txt {
opacity: 1;
transition: all .5s linear;
}
.show-txt.hidden {
opacity: 0;
color: #eee;
}
</style>
</head>
<body>
<div id="app">
<h2>{{ person.name }} 的 {{ app }}</h2>
<input type="text" v-model="person.name" /><br />
<input id="male" name="sex" type="radio" v-model="person.sex" value="男">
<label for="male"> 男 </label>
</input>
<input id="female" name="sex" type="radio" v-model="person.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>
<b>姓名:</b><span>{{ person.name }}</span> <br />
<b>性别:</b><span>{{ person.sex }}</span>
<hr />
<button v-on:click="onTest">按钮v-on</button>
<button @click="aitTest">按钮@</button><img v-bind:src="imgSrc" v-bind:alt="altTitle">
<div v-text="text"></div>
<div v-html="htmlStr"></div>
<hr/>
</div>
<script>
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
notify(oldValue) {
this.subs.forEach(sub => {
sub.update(oldValue)
})
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
Dep.target = this
this.oldVal = compileUtils.getVal(this.expr, this.vm)
Dep.target = null
}
update(oldValue) {
this.oldVal = oldValue;
let newValue = compileUtils.getVal(this.expr, this.vm)
if (newValue === this.oldVal) return;
this.cb(newValue)
}
}
const compileUtils = {
getVal: (express, vm) => {
return express.split('.').reduce((data, attr) => {
return data[attr.trim()];
}, vm.$data)
},
setVal: (express, vm, value) => {
return express.split('.').reduce((data, attr, index, arr) => {
return index === arr.length -1 ? data[attr.trim()] = value : data[attr.trim()];
}, vm.$data)
},
getMehods: (express, vm) => {
return vm.$options.methods[express].bind(vm)
},
contentUpdate: (node, vm, oldval) => {
let reg = /\{\{(.+?)\}\}/g
const value = oldval.replace(reg, (...args) => {
const key = args[1];
new Watcher(vm, key, (newVal) => {
node.textContent = newVal
})
return compileUtils.getVal(args[1], vm)
})
node.textContent = value
}
}
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
let fragment = this.node2Fragment(this.el)
this.compile(fragment)
this.el.appendChild(fragment)
}
node2Fragment(el){
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment;
}
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 val = node.textContent
if (val.includes('{{')) {
compileUtils.contentUpdate(node, this.vm, val)
}
}
compileElement(node) {
![...node.attributes].forEach(attr => {
let {name, value} = attr;
if (this.isDirective(name)) {
let [,attrName] = name.split('-');
const [drective, eventName] = attrName.split(':');
eventName && this.addEventOrAttr(node, drective, eventName, value)
this.update(node, value, attrName)
node.removeAttribute('v-'+attrName)
}else{
if(/^[@](.+?)$/.test(name)){
this.addEventOrAttr(node, 'on', RegExp.$1, value)
}else if(/^[:](.+?)$/.test(name)){
this.addEventOrAttr(node, 'bind', RegExp.$1, value)
}
}
})
}
addEventOrAttr(node, derective, eventName, express) {
this[derective + 'Update'](node, eventName, express)
}
update(node, key, attrName) {
let updateFn = this[attrName + 'Update']
const value = compileUtils.getVal(key, this.vm)
updateFn && updateFn.call(this, node, key, value)
}
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('input', () => {
compileUtils.setVal(key, this.vm, node.value)
}, false)
} else if (typeAttr === "radio") {
new Watcher(this.vm, key, newVal => {
node.classList.add('class-' + newVal)
})
const nameAttr = node.getAttribute('name')
node.addEventListener('change', (ev) => {
compileUtils.setVal(key, this.vm, ev.target.value)
}, false)
} else if (typeAttr === 'checkbox') {
node.addEventListener('change', (ev) => {
compileUtils.setVal(key, this.vm, ev.target.checked)
}, false)
}
}
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)
})
}
htmlUpdate(node, key, htmlContent) {
node.innerHTML = htmlContent;
new Watcher(this.vm, key, newVal => {
node.innerHTML = htmlContent;
})
}
bindUpdate(node, attr, express) {
new Watcher(this.vm, express, newVal => {
node.setAttribute(attr, newVal)
})
const value = compileUtils.getVal(express, this.vm);
node.setAttribute(attr, value)
}
onUpdate(node, eventName, express){
console.log(eventName, express)
const cbFn = compileUtils.getMehods(express, this.vm)
node.addEventListener(eventName, cbFn)
}
isDirective(attr) {
return attr.startsWith('v-')
}
isTextNode(node) {
return node.nodeType === 3
}
isElementNode(node) {
return node.nodeType === 1
}
}
class Observer {
constructor(vm) {
this.vm = vm;
this.walk(this.vm.$data);
}
walk(data) {
if (!data || typeof data != 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, value) {
const self = this;
self.walk(value);
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
const oldval = compileUtils.getVal(key, self.vm)
if (newValue === oldval) return;
value = newValue;
self.walk(newValue)
dep.notify(oldval)
}
})
}
}
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)
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;
}
})
})
}
}
new Vue({
el: '#app',
data: {
app: 'vueApp',
person: {
name: 'ethan',
sex: '男',
},
text: 'text',
show: true,
htmlStr: '<b>我是html字符串</b>',
imgSrc:'https://js.tuguaishou.com/img/indexhead/people_vip.png'
},
methods: {
onTest(){
this.person.name = '伊森'
console.log('on: ', this.person)
},
aitTest(){
this.imgSrc = 'https://js.tuguaishou.com/img/indexhead/company_vip.png'
console.log('@: ', this.imgSrc)
}
}
})
</script>
</body>
注意
从前面效果图展示可能看出来了,本实现还有个Bug:
对于 <span>{{ person.name }} --- {{ person.age }}<span> ,改变其中一个之后,就会出现 span 内仅有一个值(改变那个),而不是两个值的组合,这点我没找到好的解决方式(可能能通过正则那里每个依赖值给个前后索引值能解决),后面再去看看源码吧。如果各位有好的实现或者思路有希望评论交流。
聊聊 MVVM
阐述一下你所理解的MVVM响应式原理
vue是采用数据劫持配合发布者-订阅者模式 通过 Object.defineProperty() 劫持数据各个属性的 gettter , sertter .在数据发生变化时,发布消息给依赖收集器,通知观察者去触发相应依赖的回调函数,去更新视图。
MVVM作为绑定的入口,整合Observer , Compile , Watcher 三者,通过 Observer 监听model数据变化,通过 Compile 来解析模板指令和动态的内容,最终利用Watcher 将 Observer 与 Compile 联系起来,达到 数据变化 => 视图更新 ,视图交互变化 => 数据model变更的双向绑定结果。
|