Vue响应式原理
一、研究Vue对象本身
创建一个文件夹(项目),初始化这个文件夹
js
npm init -y
执行了项目初始化,我们才能npm下载我们第三方的包。
必须保证我们项目中有package.json这个文件,我们才能在这个项目中下载第三方包。
下载vue
js
npm install vue@2.6.10
需要了解到一些特定:
$el:获取到当前这个vue对象根节点
$data:获取到vue中的data数据
$parent:当前这个组件的父组件
$children:当前这个组件的所有子组件
_vnode:抽象出来的虚拟dom对象
打印出来的内容
js
Vue
$attrs: (...)
$children: []
$createElement: ƒ (a, b, c, d)
$el: div#app
$listeners: (...)
$options: {components: {...}, directives: {...}, filters: {...}, el: "#app", _base: ƒ, ...}
$parent: undefined
$refs: {}
$root: Vue {_uid: 0, _isVue: true, $options: {...}, _renderProxy: Proxy, _self: Vue, ...}
$scopedSlots: {}
$slots: {}
$vnode: undefined
password: (...)
username: (...)
_renderProxy: Proxy {_uid: 0, _isVue: true, $options: {...}, _renderProxy: Proxy, _self: Vue, ...}
_self: Vue {_uid: 0, _isVue: true, $options: {...}, _renderProxy: Proxy, _self: Vue, ...}
_staticTrees: null
_uid: 0
_vnode: VNode {tag: "div", data: {...}, children: Array(1), text: undefined, elm: div#app, ...}
_watcher: Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, ...}
_watchers: [Watcher]
$data: (...)
$isServer: (...)
$props: (...)
二、创建vuejs文件实现数据劫持
ES5里面JS提供了一个Object.defineProperty方法
可以对你指定的对象进行一个数据劫持。
当你操作指定对象的时候,我能检测到修改的内容,
当你在页面使用我的对象属性的时候。我能检测到被使用了
ld数据劫持
js
<script>
const user = {
username:"xiaowang",
password:123
}
let username = user.username
// 好好研究一下Object这个对象里有哪些方法
Object.defineProperty(user,"username",{
// 监控是否使用
get(){
console.log("使用了user.username");
return username
},
// 监控是否修改
set(val){
console.log("设置user.username的值");
console.log(val);
}
})
console.log(user.username)
user.username = "xiaofeifei"
console.log(user)
</script>
Object.defineProperty接受三个参数
- 监控的对象
- 监控属性
- 执行get和set
Vue底层默认就是采用数据劫持的方式来上实现数据变化,驱动dom的变化
三、对象属性Reactive化
我们上面的代码可以实现数据劫持,但是仅仅只能针对一个属性。
js
<script>
const user = {
username: "xiaowang",
password: 123
}
// 封装一个函数,这个函数负责数据劫持
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
// 监控是否使用
get() {
console.log(`${data}的${key}被使用`);
return value
},
// 监控是否修val改
set(val) {
console.log(`${data}的${key}被修改`);
value = val
}
})
}
// 以后只要有一个属性要被接触,调用一下这个函数
// defineReactive(user,"username",user.username)
// console.log(user.username);
// user.username = "xiaofeifei"
// 根据user的key来进行循环。
Object.keys(user).forEach(key=>{
defineReactive(user,key,user[key])
})
console.log(user.password);
console.log(user.username);
</script>
我们需要用到一个Object.,keys来获取所有对象的key。进行遍历。
四、Vue的数据劫持
定义Observer类完成data数据劫持
在自己的vuejs文件中进行数据劫持的类定义
满足一个单一职责:一个类做一件事,一个函数实现一个业务
js
// Vue提供了一个Observer类来进行数据劫持
// 这个类主要就是针对我们页面Vue对象中data进行数据劫持
class Observer {
constructor(data) {
this.data = data
this.walk()
}
// 这个方法就是针对你传递进来的data进行数据劫持
defineReactive(data,key,value) {
Object.defineProperty(data, key, {
get() {
console.log(`${data}对象的${key}属性被调用`);
return value
},
set(val) {
console.log(`${data}对象的${key}属性被赋值`);
value = val
}
})
}
walk(){
Object.keys(this.data).forEach(key => {
this.defineReactive(this.data,key,this.data[key])
});
}
}
const user = {
username:"xiaowang"
}
new Observer(user)
console.log(user.username)
五、Vue对象创建
我们在页面中要使用vue,需要引入vue对象,创建这个对象
js
const app = new Vue({
el:"#app",
data(){
return{
username:"xiaowang"
}
}
})
接下来要定义好Vue类
js
class Vue{
// 创建Vue对象的时候,传递进来的对象
constructor(options){
this.$options = options
this.$data = options.data()
this.$el = options.el
// 针对$data这个对象里所有属性进行数据劫持
new Observer(this.$data)
// 针对Vue对象上面的属性进行劫持
this.proxy()
}
// 需要将传递进来$data数据,绑定到Vue对象身上
// this.username
// this.$data.username
// Vue对象身上默认会有属性,Vue里$data也会有属性
proxy(){
Object.keys(this.$data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return this.$data[key]
},
set(val){
this.$data[key] = val
}
})
})
}
}
核心思想,接受创建Vue的时候传递进来的对象。
针对$data进行数据劫持
针对 Vue身上的属性进行数据劫持
六、模板编译
将你们Vue对象中定义好的数据,渲染到页面模板上
默认Vue采用的mastache语法{{}}
js
// 模板编译代码
// 专门用于模板编译
class Complier{
// 获取到Vue根节点,data
// $el:"#app"
constructor(el,data){
// #app document.querySelector("#app")
this.$el = document.querySelector(el)
this.$data = data
this.complier()
}
complier(){
// this.$el.children.forEach(item=>{
// console.log(item)
// })
// 遍历所有的子标签,寻找子标签中间有{{}}
[...this.$el.children].forEach(item=>{
if(/\{\{([a-zA-Z0-9]+)\}\}/.test(item.innerHTML)){
// RegExp.$1 代表获取到 正则表达式第一个 ()里面文本
const key = RegExp.$1.trim()
console.log(key);
item.innerHTML = this.$data[key]
}
})
}
}
完整代码
js
// Vue提供了一个Observer类来进行数据劫持
// 这个类主要就是针对我们页面Vue对象中data进行数据劫持
class Observer {
constructor(data) {
this.data = data
this.walk()
}
// 这个方法就是针对你传递进来的data进行数据劫持
defineReactive(data,key,value) {
Object.defineProperty(data, key, {
get() {
console.log(`${data}对象的${key}属性被调用`);
return value
},
set(val) {
console.log(`${data}对象的${key}属性被赋值`);
value = val
}
})
}
walk(){
Object.keys(this.data).forEach(key => {
this.defineReactive(this.data,key,this.data[key])
});
}
}
class Vue{
// 创建Vue对象的时候,传递进来的对象
constructor(options){
this.$options = options
this.$data = options.data()
this.$el = options.el
// 针对$data这个对象里所有属性进行数据劫持
new Observer(this.$data)
// 针对Vue对象上面的属性进行劫持
this.proxy()
// 实现模板编译,显示数据
new Complier(this.$el,this.$data)
}
// 需要将传递进来$data数据,绑定到Vue对象身上
// this.username
// this.$data.username
// Vue对象身上默认会有属性,Vue里$data也会有属性
proxy(){
Object.keys(this.$data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return this.$data[key]
},
set(val){
this.$data[key] = val
}
})
})
}
}
// 模板编译代码
// 专门用于模板编译
class Complier{
// 获取到Vue根节点,data
// $el:"#app"
constructor(el,data){
this.$el = document.querySelector(el)
this.$data = data
this.complier()
}
complier(){
// this.$el.children.forEach(item=>{
// console.log(item)
// })
// 遍历所有的子标签,寻找子标签中间有{{}}
[...this.$el.children].forEach(item=>{
if(/\{\{([a-zA-Z0-9]+)\}\}/.test(item.innerHTML)){
// RegExp.$1 代表获取到 正则表达式第一个 ()里面文本
const key = RegExp.$1.trim()
console.log(key);
item.innerHTML = this.$data[key]
}
})
}
}
七、发布订阅(观察者模式)
Vue底层引入了观察者模式(发布订阅)
因为我们在实际开发过程中,页面上会有很不多节点使用了data数据。
订阅者:食客就是订阅者,订阅了干拌抄手。
发布者:发布了干拌抄手的消息就会接受到通知
Vue发布订阅模式流程
草图:
完整代码
js
// Vue提供了一个Observer类来进行数据劫持
// 这个类主要就是针对我们页面Vue对象中data进行数据劫持
class Observer {
constructor(data) {
this.data = data
this.walk()
}
// 这个方法就是针对你传递进来的data进行数据劫持
defineReactive(data,key,value) {
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 依赖收集
// 将wathcer存放到dep对象
// 页面console.log执行get 页面{{username}}
if(Dep.target){
dep.subs.push(Dep.target)
}
console.log(`${data}对象的${key}属性被调用`);
return value
},
set(val) {
// 检测到页面修改的指定的属性
// 调用dep通知所有的watcher进行页面更新
console.log(`${data}对象的${key}属性被赋值`);
value = val
// 让dep来通知所有的Wathcer进行页面更新
dep.notify()
}
})
}
walk(){
Object.keys(this.data).forEach(key => {
this.defineReactive(this.data,key,this.data[key])
});
}
}
class Vue{
// 创建Vue对象的时候,传递进来的对象
constructor(options){
this.$options = options
this.$data = options.data()
this.$el = options.el
// 针对$data这个对象里所有属性进行数据劫持
new Observer(this.$data)
// 针对Vue对象上面的属性进行劫持
this.proxy()
// 实现模板编译,显示数据
new Complier(this.$el,this.$data)
}
// 需要将传递进来$data数据,绑定到Vue对象身上
// this.username
// this.$data.username
// Vue对象身上默认会有属性,Vue里$data也会有属性
proxy(){
Object.keys(this.$data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return this.$data[key]
},
set(val){
this.$data[key] = val
}
})
})
}
}
// 模板编译代码
// 专门用于模板编译
class Complier{
// 获取到Vue根节点,data
// $el:"#app"
constructor(el,data){
this.$el = document.querySelector(el)
this.$data = data
this.complier()
}
complier(){
// this.$el.children.forEach(item=>{
// console.log(item)
// })
// 遍历所有的子标签,寻找子标签中间有{{}}
[...this.$el.children].forEach(item=>{
if(/\{\{([a-zA-Z0-9]+)\}\}/.test(item.innerHTML)){
// RegExp.$1 代表获取到 正则表达式第一个 ()里面文本
const key = RegExp.$1.trim()
console.log(key);
// 这个代码是直接渲染到页面上。底层不是直接操作
// render方法就是页面上渲染方法
const render = ()=>item.innerHTML = this.$data[key]
render()
// 给页面的元素创建Watcher对象
new Watcher(render)
}
})
}
}
// 创建订阅者(Watcher)
class Watcher{
// 接受render方法,完成页面渲染
constructor(callback){
// Dep类新增了一个静态属性,this代表当前watcher
Dep.target = this
this.callback = callback
this.update()
Dep.target = null
}
update(){
this.callback()
}
}
// 创建一个发布者
class Dep{
constructor(){
// 存放所有我需要管理Watcher
this.subs = []
}
notify(){
// 通知所有watcher进行页面修改
this.subs.forEach(watcher=>{
watcher.update()
})
}
}
八、抽象语法树AST
AST称为抽象语法树(Abstract Syntanx Tree)
简称:语法树
将你们源代码抽象为JavaScript对象,用对象的形式来表示我们的源代码
Vue的源代码
html
<div id="app">
<p :class="{active:true}">{{username}}</p>
<span v-bind:index="active">{{password}}</span>
<span v-on:click="check">{{password}}</span>
</div>
这个源代码是无法直接在浏览器里面进行加载。
Vue为了解决这个问题,将Vue模板转化为HTML代码,页面能直接识别的代码
使用抽象语法树来进行中间转换
Vue模板代码转化为抽象语法树
html
<div id="app">
<p :class="{active:true}" index="1">{{username}}</p>
<span v-bind:index="active">{{password}}</span>
<span v-on:click="check">{{password}}</span>
</div>
我们会将模板代码编译为字符串,innerHTML
bash
<div id="app">
<p :class="{active:true}">{{username}}</p>
<span v-bind:index="active">{{password}}</span>
<span v-on:click="check">{{password}}</span>
</div>
将字符串解析为JavaScript对象
js
[
{
tag:"div",
attrs:[
{id:"app"}
],
children:[
{
tag:"p",
attrs:[
{
name:"class",
value:"active"
},
{
name:"index",
value:"1"
}
],
children:[
]
type:0
},
{
tag:"span",
attrs:[
{
name:"index",
value:"1"
}
],
type:1
}
]
}
]
抽象语法树:本质就是一个JavaScript对象,针对原来的属性代码进行了抽象后结果
Vue模板如何变成AST,这个过程:链表、递归等等很多算法
总结:
面试题1:请你说一下你了解Vue响应式原理?
Vue2的响应式原理,底层默认采用的数据劫持+发布订阅模式来实现的。
- 数据劫持使用Object.defineProperty进行data里面所有数据的接触。包括对Vue对象身上的属性接触和$data对象的属性进行劫持。
- 在Object.defineProperty里面会有get和set、get主要用于收集依赖(收集watcher和dep关系),set方法主要执行Dep里卖弄notify方法进行通知wacther进行页面更新
- Dep类属于发布者、Wacther属于订阅者,一旦Dep调用notify我们就会执行Watcher更新,更新执行render渲染
面试题2:Vue语法如何最终被浏览器识别?这个过程是什么?
Vue模板代码不好直接编译为HTML。里面包含特殊语法太多了
Vue底层默认会将Vue、template模板代码抽象为语法树AST,目的就是将Vue模板抽象为JavaScript方便我们后续解析,遍历里面每一个节点。
AST抽象语法树,使用编译函数、转化为虚拟DOM。虚拟dom里面包含变化的内容。
通过diff算法里卖弄patch函数实现页面的更新
页面渲染就是普通HTML代码
面试题3:Vue的效率相对于JS来说,谁高谁低?
根据情况来决定,Vue为了让我们开发方便,数据驱动。封装了很多底层代码。DOM操作。数据更新。封装的越多效率越低。
但是JS里对于JS的大量操作、频繁更新,这个无法进行很好优化,在这种情况下,用Vue,在这些方便提升性能
JS代码本身就很简单,没有复杂的操作。原生JS代码效率肯定比Vue更高
.env.development文件
js
VUE_APP_TITLE = 'woniu'
VUE_APP_BASE_URL = "/api"
VUE_APP_PORT = 8889
代码中
js
methods:{
async fetchData(){
console.log(process.env);
axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
const res = await axios({
url:"/usersAD/getSearch",
method:"POST",
data:{}
})
console.log(res);
}
}
使用环境配置代理服务器
js
const replacePath = "^"+process.env.VUE_APP_BASE_URL
module.exports = {
devServer:{
port:Number(process.env.VUE_APP_PORT),
//配置代理服务器,webpack内置的
proxy:{
//当检测到路径包含了/api 进入代理服务器
//http://127.0.0.1:4001/api/usersAD/getSearch
[process.env.VUE_APP_BASE_URL]:{
target:"http://127.0.0.1:4001",
changeOrigin:true,
//表达的意思就是将路径中/api,替换成空字符串
pathRewrite:{
[replacePath]:''
}
},
}
}
}