再谈vue的响应式这次争取讲的明明白白
<div #app>name is {{this.name}}, age is {{this.age}</div>
const vm = new Vue({
el: '#app',
data: {
name: 'Jason',
age: 18,
}
})
整体流程(最核心):
调用beforeCreate钩子函数
拿到option中的data,将其交给Observer类变成响应式的数据
调用created钩子函数
-
判断有没有el属性,
有的话就调用$mount方法,将el传进去,这个函数里面会进行将模板编译为render函数(如果是运行时编译的话),然后调用render函数拿到最新地vnode,根据vnode进行遍历递归渲染页面
-
没有就啥也不干,就完事了
响应式(vue中通过Observer,Dep,Watcher配合scheduler调度器来实现,接下来会逐步引入这些概念)
什么是响应式数据?
响应式数据就是当我们给响应式数据重新赋值的时候,会自动执行某一些依赖这个响应式数据的函数
具体表现就是:
比如这里有一个对象,还有一个函数,我们把它叫做render函数吧
const obj = { name: "Jason", age: 18 } function render () { const div = document.querySelector('#app') div.innerHTML = `name is ${obj.name}, age is ${obj.age}` }
通过简单的观察,我们可以发现render函数执行的时候,使用到了obj的两个属性。
思考一下,如果说我们做这么一步操作
obj.age = 19
, 然后自动地执行了render函数,界面就会更新是不是就好像跟vue差不多了,数据一改变,视图自当更新!
然后我们再回过头看看上面那句话:
响应式数据就是当我们给响应式数据重新赋值的时候,会自动执行某一些依赖这个响应式数据的函数
是不是感觉好像明白了些什么~~
响应式数据怎么实现呢?
我们通过一个函数专门来做这件事,暂且就叫做Observer吧
这个函数接收一个普通的对象,然后再对这个对象进行一些处理,那么这个对象就变成了响应式对象
function Observer (data) { for (const prop in data) { let value = data[prop] Object.defineProperty(data, prop, { get() { // 虽然获取data[prop]的时候这里我可以知道,但是这里我要做啥? return value }, set(val) { value = val // 虽然给data[prop]赋值的时候我可以在这里知道,但是这里我要做啥? } }) } }
思考一下我们可以发现,在调用前面的render函数的时候,会用到响应式数据,就会触发getter
那么我们就可以在getter里面做文章了
function Observer (data) { for (const prop in data) { let value = data[prop] const dep = [] // 新增代码 Object.defineProperty(data, prop, { get() { // 有函数用到了我,我得将这个函数收集一下,但是收集到哪里去呢? // 我们给每一个属性分配一个数组dep容器,就放到这里面去 dep.push(render) // 新增代码 return value }, set(val) { value = val // 虽然给data[prop]赋值的时候我可以在这里知道,但是这里我要做啥? } }) } }
然后我们再思考,setter里面要干嘛呢?
setter执行,说明什么,说明有人要给这个属性重新赋值,那么我们需要怎么做?是不是把刚刚收集到的那个render函数拿出来执行一下就可以了
于是就有了以下代码:
function Observer (data) { for (const prop in data) { let value = data[prop] const dep = [] // 新增代码 Object.defineProperty(data, prop, { get() { // 有函数用到了我,我得将这个函数收集一下,但是收集到哪里去呢? // 我们给每一个属性分配一个数组dep容器,就放到这里面去 dep.push(render) // 新增代码 return value }, set(val) { value = val // 这里把刚刚getter收集到的依赖函数拿出来执行一遍 dep.forEach(item => { // 新增代码 item() }) } }) } }
现在,我们可以浅浅地模拟一下vue的源码:
function Observer(vm, data) { for (const prop in data) { let value = data[prop]; const dep = []; // 新增代码 Object.defineProperty(data, prop, { get() { // 有函数用到了我,我得将这个函数收集一下,但是收集到哪里去呢? // 我们给每一个属性分配一个数组dep容器,就放到这里面去 dep.push(vm._render); // 新增代码 return value; }, set(val) { value = val; // 这里把刚刚getter收集到的依赖函数拿出来执行一遍 dep.forEach((item) => { // 新增代码 item.call(vm); }); }, }); Object.defineProperty(vm, prop, { get() { return data[prop]; }, set(val) { data[prop] = val }, }); } } function Vue(options) { // 1\. 调用beforeCreate钩子函数 ... // 2\. 拿到option中的data,将其交给Observer类变成响应式的数据 Observer(this, options.data || {}); this._render = options.render; // 3\. 调用created钩子函数 ... // 4\. 判断有没有el属性 // if (options.el) { // this.$mount(options.el) // 这个代码就不实现了 // } // 我们将第四部简化一下 options.render.call(this); }
<!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> </head> <body> <div id='app'></div> <script src="./my-vue.js"></script> <script> const vm = new Vue({ data: { name: "Jason", age: 18, }, render() { const div = document.querySelector("#app"); div.innerHTML = `name is ${this.name}, age is ${this.age}`; }, }); setTimeout(() => { vm.age = 19 }, 1000) </script> </body> </html>
强烈建议大家把上面的代码copy到编辑器中,然后运行看看效果,然后捋一下代码每一行是什么意思
引入Dep概念:
我们可以把依赖收集,派发更新这些操作专门抽离出一个类来处理
class Dep { constructor() { this.subs = []; } depend(target) { this.subs.push(target); } notify() { this.subs.forEach((sub) => { sub(); }); } }
然后把Observer函数里面的代码小改一下,就是下面这个样子:
class Dep { constructor() { this.subs = []; } depend(target) { this.subs.push(target); } notify() { this.subs.forEach((sub) => { sub(); }); } } function Observer(vm, data) { for (const prop in data) { let value = data[prop]; const dep = new Dep(); // 改动点 Object.defineProperty(data, prop, { get() { dep.depend(vm._render.bind(vm)); // 改动点 return value; }, set(val) { value = val; dep.notify(); // 改动点 }, }); Object.defineProperty(vm, prop, { get() { return data[prop]; }, set(val) { data[prop] = val; }, }); } } function Vue(options) { // 1\. 调用beforeCreate钩子函数 ... // 2\. 拿到option中的data,将其交给Observer类变成响应式的数据 Observer(this, options.data || {}); this._render = options.render; // 3\. 调用created钩子函数 ... // 4\. 判断有没有el属性 // if (options.el) { // this.$mount(options.el) // 这个代码就不实现了 // } // 我们将第四部简化一下 options.render.call(this); }
引入Watcher概念:
我们仔细想想会发现一个大问题,我们在依赖收集的时候,是不是把收集到的东西写死了,导致只能收集到render函数,不能收集到别的。
这里大家可能会想,我也不需要收集其他什么东西了啊,不就是render函数嘛。数据更新,视图自动更新,还有什么其他的东西嘛
大家可以看看如下代码:
const vm = new Vue({ el: "#app", data: { lastname: "老", firstname: "王", no: 1, }, computed: { fullname() { console.log("fullname"); return this.lastname + this.firstname; } }, methods: { console() { console.log(this.fullname) } }, render(h) { return h("p", [h("span", this.no)]); }, });
可以发现,我的视图只依赖no属性,你firstname,lastname变了跟我视图有什么关系,我并不需要更新视图。
虽然大家目前还不知道firstname变了需要干嘛,可以猜想应该是执行跟fullname这个计算属性有关的函数,但肯定不是执行render函数对吧。
总而言之,依赖收集的时候不能写死,而应该跟在获取这个属性的时候,所在的函数有关
那这句话又怎么理解呢?
是这样的,no属性在获取的时候是由于render函数调用,而firstname属性获取的时候,是由于fullname这个计算属性的调用,
那么他们应该分别收集render函数,fullname函数,而不能写死为render函数。
说了这么多,其实就是想引入Watcher这么一个概念
让watcher去管理这些属性到底应该收集什么东西,你在收集的时候,只管去收集一个固定的变量就好了,Wacther会去管理那个变量的值。
那具体应该怎么管理呢?
其实就是把那些要执行的函数不要直接去执行,而是交给Watcher去执行。
上代码:
class Watcher { // 新增代码 constructor(vm, fn) { this.vm = vm; this.getter = fn; this.get(); } get() { Dep.target = this; this.getter.call(this.vm); Dep.target = undefined; } update() { this.get(); } } class Dep { static target = undefined; // 改动点 constructor() { this.subs = []; } depend(target) { this.subs.push(target); } notify() { this.subs.forEach((sub) => { sub.update(); // 改动点 }); } } function Observer(vm, data) { for (const prop in data) { let value = data[prop]; const dep = new Dep(); Object.defineProperty(data, prop, { get() { if (Dep.target) { // 改动点 dep.depend(Dep.target); // 改动点 } return value; }, set(val) { value = val; dep.notify(); }, }); Object.defineProperty(vm, prop, { get() { return data[prop]; }, set(val) { data[prop] = val; }, }); } } function Vue(options) { // 1\. 调用beforeCreate钩子函数 ... // 2\. 拿到option中的data,将其交给Observer类变成响应式的数据 Observer(this, options.data || {}); this._render = options.render; // 3\. 调用created钩子函数 ... // 4\. 判断有没有el属性 // if (options.el) { // this.$mount(options.el) // 这个代码就不实现了 // } // 我们将第四部简化一下 // options.render.call(this); new Watcher(this, options.render); // 新增代码 }
强烈建议大家把上面的代码copy到编辑器中,然后运行看看效果,然后捋一下代码每一行是什么意思
那么到这里呢,关于vue的响应式的三个类的最最核心功能就讲完了,
虽然这一套流程还是有很多的缺陷,但是肯定能够帮助大家理解vue源码里面的主线。
其实大家好好捋捋,多看几遍,然后打打断点啥的,还是能够理解这一套流程的。
这里面的每一个类中,都有很多实现细节,我这里就不展开了,打算之后专门弄一个系列来讲这些细节部分,每一个细节可能都会用一篇文章来讲解。(先给自己挖个坑)
(调度器相关的nextTick好像还没讲到,尴尬,咱们先把这一套流程弄明白,下期再会也不迟)