共计 8309 个字符,预计需要花费 21 分钟才能阅读完成。
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
执⾏响应化解决
// 自定义 Vue 类 | |
class CVue {constructor(options) { | |
this.$options = options | |
this.$data = options.data | |
// 响应化解决 | |
observe(this.$data) | |
} | |
} | |
// 数据响应式, 批改对象的 getter,setter | |
function defineReactive(obj, key, val) { | |
// 递归解决,解决 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 | |
// 持续进行响应式解决,解决 newVal 是对象状况 | |
observe(val) | |
} | |
} | |
}) | |
} | |
// 遍历 obj,对其所有属性做响应式 | |
function observe(obj) { | |
// 只解决对象类型的 | |
if(typeof obj !== 'object' || obj == null) {return} | |
// 实例化 Observe 实例 | |
new Observe(obj) | |
} | |
// 依据传入 value 的类型做相应的响应式解决 | |
class Observe {constructor(obj) {if(Array.isArray(obj)) {// TODO} else { | |
// 对象 | |
this.walk(obj) | |
} | |
} | |
walk(obj) { | |
// 遍历 obj 所有属性,调用 defineReactive 进行响应化 | |
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) | |
// 代理 data 上属性到实例上 | |
proxy(this) | |
} | |
} | |
// 把 CVue 实例上 data 对象的属性到代理到实例上 | |
function proxy(vm) {Object.keys(vm.$data).forEach(key => { | |
Object.defineProperty(vm, key, {get() { | |
// 实现 vm.count 取值 | |
return vm.$data[key] | |
}, | |
set(newVal) { | |
// 实现 vm.count = 123 赋值 | |
vm.$data[key] = newVal | |
} | |
}) | |
}) | |
} |
编译
初始化视图
依据节点类型进行编译
class CVue {constructor(options) { | |
// 省略。。// 2 代理 data 上属性到实例上 | |
proxy(this) | |
// 3 编译 | |
new Compile(this, this.$options.el) | |
} | |
} | |
// 编译模板中 vue 语法,初始化视图,更新视图 | |
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)) { | |
// TODO | |
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) | |
} | |
} |
编译插值表达式
// 编译模板中 vue 语法,初始化视图,更新视图 | |
class Compile {complie(el) {Array.from(childNodes).forEach(node => {if(this.isElement(node)) {console.log(` 编译元素 ${node.nodeName}`) | |
} else if(this.isInterpolation(node)) {// console.log(` 编译插值文本 ${node.textContent}`) | |
this.complieText(node) | |
} | |
// 省略 | |
}) | |
} | |
// 是插值表达式 | |
isInterpolation(node) { | |
return node.nodeType === 3 | |
&& /\{\{(.*)\}\}/.test(node.textContent) | |
} | |
// 编译插值 | |
complieText(node) {// RegExp.$1 是 isInterpolation()中 /\{\{(.*)\}\}/ 匹配进去的组内容 | |
// 相等于 {{count}} 中的 count | |
const exp = String(RegExp.$1).trim() | |
node.textContent = this.$vm[exp] | |
} | |
} |
编译元素节点和指令
须要取出指令和指令绑定值
应用数据更新视图
// 编译模板中 vue 语法,初始化视图,更新视图 | |
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 => { | |
// c-text="count" 中 c -text 是 attr.name,count 是 attr.value | |
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-') | |
} | |
// 解决 c -text 文本指令 | |
text(node, exp) {node.textContent = this.$vm[exp] | |
} | |
// 解决 c -html 指令 | |
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 类
监听器,数据变动更新对应节点视图
// 创立 Watcher 监听器,负责更新视图 | |
class Watcher {// vm vue 实例,依赖 key,updateFn 更新函数(编译阶段传递进来) | |
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) {// RegExp.$1 是 isInterpolation()中 /\{\{(.*)\}\}/ 匹配进去的组内容 | |
// 相等于 {{count}} 中的 count | |
const exp = String(RegExp.$1).trim() | |
// node.textContent = this.$vm[exp] | |
this.update(node, exp, 'text') | |
} | |
// 解决 c -text 文本指令 | |
text(node, exp) {// node.textContent = this.$vm[exp] | |
this.update(node, exp, 'text') | |
} | |
// 解决 c -html 指令 | |
html(node, exp) {// node.innerHTML = this.$vm[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} | |
// html 更新器 | |
htmlUpdater(node, value) {node.innerHTML = value} | |
} |
定义 Dep 类
- data 的一个属性对应一个 Dep 实例
- 治理多个
Watcher
实例,告诉所有Watcher
实例更新
// 创立订阅器,每个 Dep 实例对应 data 中的一个属性 | |
class Dep {constructor() {this.deps = [] | |
} | |
// 增加 Watcher 实例 | |
addDep(dep) {this.deps.push(dep) | |
} | |
notify() { | |
// 告诉所有 Wather 更新视图 | |
this.deps.forEach(dep => dep.update()) | |
} | |
} |
创立 Watcher 时触发 getter
class Watcher {// vm vue 实例,依赖 key,updateFn 更新函数(编译阶段传递进来) | |
constructor(vm, key, updateFn) { | |
// 省略 | |
// 把 Wather 实例长期挂载在 Dep.target 上 | |
Dep.target = this | |
// 获取一次属性,触发 getter, 从 Dep.target 上获取 Wather 实例寄存到 Dep 实例中 | |
this.$vm[key] | |
// 增加后,重置 Dep.target | |
Dep.target = null | |
} | |
} |
defineReactive 中作依赖收集,创立 Dep 实例
function defineReactive(obj, key, val) { | |
// 递归解决,解决 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 | |
// 持续进行响应式解决,解决 newVal 是对象状况 | |
observe(val) | |
// 更新视图 | |
dep.notify()} | |
} | |
}) | |
} |
监听事件指令@xxx
- 在创立 vue 实例时,须要缓存
methods
到 vue 实例上 - 编译阶段取出 methods 挂载到 Compile 实例上
- 编译元素时
- 辨认出
v-on
指令时,进行事件的绑定 - 辨认出
@
属性时,进行事件绑定 - 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,应用
bind
批改监听函数的 this 指向为组件实例
// 自定义 Vue 类 | |
class CVue {constructor(options) {this.$methods = options.methods} | |
} | |
// 编译模板中 vue 语法,初始化视图,更新视图 | |
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 => { | |
// c-text="count" 中 c -text 是 attr.name,count 是 attr.value | |
const {name: attrName, value: exp} = attr | |
if(this.isDirective(attrName)) { | |
// 省略。。。if(this.isEventListener(attrName)) {// v-on:click, subStr(5)即可截取到 click | |
const eventType = attrName.substring(5) | |
this.bindEvent(eventType, node, exp) | |
} | |
} else if(this.isEventListener(attrName)) {// @click, subStr(1)即可截取到 click | |
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] | |
// 减少监听并批改 this 指向以后组件实例 | |
node.addEventListener(eventType, method.bind(this.$vm)) | |
} | |
} |
v-model 双向绑定
实现 v-model
绑定 input
元素时的双向绑定性能
// 编译模板中 vue 语法,初始化视图,更新视图 | |
class Compile { | |
// 省略... | |
// 解决 c -model 指令 | |
model(node, exp) { | |
// 渲染视图 | |
this.update(node, exp, 'model') | |
// 监听 input 变动 | |
node.addEventListener('input', (e) => {const { value} = e.target | |
// 更新数据,相当于 this.username = 'mio' | |
this.$vm[exp] = value | |
}) | |
} | |
// model 更新器 | |
modelUpdater(node, value) {node.value = value} | |
} |
数组响应式
- 获取数组原型
- 数组原型创建对象作为数组拦截器
- 重写数组的 7 个办法
// 数组响应式 | |
// 获取数组原型, 前面批改 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)) | |
} | |
} |
正文完
发表至: javascript
2023-01-06