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,setterfunction 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.countvm.$data.count = 233
代理之后后,能够应用如下形式
vm.countvm.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)) }}