深入理解响应式设计
svelte svelte
<script>
let count = 1
function handleIncrease() {
count++
}
$: doubleCount = count * 2
</script>
<div>{count}</div>
<div>{doubleCount}</div>
<button on:click={handleIncrease}>+</button>
不用this.data 原理就是静态编译,纯函数
Vue2 响应式
vue 响应式要对依赖进行依赖收集,本质是对data 进行拦截 -> ObjectDefineProperty 操作 get set
export class Vue {
constructor(options = {}) {
// 值挂载实例上
this.$options = options
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
this.$data = options.data
this.$methods = options.methods
this.proxy(this.$data)
// Observer 拦截 this$data
new Observer(this.$data)
}
// 代理 $this.data -> $this.$data.xxx -> this.xxx 属性值代理到实例上
// data: { count: 0 }; this.$data.count++ -> this.count++
proxy(data) {
Object.keys(data).forEach(key => {
// this
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
// 相当于 this.data.count 直接拿到this.count
return data[key]
},
set(newVal) {
// NaN !== NaN
if (data[key] === newVal || __isNaN(data[key], newVal)) return
data[key] = newVal
}
})
})
}
}
function __isNaN(a, b) {
return Number.isNaN(a) && Number.isNaN(b)
}
Observer 数据拦截
Vue2 递归地使用defineProperty
edge case: array
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 指针
let _this = this
this.walk(value) // 因为值也可能是对象 {a: {b: 12}}
// 对象的每个属性 做劫持,可以通过get set控制属性
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() { // 调用 this.count 会触发
return value
},
set() { // 调用 this.count++ 会触发
// NaN
if (value === newVal || __isNaN(value, newVal)) return
value = newValue
// 值可能是对象
_this.walk(newValue)
}
})
}
}
所谓响应式:如 有个 data: { count: 0 },操作this.count++ 页面都会响应
目前对整个属性做了劫持,比如不论访问还是赋值 count 都会被拦截到
-> 发布订阅模式:触发get,知道可能会用哪个值,收集起来;当赋值count时,之前订阅与count有关的更改都应该触发
收集依赖,触发
class Dep {
constructor() {
this.deps = new Set()
}
// 收集副作用代码,页面依赖 count 渲染的代码 - 副作用
add(dep) {
if (dep && dep.update) this.deps.add(dep)
}
// 触发
notify(){
this.deps.forEach(dep => dep.update())
}
}
在Observer 中 收集
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 指针
let _this = this
this.walk(value) // 因为值也可能是对象 {a: {b: 12}}
// 收集依赖
let dep = new Dep()
// 对象的每个属性 做劫持,可以通过get set控制属性
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() { // 调用 this.count 会触发
// 收集
Dep.target && dep.add(Dep.target)
return value
},
set() { // 调用 this.count++ 会触发
// NaN
if (value === newVal || __isNaN(value, newVal)) return
value = newValue
// 值可能是对象
_this.walk(newValue)
// 通知
dep.notify()
}
})
}
}
target 为 Watcher 的实例 观察count,对应有个 update 方法,劫持count 发生的变化,把变化渲染到页面(监听的副作用)
class Watcher { //在编译html的时候用
// cb key 变了之后回调方法,页面重新渲染,或者 diff -> patch
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb // 今天的例子中就是绘制数据到页面
Dep.target = this
this.__old = vm[key] // 当前的count 的值,存下,触发了 getter
Dep.target = null // 防止溢出
}
update() {
let newValue = this.vm[this.key]
if (this.__old === newValue || __isValue(newValue, this.__old)) return
this.cb(newValue)
}
}
整体过程:
html 字符串 -> <h1> {{ count }} </h1>
-> compiler 解析时发现有 {{ count }} 在h1节点 new 一个Watcher
-> new Watcher(vm, 'count', () => renderToView(count))
new Watcher 的时候会走
Dep.target = this
this.__old = vm[key]
Dep.target = null
-> 触发 count getter -> getter 触发 会走 dep.add(watcher实例) -> this.count++ -> cont setter -> dep.notify -> () => renderToView(count)) -> 页面就变了
再来写 compiler
export class Vue {
constructor(options = {}) {
// 值挂载实例上
this.$options = options
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
this.$data = options.data
this.$methods = options.methods
this.proxy(this.$data)
// Observer 拦截 this$data
new Observer(this.$data)
// complier
new Complier(this) // 绑定this
}
// Complier
class Complier {
constructor(vm) {
this.el = vm.el // 拿到dom节点
this.vm = vm
this.methods = vm.$methods
this.compile(vm.$el)
}
compile(el) {
let childNodes = el.childNodes
// 类数组 - 转一下数组
Array.from(childNodes).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)
// ..
})
}
// <div v-mode='msg'>
compileElement(node) {
if (node.attributes.length) {
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-on: click v-model
attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) // 'click' : 'model'
let key = attr.value
this.update(node, key, attrName, this.vm[key])
}
})
}
}
update(node, key, attrName, value) {
if (attrName === 'text') {
node.textContent = value
new Watcher(this.vm, key, val => node.textContent = val)
} else if (attrName === 'model') {
node.value = value
new Watcher(this.vm, key, val => node.value = val)
node.addEventListener('input' => {
this.vm[key] = node.value
})
} else if (attrName === 'click') {
node.addEventListener(attrName, this.methods[key].bind(this.vm))
}
//...
}
// 'this is {{ count }}'
compileText(node) {
let reg = /\{\{(.*?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
new Watcher(this.vm, key, val => {
node.textContent = val
})
}
}
// 判断指令
isDirective(str) {
return str.startsWith('v-')
}
// 是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
// 是否是文本节点
isTextNode(node) {
return node.nodeType === 3
}
}
实现双向绑定:
(attrName === 'model') {
node.value = value
// 绑定了字段
new Watcher(this.vm, key, val => node.value = val)
// 输入时候,修改vm值
node.addEventListener('input' => {
this.vm[key] = node.value
})
}
html 文件:
<script type='module'> 因为要有ES6 语法
<!DOCTYPE html>
<html lang="en">
<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>
<script type='module'>
import { Vue } from './index.js'
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue2.x',
count: 666
},
methods: {
increase() {
this.count++
}
}
})
</script>
</head>
<body>
<div id="app">
<h3>{{ msg }}</h3>
<h3>{{ count }}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg" >
<input type="text" v-model="count">
<button v-on:click="increase">按钮</button>
</div>
</body>
</html>
Vue3
// 判断是否是对象
function isObject (data) {
return data && typeof data === 'object'
}
// 收集过程
let targetMap = new WeakMap()
let activeEffect
/**
* {
* target: {
* key: [effect, effect, effect, effect]
* }
* }
*/
function track(target, key) { // dep.add
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()))
if (!dep.has(activeEffect)) dep.add(activeEffect) // Dep.target && dep.add(Dep.target)
}
// 通知过程
function trigger(target, key) { // dep.notify
const depsMap = targetMap.get(target)
if (!depsMap) return
depsMap.get(key).forEach(e => e && e())
}
// 收集副作用 -> 收集的时间(getter)-> 触发副作用执行(setter)
function effect(fn, options = {}) { // compiler + watcher
const __effect = function(...args) {
activeEffect = __effect
return fn(...args) // this.cb()
}
if (!options.lazy) {
__effect()
}
return __effect
}
// 响应式
/*
const a = reactive({ count: 0 })
a.count++
*/
// reactive 返回代理对象
export function reactive(data) {
if (!isObject) return
return new Proxy(data, {
// 同样数据拦截
get(target, key,receiver) {
// 反射target[key] -> 继承情况关系下有坑
// Reflect.get 获取对象身上某个属性的值,类似于 target[name]
const ret = Reflect.get(target, key, receiver)
// TODO 依赖收集
track(target, key)
return isObject(ret) ? reactive(ret) : ret
},
set(target, key, val, receiver) {
Reflect.set(target, key, val, receiver)
// TODO 通知
trigger(target, key)
return true
},
deleteProperty() {
const ret = Reflect.deleteProperty(target, key, receiver)
// TODO 通知
trigger(target, key)
return ret
}
})
}
// proxy 对于基本类型没办法
// 因此需要 ref 方法,将基类型包装成对象
// 基本类型
/**
* const count = ref(0)
* count.value++
*/
export function ref(target) {
let value = target
const obj = {
get value() {
track(obj, 'value')
return value
},
set value(newValue) {
if (value === newValue) return
value = newValue
trigger(obj, 'value')
}
}
return obj
}
export function computed(fn) {//只考虑函数的情况
// 延迟计算 const c = computed(() => `${count.value} + !!!!`); c.value
let __computed
const run = effect(fn, { lazy: true })
__computed = {
get value() {
return run()
}
}
return __computed
}
export function mount(instance, el) {
effect(function() {
instance.$data && update(instance, el)
})
instance.$data = instance.setup()
update(instance, el)
function update(instance, el) {
el.innerHTML = instance.render()
}
}