Vue响应式设计思路
Vue响应式主要包含:
- 数据响应式
- 监听数据变化,并在视图中更新
- Vue2使用
Object.defineProperty 实现数据劫持 - Vu3使用
Proxy 实现数据劫持 - 模板引擎
- 提供描述视图的模板语法
- 插值表达式
{{}} - 指令
v-bind , v-on , v-model , v-for ,v-if - 渲染
- 将模板转换为html
- 解析模板,生成
vdom ,把vdom 渲染为普通dom
数据响应式原理
数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty 实现数据变化的检测
原理解析
new Vue() ?先执?初始化,对data 执?响应化处理,这个过程发?在Observer 中- 同时对模板执?编译,找到其中动态绑定的数据,从
data 中获取并初始化视图,这个过程发?在 Compile 中 - 同时定义?个更新函数和
Watcher实例 ,将来对应数据变化时,Watcher会调?更新函数 - 由于
data 的某个key 在?个视图中可能出现多次,所以每个key 都需要?个管家Dep来管理多个 Watcher - 将来
data 中数据?旦发?变化,会?先找到对应的Dep ,通知所有Watcher 执?更新函数
一些关键类说明
CVue :自定义Vue类 Observer :执?数据响应化(分辨数据是对象还是数组) Compile :编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher :执?更新函数(更新dom) Dep :管理多个Watcher实例,批量更新
涉及关键方法说明
observe : 遍历vm.data 的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例 进行真正响应式处理
html页面
<!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>cvue</title>
<script src="./cvue.js"></script>
</head>
<body>
<div id="app">
<p>{{ count }}</p>
</div>
<script>
const app = new CVue({ el: '#app', data: { count: 0
} }) setInterval(() => { app.count +=1
}, 1000); </script>
</body>
</html>
CVue
- 创建基本CVue构造函数:
- 执?初始化,对
data 执?响应化处理
class CVue {
constructor(options) {
this.$options = options
this.$data = options.data
observe(this.$data)
}
}
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if(val !== newVal) {
console.log(`set ${key}:${newVal}, old is ${val}`)
val = newVal
observe(val)
}
}
})
}
function observe(obj) {
if(typeof obj !== 'object' || obj == null) {
return
}
new Observe(obj)
}
class Observe {
constructor(obj) {
if(Array.isArray(obj)) {
} else {
this.walk(obj)
}
}
walk(obj) {
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}
为vm.$data做代理
方便实例上设置和获取数据
例如
原本应该是
vm.$data.count
vm.$data.count = 233
代理之后后,可以使用如下方式
vm.count
vm.count = 233
给vm.$data做代理
class CVue {
constructor(options) {
observe(this.$data)
proxy(this)
}
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(newVal) {
vm.$data[key] = newVal
}
})
})
}
参考 前端手写面试题详细解答
编译
初始化视图
根据节点类型进行编译
class CVue {
constructor(options) {
proxy(this)
new Compile(this, this.$options.el)
}
}
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
if(this.$el) {
this.complie(this.$el)
}
}
complie(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
} else if(this.isInterpolation(node)) {
console.log(`编译插值文本 ${node.nodeName}`)
}
if(node.childNodes) {
this.complie(node)
}
})
}
isElement(node) {
return node.nodeType === 1
}
isInterpolation(node) {
return node.nodeType === 3
&& /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值表达式
class Compile {
complie(el) {
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
} else if(this.isInterpolation(node)) {
this.complieText(node)
}
})
}
isInterpolation(node) {
return node.nodeType === 3
&& /\{\{(.*)\}\}/.test(node.textContent)
}
complieText(node) {
const exp = String(RegExp.$1).trim()
node.textContent = this.$vm[exp]
}
}
编译元素节点和指令
需要取出指令和指令绑定值 使用数据更新视图
class Compile {
complie(el) {
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
this.complieElement(node)
}
})
}
isElement(node) {
return node.nodeType === 1
}
complieElement(node) {
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const { name: attrName, value: exp } = attr
if(this.isDirective(attrName)) {
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
}
isDirective(attrName) {
return attrName.startsWith('c-')
}
text(node, exp) {
node.textContent = this.$vm[exp]
}
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
}
以上完成初次渲染,但是数据变化后,不会触发页面更新
依赖收集
视图中会?到data中某key,这称为依赖。 同?个key可能出现多次,每次出现都需要收集(??个Watcher来维护维护他们的关系),此过程称为依赖收集。 多个Watcher 需要?个Dep 来管理,需要更新时由Dep 统?通知。
- data中的key和dep是一对一关系
- 视图中key出现和Watcher关系,key出现一次就对应一个Watcher
- dep和Watcher是一对多关系
实现思路
- 在
defineReactive 中为每个key 定义一个Dep实例 - 编译阶段,初始化视图时读取key, 会创建
Watcher实例 - 由于读取过程中会触发key的
getter 方法,便可以把Watcher实例 存储到key对应的Dep实例 中 - 当key更新时,触发setter方法,取出对应的
Dep实例 ,Dep实例 调用notiy 方法通知所有Watcher更新
定义Watcher类
监听器,数据变化更新对应节点视图
class Watcher {
constructor(vm, key, updateFn) {
this.$vm = vm
this.$key = key
this.$updateFn = updateFn
}
update() {
this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
complieText(node) {
const exp = String(RegExp.$1).trim()
this.update(node, exp, 'text')
}
text(node, exp) {
this.update(node, exp, 'text')
}
html(node, exp) {
this.update(node, exp, 'html')
}
update(node, exp, dir) {
const fn = this[`${dir}Updater`]
fn && fn(node, this.$vm[exp])
new Watcher(this.$vm, exp, function(newVal) {
fn && fn(node, newVal)
})
}
textUpdater(node, value) {
node.textContent = value
}
htmlUpdater(node, value) {
node.innerHTML = value
}
}
定义Dep类
- data的一个属性对应一个Dep实例
- 管理多个
Watcher 实例,通知所有Watcher 实例更新
class Dep {
constructor() {
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this
this.$vm[key]
Dep.target = null
}
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
observe(val)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if(val !== newVal) {
val = newVal
observe(val)
dep.notify()
}
}
})
}
监听事件指令@xxx
- 在创建vue实例时,需要缓存
methods 到vue实例上 - 编译阶段取出methods挂载到Compile实例上
- 编译元素时
- 识别出
v-on 指令时,进行事件的绑定 - 识别出
@ 属性时,进行事件绑定 - 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用
bind 修改监听函数的this指向为组件实例
class CVue {
constructor(options) {
this.$methods = options.methods
}
}
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
this.$methods = vm.$methods
}
complieElement(node) {
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const { name: attrName, value: exp } = attr
if(this.isDirective(attrName)) {
if(this.isEventListener(attrName)) {
const eventType = attrName.substring(5)
this.bindEvent(eventType, node, exp)
}
} else if(this.isEventListener(attrName)) {
const eventType = attrName.substring(1)
this.bindEvent(eventType, node, exp)
}
})
}
isEventListener(attrName) {
return attrName.startsWith('@') || attrName.startsWith('c-on')
}
bindEvent(eventType, node, exp) {
const method = this.$methods[exp]
node.addEventListener(eventType, method.bind(this.$vm))
}
}
v-model双向绑定
实现v-model 绑定input 元素时的双向绑定功能
class Compile {
model(node, exp) {
this.update(node, exp, 'model')
node.addEventListener('input', (e) => {
const { value } = e.target
this.$vm[exp] = value
})
}
modelUpdater(node, value) {
node.value = value
}
}
数组响应式
- 获取数组原型
- 数组原型创建对象作为数组拦截器
- 重写数组的7个方法
const originProto = Array.prototype
const arrayProto = Object.create(originProto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
arrayProto[method] = function() {
originProto[method].apply(this, arguments)
console.log(`method:${method} value:${Array.from(arguments)}`)
}
})
class Observe {
constructor(obj) {
if(Array.isArray(obj)) {
obj.__proto__ = arrayProto
this.observeArray(obj)
} else {
this.walk(obj)
}
}
observeArray(items) {
items.forEach(item => observe(item))
}
}
|