前言
- 文分【思路篇】和【实现篇】,本文为实现篇,建议看两个窗口同步阅读,或请先阅读-》vue2.0|思路篇|依赖收集
- 阅读本文需具备Vue数据劫持及渲染页面有源码级别了解基础,否则会较为吃力,望理解(如有需要可见拙作vue2.0|思路篇|数据劫持,Vue|思路篇|编译ast)
Vue.mixin实现及生命周期实现
在Vue类上挂载options属性用于存储“混入”的数据或函数
- 多次使用mixin时会先将多个mixin对象合并成一个对象,Vue.options指向此对象
- 在Vue初始化数据前将Vue.options与用户传递过来的配置对象进行合并,从而完成mixin操作
首先,Vue.mixin是挂载在类上的函数,即全局函数(如extend/component等),那就同样新建一个模块global-api进行挂载
global-api/index.js
import { mergeOptions } from "../util";
export function initGlobalApi(Vue) {
Vue.options = {};
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options,mixin);
console.log(this.opions);
}
}
策略模式,定义一个策略类(对象),根据不同key对应不同的处理函数
util.js
strats.data = function (parentVal,childValue){
return childValue
}
strats.computed = function (){}
strats.watch = function (){}
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestory",
"destoryed"
];
LIFECYCLE_HOOKS.forEach(lifeCycle=>{
strats[lifeCycle] = mergeHook;
})
此时为实现合并操作,需定义一个合并函数,功能--根据传递的parent,children进行特定策略的合并
- 处理父对象所有属性
- 处理父对象无但子对象有的属性
- 根据key的不同通过策略对象的配置进行不同的处理
export function mergeOptions(parent,child) {
const options = {};
// 遍历处理父亲所有key
for (const key in parent) {
mergeField(key);
}
// 遍历所有父无子有属性
for (const key in child) {
if (!parent.hasOwnProperty(key)) {
mergeField(key);
}
}
// 子有父无
/**
* 合并字段
* @param {*} key
*/
function mergeField(key) {
if(strats[key]){
options[key] = strats[key](parent[key],child[key]);
}else {
options[key] = child[key];
}
}
return options;
}
以生命周期为例
- 记住,父元素初次是空对象,子元素只可能是两个来源
- 初次初始化时用户传递的options
- 多次mixin时Vue类上的option
- 执行逻辑
- 子元素不存在,直接返回父元素
- 子元素存在且父元素不存在,返回一个含有子元素数组
- 子元素存在且父元素存在,父元素拼接子元素
function mergeHook(parentVal,childValue){ // 生命周期的合并
if(childValue){
// 父有子有
if (parentVal) {
return parentVal.concat([childValue]);
}else {
// 父无子有
return [childValue];
}
}
else {
// 父有子无
return parentVal;
}
}
对于生命周期的实现
如果理解了之前【vue2.0|思路篇|数据劫持】、【Vue|思路篇|编译ast】,不难理解,其实就是在特定的地方执行用户传递的回调;以beforeCreate为例:
- 在Vue执行_init方法时,需要执行initState函数,在此函数调用前执行即可
- 需注意,由于存在mixin的可能,回调必是采用数组的形式
在lifecycle.js中定义执行hook的函数
- hook是一个数组,遍历执行
lifecycle.js
export function callHook(vm,hook) {
const handlers = vm.$options[hook];
if(handlers){
for (let index = 0; index < handlers.length; index++) {
const handler = handlers[index];
handler.call(vm);
}
}
}
watcher+dep实现依赖收集(对象)
定义Watcher概念,取代直接使用render
Watcher.js
需要四个参数:1. vue实例 2. 用户传递表达式或函数 (相当于 vm._update(vm._render());
)3. 回调函数 4. 配置信息对象
- 定义id,标识是一个独特的id
- 做兼容处理,因为可能用户传递的exprOrFn不是函数,所以定义一个get方法,后期将实现将expr改写为Fn的情况
let id = 0;
class Watcher {
constructor(vm,exprOrFn,cb,options){
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.id = id++;
if(typeof exprOrFn == 'function'){
this.getter = exprOrFn;
}
this.get();
}
get(){
this.getter();
}
update(){
this.get();
}
。。。
}
export default Watcher;
Watcher改写mountComponent逻辑
lifecycle.js
let updateComponent=()=>{
vm._update(vm._render());
}
new Watcher(vm,updateComponent,()=>{
callHook(vm,'beforeUpdate')
},true);
订阅发布,在对象属性取值时进行订阅,属性对应的dep(可理解为存储多个watcher的数组)收集依赖
-
为实现属性对应的dep能访问到当前执行的Watcher实例:利用js单线程的特性,在一个Watcher执行时先将自身存在Dep.target(全局可访问即可)
# Dep.js export function pushTarget(watcher) { Dep.target = watcher; } export function popTarget() { Dep.target = null; } #Watcher.js get(){ pushTarget(this); this.getter(); popTarget(); }
-
为实现属性/Dep一对一关系,在defineReactive函数时new Dep形成闭包
function defineReactuve(data,key,value){ // 实现递归 observe(value) let dep = new Dep(); 。。。 }
-
依赖收集
- 判断如果当前处于watcher渲染,则进行订阅,属性对应dep存入此依赖,以便后面属性改值时触发
- 在数据改值时发布,执行watcher的update方法,update方法内部默认执行get函数,从而实现了【数据改变-》页面更新/执行逻辑】
# Dep let id = 0; class Dep { constructor(){ this.subs = []; this.id = id++; } depend(){ this.subs.push(Dep.target) } notify(){ this.subs.push.forEatch(sub=>sub.update()); } ... } # watcher.js update(){ this.get(); } # init.js function defineReactuve(data,key,value){ // 实现递归 observe(value) let dep = new Dep(); Object.defineProperty(data,key,{ get(){ if(Dep.target){ dep.depend(); } console.log('用户取值'); return value }, set(newValue){ console.log('用户赋值'); if(value !== newValue){ // 对新赋值的对象进行观测 observe(newValue); value = newValue } // 发布 dep.notify(); } } ) }
解决收集了相同的watcher的问题
如果页面多次取值
{{age}}{{age}}{{age}}{{age}}
问题:
按照目前逻辑就会多次触发get,在渲染页面(渲染watcher)执行的过程中,age对应的dep会多次存入渲染watcher,导致后面一旦改变一次age的值,会触发四次更新;
解决:
让dep和watcer彼此记住,然后相同则不存入;
- Dep收集时不直接push,而是调用将存入的watcher的记录Dep的方法,此方法除使得watcher记录dep外还会默认将dep存储watcher
# watcher.js
depend(){
if(Dep.target){
Dep.target.addDep(this);
}
}
# watcher.js
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this);
}
}
至此对象的依赖收集完成
watcher+dep实现依赖收集(数组)
通过observe返回值获取到观察者对象,在观测者对象上挂载一个dep属性,这样就可以在这个dep上进行依赖收集
observe.js
class Observer {
constructor(data){
this.dep = new Dep();
observe/index.js
function defineReactuve(data,key,value){
// 实现递归
// 获取到数组对应的dep
let childDep = observe(value);
let dep = new Dep();
Object.defineProperty(data,key,{
get(){
if(Dep.target){
dep.depend();
if(typeof childDep == 'object'){
childDep.dep.depend();
}
}
console.log('用户取值');
return value
},
在数组重写方法的逻辑中去通过__ob__.dep获取到收集到的依赖,并发布,从而实现数组的依赖收集
observe/array
arrayMethods[method] = function (...args){
let r = oldArrayProtoMethods[method].apply(this,args)
// todo
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args;break;
case "splice":
inserted = args.slice(2)
default:
break;
}
// console.log(inserted);
if(inserted) ob.observerArray(inserted)
ob.dep.notify();
return r;
}
至此数组的依赖收集完成
最终实现
- 仓库地址:git@github.com:Sympath/blingSpace.git
- 直接访问地址:github.com/Sympath/bli…