源码实现
ViewModel
vue2.x 中 Options API 中的data函数返回一个对象实现了数据的响应式处理
//vue2.x写法在源码里是一个Vue构造函数实例化传入一个options参数let vm = new Vue({el: '#app',data(){return {}}});
数据劫持其实在初始化的时候已经完成了
Vue.prototype._init = function(){ initState(vm); }
问题:什么叫初始化状态initState?
state是 保存的数据被定义为状态,状态的更改使视图发生更改,在 vue2 里,如computed,data, watch等配置项也归纳为state状态的一部分
问题:为什么要缓存另外的一个_data?
不希望操作它将用户编写的vm.$options.data,所以要区分开来
var data = vm.$options.data;data = vm._data = typeof data === 'function'//如果用户编写的data是函数就执行该函数? data.call(vm)//如果不是就放入用户些的data对象或者是空对象: data || {};
问题:为什么要用代理?
如果用户想要访问data下的属性,需要vm.$options.data.title/vm.$options.data().title而不是vm.title访问,不便于开发编写,所以需要通过defineProperty代理
for (var key in data) {// title/classNum/total/teacher/studentsdefineProperty(data, key, {//访问该属性的时候进行拦截并返回想要的数据get(){return vm['_data'][key];},set(newValue){vm[_data][key] = newValue;}});}
问题:为什么要观察data?
通过观察者模式不仅对data进行观察,对内部的属性也要观察,如果内部的属性是对象就要做相关的拦截,如果是数组要对数组方法的拦截
问题:为什么观察者对象/数组是一个类或构造函数Observer来处理?
因为修改的对象或数组里的属性是不确定的,有时候也会有新增内容,所以需要实例化一个Observe构造函数来写较为合适
问题:Observer构造函数内部如何实现数据观察?
通过Object.keys()遍历拿到data里所有的key和value,并将其通过defineProperty做下一步的响应式数据处理
问题:为什么不能对数组进行defineProperty?
因为数组本身defineProperty是不处理数组的,当然可以用一些方法去处理,但是比较麻烦,目的仅仅是为了拦截数组,那么就对数组中的方法进行重写
问题:为什么在 vue2里需要重写数组里的原生方法?
- 需要保留原有的数组方法操作数据
- 希望在
push/unshift/splice新增数组元素时新增更多业务逻辑
因为有些数组的数据变更并不能被 vue 检测到,操作数组的一些动作,如通过索引值修改值,或者修改长度,或者是调用一些Array.prototype上的方法并不能触发这个属性的setter, splice方法也可以触发状态更新,在 vue2.x 版本将数组的 7 个方法push,pop,shift,unshift,splice,sort,reverse重写,调用包装后的数组方法就可以被 vue检测到
问题:数据劫持的目的是什么?
不希望原生对对象和数组的操作是一个纯操作,而是赋值或操作数组的过程时仍可以新增新的业务逻辑进去,像绑定视图数据,希望数据变化的过程中,视图也跟随着变化,那么就必须拦截 getter/setter 行为,在拦截的过程中,保留原来数据的操作的同时可以更改视图
问题:为什么observe函数内部要new Observer构造函数?
不能直接让程序走到观察者构造函数而是需要提前判断data是否符合是一个对象的条件,观察者构造函数的任务仅仅是观察一个对象
问题:如何将data进行数据劫持?
通过Object.definePropery劫持子属性里的对象/数组包括深度劫持
实现流程:
初始化状态
initState初始化数据
initData- 缓存
_data并拿到里面的数据对象 - 代理属性访问方式
- 观察
data内部
- 缓存
observe:- 观察
data是不是对象,如果不是对象不进行观察 - 对
data对象进行观察new Observer
- 观察
Observer:对数组的处理:
定义
observeArr(data):- 给
data的原型__proto__新增重写好的 7 个数组方法 - 遍历数组里的每一项
- 对每一项数组元素进行观察
observe
- 给
定义 7 个数组方法名称的数组
定义
array.js:创建保留原有数组操作方法的一个新的对象
遍历 7 个数组方法名称,对新的对象里的属性方法进行修改(重写数组方法)
- 保存形参的参数类数组列表
- 通过
slice将类数组转为新的数组 - 执行原数组的所有方法并传入新的形参参数列表数组
- 给
push/unshift/splice方法的参数改为保存的形参列表 - 给新增的数组参数数据劫持拦截
observeArr - 新增数组元素时更多业务逻辑
对对象的处理:
Observer.prototype.walk:对data对象进行definePropery- 遍历拿到所有的属性
key和属性值value defineReactiveData:defineProperty对属性进行getter/setter拦截setter内部设置值的时候,newValue不知道是否对象,如果是也要进行数据劫持拦截observe:递归深度拦截- 此时 getter/setter 内部可以新增新的业务代码如视图更新等
源码地址:
https://gitee.com/kevinleeeee/data-hijacked-vue2.x-demo
案例:源码实现 vue2.x
- 功能 1:
observe监听器/数据劫持/数据响应式/代理 - 功能 2:页面渲染
- 功能 3:编译文本/元素
- 功能 4:依赖收集实现读写数据时重新更新组件和渲染页面
- 功能 5:批量异步更新策略
- 功能 6:数组依赖收集
- 功能 7:
watch实现 - 功能 8:计算属性
computed实现
//项目目录├─package.json├─Readme.md├─webpack.config.js├─src| └index.js├─source| ├─vue| | ├─index.js - Vue构造函数/初始化状态| | ├─utils.js - 编译文本/元素/去空格/拿到data对象key属性的值| | ├─observe| | | ├─array.js - 观察数组函数/原数组方法保留| | | ├─dep.js - 收集watcher/订阅/发布/定义管理stack里watcher静态方法| | | ├─index.js - 观察data/访问属性代理| | | ├─observer.js - 观察类/观察数组和对象/定义响应式| | | └watcher.js - 收集deps/depsId/管理stack里的watcher/更新组件和渲染/添加依赖方法├─public| └index.html

功能 1 实现步骤:
- 编写
Vue构造函数,挂载option到实例,且初始化数据状态 - 挂载
data到实例并改写data副本,代理并修改访问属性,对data数据进行观察 - 定义观察类函数,处理对象劫持和数组劫持
- 定义响应式方法并对对象和数组数据进行新增
getter/setter - 定义观察数组函数在数组原型上保留原数组的操作方法并新增业务逻辑接口
- 对嵌套的对象和数组进行观察和响应式处理
问题:如何渲染页面?
通过 watcher实例化后执行更新组件函数,组件函数内部执行实例原型上的更新函数,根据 dom节点渲染组件实现页面渲染
功能 2 实现步骤:
- 定义
Watcher类实现页面渲染,初始化watcher - 页面挂载时实例化
watcher,传入实例和更新组件函数 - 定义更新函数,根据用户数据渲染组件
- 将用户定义模板
dom节点保存到文档碎片里 - 将文档碎片插入到
el里实现页面渲染
功能 3 实现步骤:
- 在文档碎片插入
el之前定义一个编译函数 - 处理文本节点,拿到
key值替换html文本节点内容 - 处理元素节点
问题:如何将 data定义的属性数据替换 html里定义的标签属性?
通过正则替换文本节点内容(node.textContext), 拿到 key以后再去获取 data数据里 key的值并作为被替换的文本的内容
问题:如何将vm['person.name']写法改为vm['person']['name']写法实现嵌套属性访问?
通过 reduce整理
//获取data数据里对应key属性的值export function getValue(exp, vm) {//console.log(exp);//person.name//问题:明显vm['person.name']这样的写法是无法访问属性值//实际访问写法应该是vm['person']['name']//解决:通过reduce方法完成//1.以 . 作为分隔符分割字符串let keys = exp.split('.');//console.log(keys);//['person', 'name']//2.整理写法return keys.reduce((prev, next) => {prev = prev[next];return prev;}, vm);}
功能 4 实现步骤:
- 创建
Dep类(data里每个属性对应一个实例化的dep,每个dep对应一个唯一的id) - 利用发布订阅模式收集订阅者,定义订阅方法和发布方法
- 定义一个保存当前
watcher的函数 - 定义一个删除栈里当前的
watcher的函数 - 在
watcher类的get()里使用入栈和出栈的dep函数 - 在定义响应式函数里的
getter新增执行订阅动作的逻辑(读取时会更新组件和渲染页面) - 在定义响应式函数里的
setter新增执行发布动作的逻辑(改写数据时会再次更新组件和渲染页面) - 在
dep类定义depend方法 - 在
watcher类里定义addDep方法,定义一个deps容器,depsId容器
关于 Dep类(依赖):
- 有
id属性 - 有收集订阅者的数组容器(存放
watcher) - 有发布
notify方法(遍历每一个watcher并执行底下的update方法) - 有订阅
addSub方法(将每一个watcher加入到容器里) - 有
depend方法(将watcher存入dep中,然后把dep也存入watcher中 多对多)
关于 Dep静态属性和方法:
- 有
stack栈数组容器(存放watcher) - 有
target属性 - 有
pushTarget方法(将watcher赋值给target,并加入stack栈里) - 有
popTarget方法(删除stack里的watcher,target指向stack里的前一位watcher)
关于 Wachcer类:
- 有
id属性 - 有收集依赖的数组容器(存放
dep) - 有
depsId的set容器(存放depId) - 有
addDep方法(存放depId, 将dep加入到依赖容器, 执行dep.addSub方法) - 有
update方法(执行get方法) - 有
get方法(执行pushTarget方法,执行更新组件函数, 执行popTarget方法)

执行顺序:
defineReactive方法执行(from observer)- 实例化
dep依赖 defineProperty拦截- 当用户读取
data对象属性时执行get方法 - 当
watcher存在时执行dep.depend方法 - 执行该项
watcher里的addDep方法 - 存放
depId, 将dep加入到依赖容器, 执行dep.addSub方法 - 将每一个
watcher加入到容器里 - 当用户修改
data对象属性时执行set方法 - 执行
dep.notify方法 - 遍历每一个
watcher并执行底下的update方法 - 执行
pushTarget方法, - 将
watcher赋值给target,并加入stack栈里 - 执行更新组件函数
- 执行
popTarget方法 - 删除
stack里的watcher,target指向stack里的前一位watcher
功能 5 实现步骤:
watcher里定义队列管理函数,将watcher添加到队列里,执行nextTick函数延迟清空队列- 执行
nextTick函数时传入flushQueue清空队列函数(遍历队列所有watcher并依次执行组件更新) nextTick函数里将用户写的回调函数存入回调函数队列,将包裹着flushCallBacks的回调函数 作为参数 传入 4 种异步的方法里
setTimeout(()=>{vm.message = 'Hi!';vm.message = 'Hey!';vm.message = 'Bye!';},3000);
问题:如何只渲染一次拿到最后赋值的结果?
通过批量更新页面,避免重复渲染
//让flushCallBacks异步执行的几种方法//看浏览器是否兼容异步方法//方法一:Promiseif (Promise) {return Promise.resolve().then(timerFunction);}//方法二:html5 APIif (MutationObserver) {let observe = new MutationObserver(timerFunction);//假如有文本节点let textNode = document.createTextNode(10);//监听textNode变化//characterData变化证明文本节点发生变化observe.observe(textNode, {characterData: true});textNode.textContent = 20;return;}//方法三:类似setTimeout 性能优于setTimeout 老版本浏览器不兼容if (setImmediate) {return setImmediate(timerFunction);}//方法四:setTimeout(timerFunction, 0);
功能 6 实现步骤:
- 给
Observer类new了一个dep实例 - 给
Observer类里的data数据多定义一个属性__ob__,get时返回实例,实现数组访问时可以拿到Observer实例 - 在
defineReactive函数里保存一个执行observe(value)时返回的实例,在get访问时通过访问实例下dep底下的依赖收集方法进行数组的依赖收集 - 这样数组相关的
watcher放入dep里面,一旦数组发生变化,通知watcher进行重新的渲染 - 定义
dependArray函数实现嵌套数组的依赖收集
问题:没有做数组依赖的会发生什么情况?
当 data 对象里面的数组被修改时,虽然修改成功,但是组件没有更新,页面也没有重新渲染
功能 7 实现步骤:
- 在
initState函数里有定义options.watch配置项,且里面有initWatch函数 - 定义
initWatch函数: 打印vm.$options.watch可以拿到包含message的对象{message: ƒ} - 循环对象里的每一项键值对,将该项属性的值(事件处理函数)保存为变量
handler - 定义
createWatcher方法,接收参数vm,key,handler createWatcher方法返回挂载在vm实例$watch方法的结果- 在原型上的
$watch方法内部实例化new Watcher(vm,expr,handler,{配置项}); - 定义
this.getter = function(){return getValue(watch属性名,实例)},该函数返回的结果是watch里的属性的属性值是一个事件处理函数执行后的结果 - 将
Watcher类里get方法内部getter方法执行的value返回出去赋值给实例的this.value,此操作在每次实例化Watcher时拿到旧的值 - 当数据被修改时触发的发布的
watcher执行 watcher.update方法执行,触发get方法拿到新的值- 然后当新老值不一样的时候执行用户传入的回调函数
- 返回用户想要的新老数据
问题:watch定义方式是怎么样的?
watch: {//如何监听message的变化?message: function(newValue, oldValue){console.log(newValue, oldValue);}}
功能 7 实现步骤:
- 定义
initComputed方法时创建watcher实例 - 定义
watchers,let watchers = vm._watcherComputed = Object.create(null); - 实例传入参数
vm, userDef, () => {}, {lazy: true} lazy:true配置为了首次实例化watcher时不去取值- 将实例的结果赋值给带有用户定义属性名的对象里
watchers[key] defineProperty劫持属性,get时定义函数createComputedGetter执行createComputedGetter执行时返回watcher.value- 此时
computed实现了
问题:当更新属性发生变化时如何处理?
源码地址:
https://gitee.com/kevinleeeee/vue2.x-source-demo
data属性
底层实现对 data里变量的读取/修改
//实现通过实例对象vm访问data里面的变量,可以访问和修改var vm = new Vue({data() {return {a: 1,b: 2}}});function Vue(options) {//vue在创建实例的过程中调用data函数,返回数据对象this.$data = options.data();var _this = this;//希望访问的方式:this.a => this.$data.afor (var key in this.$data) {//独立作用域//k是当前作用域的临时局部变量(function (k) {//写法一:到IE8存在不兼容//代理方式修改访问/修改的方式//_this访问当前kObject.defineProperty(_this, k, {get: function () {return _this.$data[k];},set: function (newValue) {_this.$data[k] = newValue;}})//写法二:兼容性好,Mozilla的API//实例继承过来的方法//__defineGetter__(访问属性,回调函数)_this.__defineGetter__(k, function () {return _this.$data[k];});//__defineSetter__(访问属性,回调函数)_this.__defineSetter__(k, function (newValue) {_this.$data[k] = newValue;});})(key);}}console.log(vm);/*** Vue {...}* $data: {a: 1, b: 2}* a: 1* b: 2* get a: ƒ ()* set a: ƒ (newValue)*/
methods属性
实例方法挂载的实现
//实例方法挂载的实现var Vue = (function () {function Vue(options) {//每次实例化Vue执行data返回一个唯一的data对象防止指向同一个引用值this.$data = options.data();//挂载到实例上this._methods = options.methods;//传入实例本身this._init(this);}/*** 初始化实例对象里面的属性和方法* @param {*} vm 该组件实例*/Vue.prototype._init = function (vm) {initData(vm);initMethods(vm);}//直接越过$data访问属性function initData(vm) {for (var key in vm.$data) {//代理每一个属性(function (k) {Object.defineProperty(vm, k, {get: function () {return vm.$data[key];},set: function (newValue) {vm.$data[key] = newValue;}});})(key);}}function initMethods(vm) {//把自定义的方法挂载到vm实例对象里for (var key in vm._methods) {vm[key] = vm._methods[key];}}return Vue;})();var vm = new Vue({data() {return {a: 1,b: 2}},methods: {increaseA(num) {this.a += num;},increaseB(num) {this.b += num;},getTotal() {console.log(this.a + this.b);}}});vm.increaseA(1);vm.increaseA(1);vm.increaseA(1);vm.increaseA(1);vm.increaseB(2);vm.increaseB(2);vm.increaseB(2);vm.increaseB(2);vm.getTotal();console.log(vm);
computed属性
实现一个 computed
var Vue = (function () {//匹配{{}}var reg_var = /\{\{(.+?)\}\}/g;/*** 私有数据computedData* 容器保存computed对象里方法集合的函数本体和依赖* 该对象结构为:* dep: 依赖(就是实例里data里的属性) 数组存放的是data数据里的key** computedData:* {total: {value: 3, dep: ["a", "b"], get: ƒ}}** total = {* value: computed里get函数执行返回的结果* get: get函数本体* dep: ['a', 'b']* }*/var computedData = {};/**** 每一个属性都有对应的dom节点,属性改变时节点也会更新*/var dataPool = {};var Vue = function (options) {this.$el = document.querySelector(options.el);this.$data = options.data();this._init(this, options.computed, options.template);}/*** 初始化实例* @param {object} vm 实例对象* @param {object} computed 计算方法集合对象* @param {string} template 字符串模板*/Vue.prototype._init = function (vm, computed, template) {dataReactive(vm);computedReactive(vm, computed);render(vm, template);}/*** 将data数据应式处理* @param {object} vm 实例对象*/function dataReactive(vm) {var _data = vm.$data;//枚举data数据属性for (var key in _data) {(function (k) {//劫持数据达到直接访问/修改作用Object.defineProperty(vm, k, {//vm访问k时得到get: function () {return _data[k];},//vm访问k时设置set: function (newValue) {_data[k] = newValue;//更新数据updata(vm, k);//更新计算数据_updateComputedData(vm, k, function (k) {updata(vm, k);});}});})(key);}}/*** 将computedData数据响应式处理* 数据劫持访问到value属性里的数据* @param {object} vm 实例对象* @param {object} computed 计算方法集合对象*/function computedReactive(vm, computed) {_initComputedData(vm, computed);//使用computedData//computedData: {total: {value: 3, dep: ["a", "b"], get: ƒ}}for (var key in computedData) {(function (k) {Object.defineProperty(vm, k, {//vm访问k时得到get: function () {//value保存的是该方法执行后的结果return computedData[k].value;},//vm访问k时设置set: function (newValue) {//将开发者用户修改后的新的数据重新赋值更新computedData[k].value = newValue;}});})(key);}}/*** 渲染页面* @param {object} vm 实例对象* @param {string} template 字符串模板*/function render(vm, template) {var container = document.createElement('div');var _el = vm.$el;container.innerHTML = template;var domTree = _compileTemplate(vm, container);_el.appendChild(domTree);}/*** 编译模板* @param {object} vm 实例对象* @param {HTMLDivElement} container 带有模板的div元素包装器* @return {HTMLDivElement} container 替换好模板内容的div元素*/function _compileTemplate(vm, container) {//找到所有节点var allNodes = container.getElementsByTagName('*');var nodeItem = null;// console.log(allNodes);//HTMLCollection(5) [span, span, span, span, span]//枚举每个节点for (var i = 0; i < allNodes.length; i++) {nodeItem = allNodes[i];//匹配{{}}var matched = nodeItem.textContent.match(reg_var);// console.log(matched);//["{{a}}"]/null/["{{b}}"]/null/["{{total}}"]if (matched) {nodeItem.textContent = nodeItem.textContent.replace(reg_var, function (node, key) {//console.log(node); {{a}}/{{b}}/{{total}}//console.log(key); a/b/total//每一个属性都有对应的dom节点,属性改变时节点也会更新dataPool[key.trim()] = nodeItem;// console.log(dataPool);//{a: span, b: span, total: span}// console.log(vm[key.trim()]); 1/2/3//返回替换实例键名对应的值return vm[key.trim()];});}}// console.log(container);//被data数据替换{{变量}}好的模板return container;}/*** 初始化ComputedData容器的内部函数* @param {object} vm 实例对象* @param {object} computed 计算方法集合*/function _initComputedData(vm, computed) {//枚举computed计算方法集合里的所有方法名for (var key in computed) {//试着打印描述符// console.log(Object.getOwnPropertyDescriptor(computed, key));//注意:value保存的是当前方法函数本身,可以拿到执行//{writable: true, enumerable: true, configurable: true, value: ƒ}var descriptor = Object.getOwnPropertyDescriptor(computed, key);//如果有get 拿get 没有则拿valuevar descriptorFn = descriptor.value.get ? descriptor.value.get : descriptor.value;// console.log(key); total//初始化totol = {}computedData[key] = {};//descriptorFn()执行后的结果存入computedData对象里的computedData.value属性里//改变指向是因为total函数内部有用thiscomputedData[key].value = descriptorFn.call(vm);// console.log(computedData); 函数执行后的结果 {total: 3}//将第二个属性get保存到total对象里computedData[key].get = descriptorFn.bind(vm);//将第三个属性dep依赖保存到total对象里computedData[key].dep = _collectDep(descriptorFn);// console.log(computedData);//computedData: {total: {value: 3, dep: ["a", "b"], get: ƒ}}}}/*** 专门收集依赖函数* 匹配函数内部有哪些依赖* 匹配规则:this.字段任意字符出现1次或多次非贪婪* @param {function} fn computed方法集合里方法的函数本身* @return {array} 返回一个存放函数本身里实例对象data的变量集合的数组*/function _collectDep(fn) {//转为字符串再正则匹配var _collection = fn.toString().match(/this.(.+?)/g);// console.log(_collection);//["this.a", "this.b"]if (_collection.length > 0) {for (var i = 0; i < _collection.length; i++) {//去掉前面的this_collection[i] = _collection[i].split('.')[1];}// console.log(_collection);//["a", "b"]return _collection;}}/*** 更新修改数据信息* @param {object} vm 实例对象* @param {*} key 枚举data数据属性的key*/function updata(vm, key) {dataPool[key].textContent = vm[key];}/*** 更新计算数据* @param {object} vm 实例对象* @param {*} key 枚举data数据属性的key* @param {function} updata 回调函数*/function _updateComputedData(vm, key, updata) {//初始化第一批的依赖数据var _dep = null;//computedData: {total: {value: 3, dep: ["a", "b"], get: ƒ}}for (var _key in computedData) {// console.log(_key); total_dep = computedData[_key].dep;// console.log(_dep); ["a", "b"]for (var i = 0; i < _dep.length; i++) {//如果键名一致证明是修改该数据if (_dep[i] === key) {//重新执行第一批依赖的get方法//vm[_key] => vm.totalvm[_key] = computedData[_key].get();//更新变量updata(_key);}}}}return Vue;})();//使用var vm = new Vue({el: '#app',template: `<span>{{a}}</span><span>+</span><span>{{b}}</span><span> = </span><span>{{total}}</span>`,data() {return {a: 1,b: 2}},computed: {total() {console.log('computed total');return this.a + this.b;}}});console.log(vm);console.log(vm.total);console.log(vm.total);console.log(vm.total);vm.a = 100;vm.b = 200;console.log(vm.total);console.log(vm.total);console.log(vm.total);
watch属性
案例:驱动实现
技术:
webpack + vue + es6 类
实现功能:
computed实现watch实现- 实现响应式与暴露回调接口
data/computed/watch驱动
源码地址:
https://gitee.com/kevinleeeee/vue-drivers-demo
v-if/v-show
简单实现一个vue2.x版本的v-if/v-show/@click
原理:
通过找到注释节点占位<!-- v-if -->,找到父节点appendChild()进去或替换
/*** 原理:* 合理利用数据保存视图相关的信息* 通过数据与视图绑定在一起* update的时候可以很好的操作数据* 对事件处理函数循环绑定* 如何处理v-if删除节点/恢复节点(注释节点占位)** 把template模板转变为dom节点,将dom里的数据和dom绑定在一起,当数据更新的时候,更新节点* 用Map{ dom: {}}来实现 dom键名为对象* showPool数据结构* showPool = [* [* dom,* {* type: if/show,* prop: data* }* ]* ]** eventPool数据结构* eventPool = [* [* dom,* handler* ]* ]*/
问题:如何实现v-if/v-show?
- 数据代理实现访问
data数据 - 数据劫持
- 初始化 dom 数据使
v-if/v-show/@click和 dom 绑定在一起 - 初始化视图,根据
data数据先初始化时候显示视图组件 - 根据事件池去做时间处理函数的循环绑定
- 改变数据的同时改变 dom 视图
问题:为什么初始化 dom 时绑定v-if/v-show/@click?
在执行methods对象里的方法时才能找到视图相应的节点去更改它的视图
问题:vue在初始化 dom 时做了什么操作?
- 转化为 AST 树
- 转化为虚拟节点
- 转化为真实节点
- 将数据和真实节点保存在一起
问题:此轮子中,如何将v-if/v-show/@click和视图绑定在一起?
定义池子保存当前的节点和v-if/v-show/@click属性
/*** showPool: [* [* dom,* {* //如果是if则需要增删节点* type: if / show,* //如果是show则需要显示或隐藏* show: true / false,* data: 绑定的数据* }* ]* ]*//*** console.log(this.showPool);* Map(4) {* div.box.box1 => {* key: div.box.box1,* value: {type: 'if', show: false, data: 'boxShow1'}* },* div.box.box2 => {…},* div.box.box3 => {…},* div.box.box4 => {…}* }*/
/*** eventPool: [* [* dom,* handler* ]* ]*//*** console.log(this.eventPool);* Map(4) {* {* key: button,* value: ƒ showBox1()* },* button => ƒ,* button => ƒ,* button => ƒ* }*/
问题:当遇到v-if节点时,如何删除了之后恢复时保证位置不变?
通过新增一个注释节点替换被删除的节点从而实现占位
源码地址:https://gitee.com/kevinleeeee/vue2-vif-vshow-resource-demo
样式绑定
实现 style/class 样式绑定
解决:
标签属性的解析,并关联 data数据里的属性
技术:
es6类
源码地址:
https://gitee.com/kevinleeeee/vue-class-style-demo
模板编译
案例:实现模板编译
技术:rollup/es5/AST 树/数据响应式
//项目目录├─index.html├─package-lock.json├─package.json├─rollup.config.js - 配置rollup├─src| ├─index.js| ├─init.js - 初始化响应式数据/挂载vm/挂载组件/挂载render函数| ├─lifecycle.js - 管理组件挂载/所有的生命周期函数/进行补丁替换| ├─state.js - 初始化响应式数据/获取所有数据并代理数据| ├─vdom| | ├─index.js - 管理Vue原型上的所有render函数和内部代码的Vue原型上的方法(_c/_v/_s)| | ├─patch.js - 负责创建元素和文本节点的虚拟节点| | └vnode.js - 根据虚拟节点再创建真实的dom节点(包括属性更新)/新旧补丁的替换方法| ├─observer| | ├─array.js 对数组进行原型上的数组操作功能补全| | ├─index.js - 观察数据| | └Observer.js - 对观察的数据进行处理/定义响应式数据| ├─compiler| | ├─astParser.js - 专门正则规则解析html到和组装AST结构树| | ├─generate.js - 生成新的AST树结构并进行render函数内部代码的格式组装| | └index.js - html模板转化AST树/将AST树返回的code代码创建新的render函数├─dist| ├─umd| | ├─vue.js| | └vue.js.map
vue2.x 基于 options API 的写法
let options = {...};let vm = new Vue(options);
//如何拿到模板template?//优先级(没有找到模板的情况):render函数 > template > el(html)<body><!--查找顺序: 3.html --><div id="app" style="color: red; font-size: 20px;">hello {{name}}<h1>{{name}}</h1><ul><li style="color: green;">{{age}}</li><li>{{info.job}}</li></ul></div><script>//模拟用户填写的 optionsAPIlet vm = new Vue({el: '#app',//查找顺序: 2.template模板template: ``,//查找顺序: 1.render函数//createElement 函数方法render(createElement) {...},data() {return {...}}});</script></body>
编译过程:
- 拿到 template
编译
- 将 template 转换到 AST 树
- AST 形成了以后转化为 render 函数(一系列的字符串方法解析)
- render 函数写完后转换为虚拟 DOM 节点
- 设置 PATCH 补丁,对比新旧节点打补丁
- 形成真实 DOM
源码实现过程:
- 从 index.html 拿到 html 模板
- 执行初始化文件(初始化响应式数据),获取 el 元素
<div id="app"</div> - 将 el 作为模板传入编译和生产 render 函数(
compileToRenderFunction(el)) - 编译函数先进行 html 转化为 AST 树结构的对象
- 编译函数然后将 AST 树结构的对象组装成生产 render 的函数的内部代码(code)
- 将拼装好的 render 函数挂载到
vm.$options里 - 在 Vue 原型上完善 render 函数内部的方法(
_v()/_s()/_c()) - 执行 render 函数后生产出虚拟节点 vnode
- 通过打补丁的方式将虚拟节点创建成真实节点 dom
- 并对真实节点 DOM 的属性进行更新
- 最终新的虚拟节点替换老的真实节点
- 实现视图的内容更新
关于 AST(Abstract syntax tree):
AST 树 - 抽象语法树
是源代码的抽象语法结构的树状描述
虚拟 DOM(描述 DOM 节点)和 AST 树的区别:
- 当虚拟 DOM 变成真实 DOM 的时候,当把补丁打到真实 DOM 的时候,可以自定义一些内容
- AST 树是对源代码层面上的一种树结构的数据结构化
//希望的AST树写法//模拟html dom树型结构//注意:v-for v-model等不能存在于虚拟dom节点里,不便于浏览器识别//解决方法:将AST树形成以后,对AST进行优化,把多余的vue内置的语法糖属性(v-开头)全部解析成功能,从而让浏览器识别简化后的dom树{tag: 'div',//元素节点为1type: 1,attrs: [{ name: 'id', value: 'app' },{ name: 'style', value: { color: 'red', font-size: '20px' } }],children: [{ type: 3, text: 'hello' }]}
如何通过正则匹配模板中的内容?
/*** 正则规则* 来源于vue/src/compiler/parser/html-parser.js*///匹配格式:id="app"/id='app'/id=appconst attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;//匹配格式:标签名 div/span.../ <my-header>const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;//匹配格式:特殊的 标签格式 <my:header>const qnameCapture = `((?:${ncname}\\:)?${ncname})`;//匹配格式:<divconst startTagOpen = new RegExp(`^<${qnameCapture}`);//匹配格式:> 或者是 />const startTagClose = /^\s*(\/?)>/;//匹配格式:</div>const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
//模板<div id="app" style="color: red; font-size: 20px;">hello {{name}}<h1>{{name}}</h1><ul><li style="color: green;">{{age}}</li><li>{{info.job}}</li></ul></div>如何匹配?1.先匹配 <div2.删除 <div3.匹配 id="app"/id='app'/id=app4.删除 id="app"5.匹配 style="color: red; font-size: 20px;"6.将属性解析成对象存储-> attrs: [{name:'style', value: {color: 'red', font-size: '20px'}}]7.删除 style="color: red; font-size: 20px;"8.继续匹配直到匹配到 > 说明结束9.删除 >
//组装AST树function createASTElement(tagName, attrs) {return {tag: tagName,//元素节点type: 1,children: [],attrs,//根据父节点才能拿出结构parent}}
如何 AST 树转成 render 函数?
//利用generate生成函数 根据AST树数据生成 字符串代码//通过以下3个函数进行字符串拼接成需要的字符串代码://_c()是负责创建元素节点的函数//_v()是负责创建文本节点的函数//_s()是负责将{{name}}转化为真实数据_s(name)/**<div id="app" style="color: red; font-size: 20px;">hello {{name}}<span class="text" style="color:green">{{age}}</span></div>*/function vrender() {return `_c("div",{id: "app",style: {"color": "red","font-size": "20px"}},_v("hello" +_s(name)),_c("span",{"class": "text","style": {"color": "green"}},_v(_s(age))))`}
//根据AST树数据生成 字符串代码function generate(el) {/*** console.log(el);* {* tag: "div",* type: 1,* attrs: (2) [{…}, {…}],* children: (2) [{…}, {…}],* parent: Window* }*//*** 写法:* _c(元素, 属性对象{})*///处理children里面的属性对象let children = getChildren(el);//let code = `_c('${el.tag}',${el.attrs.length > 0 ?`${formatProps(el.attrs)}` :'undefined'}${children ? `,${children}` : ''})`;// console.log(code);return code;}
AST 形成了以后转化为 render 函数
const ast = parseHtmlToAST(html),//根据AST树数据生成 字符串代码code = generate(ast),/*** console.log(code);* _c('div',{id:"app"style:{"color":" red"," font-size":" 20px"},_v("hello "+_s(name)+" 欢迎光临"),_c('span',{class:"text"style:{"color":"green"},_v(_s(age))),_c('p',undefined,_v("hello vue")))*///生成render函数//with(obj)相当于将obj写法 省略this写法//var obj ={a: 1, b: 2}//with(obj){ console.log(a, b, a + b); } 1 2 3//将新实例的函数内部的作用域this抛出//这样,可以实现外部访问code里面的属性时不用写this.name/this.age..render = new Function(`with(this){ return ${code} }`);/*** console.log(render);* ƒ anonymous() {* with(this){ return* _c('div',{id:"app",style:{"color":" red"," font-size":" 20px"}},_v("hello "+_s(name)+" 欢迎光临"),* _c('span',{class:"text",style:{"color":"green"}},_v(_s(age))* …* }*/}
问题:render 函数如何转化为虚拟节点?
//管理所有的render函数//传入Vue 是所有render函数在该构造函数原型上进行扩展function renderMixin(Vue) {//针对vnode的render函数Vue.prototype._render = function () {const vm = this,//拿到AST形成后转出的render函数(字符串代码)render = vm.$options.render,//执行后变成vnode节点vnode = render.call(vm);//此时出来了虚拟节点/*** console.log(vnode);* {* tag: "div",* props: {id: 'app', style: {…}},* children: (3) [{…}, {…}, {…}],* text: undefined* }*/return vnode;}//负责处理创建元素节点Vue.prototype._c = function () {return createElement(...arguments);}//负责处理{{}}里面的变量字符Vue.prototype._s = function (value) {if (value === null) return;return typeof value === 'object' ? JSON.stringify(value) : value;}//负责处理创建文本节点Vue.prototype._v = function (text) {return createTextVnode(text);}}
问题:如何设置补丁并打入真实的 DOM 里面?
/*** 打补丁函数patch(oldNode, vNode)* @param {*} oldNode 指视图html已经写好的模板节点* @param {*} vNode AST生成的虚拟节点*/function patch(oldNode, vNode) {/*** console.log(vNode);* {* el: div#app,* tag: "div",* text: undefined,* children: (3) [{…}, {…}, {…}],* props: {id: 'app', style: {…}}* }*/let el = createElement(vNode),parentElement = oldNode.parentNode;//把el放到oldNode的后边//放到<script>的上方parentElement.insertBefore(el, oldNode.nextSibling);//移除旧的节点parentElement.removeChild(oldNode);}
补充以及配置:
工具:rollup 打包工具
专门打包 JS 代码
//rollup脚本//-c -> config//-w -> watch"scripts": {"dev": "rollup -c -w"}
//关于rollup-plugin-commonjs实现引入文件省略.js后缀
//配置文件rollup.config.jsimport babel from 'rollup-plugin-babel';import serve from 'rollup-plugin-serve';import commonjs from 'rollup-plugin-commonjs';export default {input: './src/index.js',output: {format: 'umd',name: 'Vue',file: 'dist/umd/vue.js',sourcemap: true},plugins: [babel({exclude: 'node_modules/**'}),serve({open: true,port: 8080,contentBase: '',openPage: '/index.html'}),commonjs]}
源码地址:
https://gitee.com/kevinleeeee/compile-template-driver-vue2.x-demo
加载器
tpl-loader
案例:手写 tpl-loader 分离模板组件
xxx.vue 是单文件应用组件,把<template>的视图文件/<style>样式文件提取分离成单个文件管理,剩下逻辑<script>代码在 xxx.vue文件里单独管理,实现代码精简,易于阅读,方便调试
//项目目录├─package-lock.json├─package.json├─webpack.config.js├─src| ├─main.js - app入口文件| ├─components| | ├─MyTitle| | | ├─index.js - 组件入口文件| | | ├─MyTitle.scss - 组件样式| | | └MyTitle.tpl - 组件模板├─public| └index.html├─loaders| ├─tpl-loader| | └index.js - 自己手写的加载器代码
//webpack.config.jsmodule.exports = {...,resolveLoader: {//通过这个配置找到tpl-loader依赖目录//合并node_modules和自己定义的tpl-loade目录modules: ['node_modules',resolve(__dirname, './loaders')]},module: {rules: [//定义tpl文件使用自己写的tpl-loader加载器{test: /\.tpl$/,loader:'tpl-loader'},]}
//tpl-loader其实是一个函数//手写的tpl-loader//commonJS规范function tplLoader(source) {// console.log(source);// 拿到的是组件入口文件里引入的tpl模板文件字符串代码//<h1 @click="handleTitleClick($event)">{{title}}</h1>//<h2 @click="handleTitleClick($event)">{{subTitle}}</h2>//其实组件里入口文件导出的是一个方法,且方法里传入一个组件//所以这里会返回的也是一个方法 (组件)=>{}//内部也会返回一个组件//这里会将template属性和内容新增至组件对象里return `export default (component) => {component.template = \`${source}\`;return component;};`;}module.exports = tplLoader;
//在APP入口文件引入组件MyTitle//打印组件发现自己写的tpl-loade加载器把template属性和内容都添加进组件对象里import MyTitle from "./components/MyTitle";/*** console.log(MyTitle);* {* data: ƒ data(),* methods: {handleTitleClick: ƒ},* template: "<h1 @click=\"handleTitleClick($event)\">{{title}}</h1>\n<h2 @click=..."* }*/
源码地址:
