vue源码理解


使用Vue也大概有一年多了,觉得Vue使用起来非常的简单,轻量,出于好奇,在github上将代码拉下来剖解分析。

0. 源码目录结构

  • .circleci 持续集成
  • benchmarks 性能评测
  • dist 输出目录
  • examples 案例
  • flow flow声明文件
  • packages vue中的包
  • scripts 工程化
  • src 源码目录
  • test 测试相关
  • types ts声明文件

为了直观的查看目录我们可以通过tree命令来查看src目录下的文件夹。先大概对源码的结构有一个大体的认识。

├─compiler # 编译的相关逻辑
│ ├─codegen
│ ├─directives
│ └─parser
├─core # vue核心代码
│ ├─components # vue中的内置组件 keep-alive
│ ├─global-api # vue中的全局api
│ ├─instance # vue中的核心逻辑
│ ├─observer # vue中的响应式原理
│ ├─util
│ └─vdom # vue中的虚拟dom模块
├─platforms # 平台代码
│ ├─web # web逻辑 - vue
│ │ ├─compiler
│ │ ├─runtime
│ │ ├─server
│ │ └─util
│ └─weex # weex逻辑 - app
│ ├─compiler
│ ├─runtime
│ └─util
├─server # 服务端渲染模块
├─sfc # 用于编译.vue文件
└─shared # 共享的方法和常量


> 通过package.json中scripts查看代码是如何运行的

```json
"build": "node scripts/build.js",  
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",

核心是使用node执行 scripts/build.js,通过传递参数来实现不同的打包结果,这里的–代表后面的内容是参数。

// ------ build.js
// 1.获取不同的打包的配置 
let builds = require('./config').getAllBuilds()

// 2.根据执行打包时的参数进行过滤
if (process.argv[2]) {
const filters = process.argv[2].split(‘,’)
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
// 默认不打包weex相关代码
builds = builds.filter(b => {
return b.output.file.indexOf(‘weex’) === -1
})
}
// 3.进行打包
build(builds)

> 打包入口: 我们可以通过打包的配置找到我们需要的入口,这两个区别在于是否涵盖compiler逻辑,我们在开发时一般使用的是entry-runtime,可以减小vue的体积,但是同样在开发时也不能再使用template,.vue文件中的template是通过vue-loader来进行编译的,和我们所说的compiler无关。

```javascript

src/platforms/web/entry-runtime.js
src/platforms/web/entry-runtime-with-compiler.js

源码调试

通过vue编写源码方式调试

  • 下载源代码
  • 查看package.json scripts打包命令
  • npm run dev
  • 编写代码debugger
  • 可以在打包命令后面加–sourcemap 调试源代码

通过cli项目调试

  • vue inspect 查看vue打包后的js文件(vue.runtime.esm.js)
  • 在文件中找到需要调试的方法debugger

1. 初始化

initState: 将挂载的data、watch、computed等数据进行劫持

let observe = new Observe(data);
  • 如果data是数组类型, 重写data原型方法(对能改变原数组的方法进行重写,Object.create(Array.prototype))
    • observeArray 遍历数组,调用observe
    • 如果调用数组新增的方法,对新增的数据进行劫持
  • 如果是对象
    • this.walk(data), 遍历对象调用defineReactive(data, key, data[key])方法(Object.defineProperty 数据劫持)
    • 如果 set 值是对象,observe 重新对这个对象进行数据劫持

2. 响应式原理

响应式原理: 每个属性会对应一个Dep实例,get当前属性值,对应的dep会收集渲染watcher,set值时,会通知收集的watcher调用渲染方法,重新渲染页面。如果是数组的话,会给数组增加dep属性,存储Dep实例,并收集watcher,当调用数组特殊方法时,通知dep内的watcher调用渲染方法,重新渲染页面.


//  Watch类
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
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++;
        this.getter = exprOrFn; 
        this.deps = []; 
        this.depsId = new Set();
        this.get(); // 默认初始化 要取值
    }
    get(){
        pushTarget(this); // Dep.target = watcher
        this.getter(); 
        popTarget(); 
    }
    update(){
       queueWatcher(this); 
    }
    run(){
        this.get();
    }
    addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)
        }
    }
}
export default Watcher
// Dep类
let id = 0;
class Dep{ // 每个属性我都给他分配一个dep,dep可以来存放watcher, watcher中还要存放这个dep
    constructor(){
        this.id = id++;
        this.subs = []; // 用来存放watcher的
    }
    depend(){
        if(Dep.target){
            Dep.target.addDep(this);
        }
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
Dep.target = null; // 一份

export function pushTarget(watcher) {
    Dep.target = watcher;
}
export function popTarget(){
    Dep.target = null
}
export default Dep

import { isObject } from "../utils";
import { arrayMethods } from "./array";
import Dep from './dep'
// 1.如果数据是对象 会将对象不停的递归 进行劫持
// 2.如果是数组,会劫持数组的方法,并对数组中不是基本数据类型的进行检测

// 检测数据变化 类有类型 , 对象无类型

// 如果给对象新增一个属性不会触发视图更新  (给对象本身也增加一个dep,dep中存watcher,如果增加一个属性后,我就手动的触发watcher的更新)
class Observer { 
    constructor(data) { // 对对象中的所有属性 进行劫持

        this.dep = new Dep(); // 数据可能是数组或者对象

        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false // 不可枚举的
        })
        // data.__ob__ = this; // 所有被劫持过的属性都有__ob__ 
        if(Array.isArray(data)){  // 我希望数组的变化可以触发视图更新?
            // 数组劫持的逻辑
            // 对数组原来的方法进行改写, 切片编程  高阶函数
            data.__proto__ = arrayMethods;
            // 如果数组中的数据是对象类型,需要监控对象的变化
            this.observeArray(data);
        }else{
            this.walk(data); //对象劫持的逻辑 
        }
    }
    observeArray(data){ // 对我们数组的数组 和 数组中的对象再次劫持 递归了
        // [{a:1},{b:2}]
        // 如果数组里放的是对象类型,也做了观测,JSON.stringify() 也做了收集一来了
        data.forEach(item=>observe(item))
    }
    walk(data) { // 对象
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key]);
        })
    }
}
function dependArray(value){
    for(let i = 0; i < value.length;i++){
        let current = value[i]; // current是数组里面的数组 [[[[[]]]]]
        current.__ob__ &&  current.__ob__.dep.depend();
        if(Array.isArray(current)){
            dependArray(current);
        }
    }
}

function defineReactive(data,key,value){ // value有可能是对象
    let childOb = observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
    let dep = new Dep(); // 每个属性都有一个dep属性

   // 获取到了数组对应ob

    Object.defineProperty(data,key,{
        get(){
            // 取值时我希望将watcher和dep 对应起来
            if(Dep.target){ // 此值是在模板中取值的
                dep.depend() // 让dep记住watcher
                if(childOb){ // 可能是数组 可能是对象,对象也要收集依赖,后续写$set方法时需要触发他自己的更新操作
                    childOb.dep.depend(); // 就是让数组和对象也记录watcher
                    if(Array.isArray(value)){ //取外层数组要将数组里面的也进行依赖收集
                        dependArray(value);
                    }

                }
            }
            return value
        },
        set(newV){ 
            // 更新视图
            if(newV !== value){
                observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
                value = newV;
                dep.notify(); // 告诉当前的属性存放的watcher执行
            }

        }
    })
}

export function observe(data) {
    // 如果是对象才观测
    if (!isObject(data)) {
        return;
    }
    if(data.__ob__){
        return data.__ob__;
    }
    // 默认最外层的data必须是一个对象
    return new Observer(data)
}
  • 无论对象嵌套多深,我们的对象取值的时候都会通过调用render方法_v的时候调用了JSON.stringify遍历了对象的每一个属性,所以对象内的每个属性会有有对应的dep,也会自动的收集对应的watcher
  • 多层数组嵌套,我们需要递归给数组添加dep属性,所以避免写多维数组,尽量的扁平化,减少递归就是提高性能

nextTick异步更新数据原理

多次调用watcher的更新方法update,我们可以先将其缓存到一个栈中,等同步代码执行完,在异步更新渲染页面.

  • 每次update调用nextTik方法,延迟到同步代码运行的后面执行,否则每次修改属性都去重新渲染页面,性能及其的差

const callbacks = [];
function flushCallbacks() {
    callbacks.forEach(cb => cb());
    waiting = false
}
let waiting = false;
function timer(flushCallbacks) {
    let timerFn = () => {}
    if (Promise) {
        timerFn = () => {
            Promise.resolve().then(flushCallbacks)
        }
    } else if (MutationObserver) {
        let textNode = document.createTextNode(1);
        let observe = new MutationObserver(flushCallbacks);
        observe.observe(textNode, {
            characterData: true
        })
        timerFn = () => {
            textNode.textContent = 3;
        }
        // 微任务
    } else if (setImmediate) {
        timerFn = () => {
            setImmediate(flushCallbacks)
        }
    } else {
        timerFn = () => {
            setTimeout(flushCallbacks)
        }
    }
    timerFn();
}
// 微任务是在页面渲染前执行 我取的是内存中的dom,不关心你渲染完毕没有
export function nextTick(cb) {
    callbacks.push(cb); // flushSchedulerQueue / userCallback
    if (!waiting) {
        timer(flushCallbacks); // vue2 中考虑了兼容性问题 vue3 里面不在考虑兼容性问题
        waiting = true;
    }
}

3. watch && computed

watch

  • watch可能是个数组,也可能是个函数,也可能是个对象
  • new Watcher 传入标识,标识是用户watch对应的watcher
  • 重写getter方法,get watch属性的值,让对应属性收集到我们的watch watcher
  • 当监听属性值改变,调用set方法,如果我们的标识是 watch watcher 就执行watch监听的方法,并传入Vue实例 vm,并将新值和旧值传入参数

function initWatch(vm, watch) { // Object.keys
    for (let key in watch) {
        let handler = watch[key];
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
                vm.$watch(key, handler[i])
            }
        } else {
            vm.$watch(key, handler)
        }

    }
}
function stateMixin(Vue) {
    Vue.prototype.$watch = function(key, handler, options = {}) {
        options.user = true;
        new Watcher(this, key, handler, options);
    }
}

computed

  • 兼容写法,可能是一个函数,也可能是一个函数get和set方法的对象
  • 遍历所有计算属性,用Object.defineProperty代理getter,setter
  • 创建watchers和每一个计算属性做一个映射,一个计算属性对应一个wathcer
  • new Watcher 默认不执行,传入标识,标识是computed watcher
  • get拦击属性是,做一个高阶函数 createComputedGetter,最终返回计算结果
  • 为了不是每次取值都调用watcher的更新方法,增加了脏值检测,通过dirty判断是否是脏值,如果是true,我们需要重新计算值,就重新调用watcher的更新方法
  • 计算属性内用到的属性,不能只是有computed watcher,还要有对应的渲染watcher,将Dep.target改成栈形结构,先存入渲染watcher,当遇到计算属性取值时,记录计算属性watcher,当我们计算完值后,当前计算watcher出栈,Dep.target始终是栈中最后一个,如果还有我们去mapping 的watcher反向记录,因为watcher和dep是多对多的关系,将mapping的watcher对应的所有dep添加上渲染watcher,这样每次修改计算属性内用到的属性时,就会重新计算值,并渲染页面(调用了计算watcher和渲染watcher)
  • 当页面重新渲染是,又会重新走watcher的更新方法(run()),脏值(dirty)检测修改为true,重新计算,后面重复取值,脏值为false,不再计算

function initComputed(vm, computed) {
    const watchers = vm._computedWatchers = {}
    for (let key in computed) {
        // 校验 
        const userDef = computed[key];
        // 依赖的属性变化就重新取值 get
        let getter = typeof userDef == 'function' ? userDef : userDef.get;
        // 每个就算属性本质就是watcher   
        // 将watcher和 属性 做一个映射
        watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); // 默认不执行
        // 将key 定义在vm上
        defineComputed(vm, key, userDef);
    }
}
function defineComputed(vm, key, userDef) {
    let sharedProperty = {};
    if (typeof userDef == 'function') {
        sharedProperty.get = userDef;
    } else {
        sharedProperty.get = createComputedGetter(key);
        sharedProperty.set = userDef.set ;
    }
    Object.defineProperty(vm, key, sharedProperty); // computed就是一个defineProperty
}
function createComputedGetter(key) {
    return function computedGetter() { // 取计算属性的值 走的是这个函数
        // this._computedWatchers 包含着所有的计算属性
        // 通过key 可以拿到对应watcher,这个watcher中包含了getter
        let watcher = this._computedWatchers[key]
        // 脏就是 要调用用户的getter  不脏就是不要调用getter

        if(watcher.dirty){ // 根据dirty属性 来判断是否需要重新求职
            watcher.evaluate();// this.get()
        }

        // 如果当前取完值后 Dep.target还有值  需要继续向上收集
        if(Dep.target){
            // 计算属性watcher 内部 有两个dep  firstName,lastName
            watcher.depend(); // watcher 里 对应了 多个dep
        }
        return watcher.value
    }
}

watcher 类修改


// Watcher
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";

let id = 0;
class Watcher {
    // vm,updateComponent,()=>{ console.log('更新视图了')},true
    constructor(vm, exprOrFn, cb, options) {

        // exporOfFn
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.user = !!options.user; // 是不是用户watcher
        this.lazy = !!options.lazy;
        this.dirty = options.lazy; // 如果是计算属性,那么默认值lazy:true, dirty:true
        this.cb = cb;
        this.options = options;
        this.id = id++;

        // 默认应该让exprOrFn执行  exprOrFn 方法做了什么是? render (去vm上了取值)
        if (typeof exprOrFn == 'string') {
            this.getter = function() { // 需要将表达式转化成函数
                // 当我数据取值时 , 会进行依赖收集
                // age.n  vm['age.n']  =》 vm['age']['n']
                let path = exprOrFn.split('.'); // [age,n]
                let obj = vm;
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]
                }
                return obj; // getter方法
            }
        } else {
            this.getter = exprOrFn; // updateComponent
        }

        this.deps = [];
        this.depsId = new Set();
        // 第一次的value
        this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值

    }
    get() { // 稍后用户更新 时 可以重新调用getter方法
        // defineProperty.get, 每个属性都可以收集自己的watcher
        // 我希望一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
        pushTarget(this); // Dep.target = watcher
        const value = this.getter.call(this.vm); // render() 方法会去vm上取值 vm._update(vm._render)
        popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了

        return value
    }
    update() { // vue中的更新操作是异步的
        // 每次更新时 this

        if(this.lazy){
            this.dirty = true;
        }else{
            queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
        }

    }
    run() { // 后续要有其他功能
        let newValue = this.get();
        let oldValue = this.value
        this.value = newValue; // 为了保证下一次更新时 上一次的最新值是下一次的老值
        if (this.user) {
            this.cb.call(this.vm, newValue, oldValue);
        }
    }
    addDep(dep) {
        let id = dep.id;
        if (!this.depsId.has(id)) {
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)
        }
    }
    evaluate(){
        this.dirty = false; // 为false表示取过值了
        this.value = this.get(); // 用户的getter执行
    }
    depend(){
        let i = this.deps.length;
        while(i--){
            this.deps[i].depend(); //lastName,firstName 收集渲染watcher
        }
    }
}
// watcher 和 dep
// 我们将更新的功能封装了一个watcher
// 渲染页面前,会将当前watcher放到Dep类上
// 在vue中页面渲染时使用的属性,需要进行依赖收集 ,收集对象的渲染watcher
// 取值时,给每个属性都加了个dep属性,用于存储这个渲染watcher (同一个watcher会对应多个dep)
// 每个属性可能对应多个视图(多个视图肯定是多个watcher) 一个属性要对应多个watcher
// dep.depend() => 通知dep存放watcher => Dep.target.addDep() => 通知watcher存放dep
// 双向存储
export default Watcher

4. 组件 && 生命周期

mergeOptions, 合并传入的选项 – 合并策略

  • Vue.mixin(options): 其中的钩子函数会合并成一个数组,其他属性和方法会覆盖传入的相同内容,并挂在掉vm.$options上
  • Vue.component(id, options) 组件合并策略: 后面传入的组件通过Object.create()方法,链式查找对应的components,不能直接覆盖options内组件,同名全局组件会遭到污染.

vm.$options = mergeOptions(vm.constructor.options, options);
let strats = {}; // 存放各种策略
// 钩子函数合并策略
function mergeHook(parentVal, childVal) {
    if (childVal) {
        if (parentVal) {
            return parentVal.concat(childVal); // 后续
        } else {
            return [childVal]; // 第一次
        }
    } else {
        return parentVal
    }
}
// 组件合并策略
strats.components = function(parentVal, childVal) {
    // Vue.options.components
    let options = Object.create(parentVal); // 根据父对象构造一个新对象 options.__proto__= parentVal
    if (childVal) {
        for (let key in childVal) {
            options[key] = childVal[key];
        }
    }
    return options
}
function mergeOptions(parent, child) {
    const options = {}; // 合并后的结果
    for (let key in parent) {
        mergeField(key);
    }
    for (let key in child) {
        if (parent.hasOwnProperty(key)) {
            continue;
        }
        mergeField(key);
    }
    function mergeField(key) {
        let parentVal = parent[key];
        let childVal = child[key];
        // 策略模式
        if (strats[key]) { // 如果有对应的策略就调用对应的策略即可
            options[key] = strats[key](parentVal, childVal)
        } else {
            if (isObject(parentVal) && isObject(childVal)) {
                options[key] = { ...parentVal, ...childVal }
            } else {
                options[key] = child[key] || parent[key];
            }
        }
    }
    return options
}

Vue.extend

  • 通过传入的选项生成一个类,并继承父类

// 给个对象返回类
Vue.extend = function (opts) { // extend方法就是产生一个继承于Vue的类
    // 并且身上应该有父类的所有功能 
    const Super = this
    const Sub = function VueComponent(options){
        this._init(options);
    }
    // 原型继承
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.options = mergeOptions(Super.options,opts);// 只和Vue.options合并
    return Sub;
}

组件渲染

全局组件: Vue.component(id, options) 会调用Sub = Vue.extend(option), 然后对options进行映射 options[id] = Sub

  1. 创建真实DOM时,判断当前标签是不是原生标签,如果不是在判断是不是组件
  2. 如果是组件,给其data属性上挂载hook对象,并加上init方法。
  3. init用于初始化组件(因为组件无el元素,手动调用组件的$mounte方法并做到兼容无el元素返回真实DOM),将组件内的模板解析成真是DOM挂载到$el上
  4. 最后将$el替换定义的组件标签,加载真实DOM

// 如果tag是组件 应该渲染一个组件的vnode isReservedTag定义了所有原生标签
if (isReservedTag(tag)) {
    return vnode(vm, tag, data, data.key, children, undefined);
} else {
    const Ctor = vm.$options.components[tag]
    return createComponent(vm, tag, data, data.key, children, Ctor);
}

// 创建组件的虚拟节点, 为了区分组件和元素  data.hook  /  componentOptions
function createComponent(vm, tag, data, key, children, Ctor) {
    // 组件的构造函数
    if(isObject(Ctor)){
        Ctor = vm.$options._base.extend(Ctor); // Vue.extend 
    }
    data.hook = { // 等会渲染组件时 需要调用此初始化方法
        init(vnode){
           let vm = vnode.componentInstance =  new Ctor({_isComponent:true}); // new Sub 会用此选项和组件的配置进行合并
           vm.$mount(); // 组件挂载完成后 会在 vnode.componentInstance.$el => <button>
        }
    }
    return vnode(vm,`vue-component-${tag}`,data,key,undefined,undefined,{Ctor,children})
}

// patch 方法生成DOM元素  createElm方法
if (createComponent(vnode)) {
    // 返回组件对应的真实节点
    return vnode.componentInstance.$el;
}
function createComponent(vnode) {
    let i = vnode.data; //  vnode.data.hook.init
    if ((i = i.hook) && (i = i.init)) {
        i(vnode); // 调用init方法
    }
    if(vnode.componentInstance){ // 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el
        return true;
    }
}

5. DOM DIFF

每次watcher调度更新dom都是全量更新,消耗比较大,所以需要在patch的时候,最大程序的复用原先的DOM,最小程序的更新DOM。

  1. 对比标签: 在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
  2. 对比文本: 如果标签一致,有可能都是文本节点,那就比较文本的内容即可
  3. 对比属性: 标签一直,将新属性赋值给老DOM并删除老DOM多余的属性,不需重新生成DOM,style和class做了特殊处理,因为在vNode中,style是一个对象,需要遍历给el.style设置属性,class不用setAttribute, 直接el.class 赋值即可
  4. 比对子元素

// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
    updateChildren(el, oldChildren, newChildren)
// 老的有儿子新的没有清空即可
}else if(oldChildren.length > 0 ){
    el.innerHTML = '';
// 新的有儿子
}else if(newChildren.length > 0){
    for(let i = 0 ; i < newChildren.length ;i++){
        let child = newChildren[i];
        el.appendChild(createElm(child));
    }
}

vue中子节点比较采用了双指针的思想

  • 头头比较 => 尾尾比较 => 头尾比较 => 尾头比较
  • 如果比对相同,则头指针后移,尾指针前移
  • 在比对过程中,可能出现空值情况则直接跳过
  • 当头尾指针重合,比对结束
  • 结束后,如果新node子节点多,appendChile
  • 如果老node子节点多,removeChild

// 1.在开头和结尾新增元素

function isSameVnode(oldVnode,newVnode){
    // 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
    return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
function updateChildren(parent, oldChildren, newChildren) {
    let oldStartIndex = 0;
    let oldStartVnode = oldChildren[0];
    let oldEndIndex = oldChildren.length - 1;
    let oldEndVnode = oldChildren[oldEndIndex];

    let newStartIndex = 0;
    let newStartVnode = newChildren[0];
    let newEndIndex = newChildren.length - 1;
    let newEndVnode = newChildren[newEndIndex];

    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        // 优化向后追加逻辑
        if(isSameVnode(oldStartVnode,newStartVnode)){
            patch(oldStartVnode,newStartVnode);
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
        // 优化向前追加逻辑
        }else if(isSameVnode(oldEndVnode,newEndVnode)){ 
            patch(oldEndVnode,newEndVnode); // 比较孩子 
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
        }
    }
    if(newStartIndex <= newEndIndex){
        for(let i = newStartIndex ; i<=newEndIndex ;i++){
            let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
            parent.insertBefore(createElm(newChildren[i]),ele);
        }
    }
}
// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
    patch(oldStartVnode,newEndVnode);
    parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
    oldStartVnode = oldChildren[++oldStartIndex];
    newEndVnode = newChildren[--newEndIndex]
// 尾部移动到头部
}else if(isSameVnode(oldEndVnode,newStartVnode)){
    patch(oldEndVnode,newStartVnode);
    parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
    oldEndVnode = oldChildren[--oldEndIndex];
    newStartVnode = newChildren[++newStartIndex]
}

暴力比对


function makeIndexByKey(children) {
    let map = {};
    children.forEach((item, index) => {
        map[item.key] = index
    });
    return map; 
}
let map = makeIndexByKey(oldChildren);
// 给所有的孩子进行编号
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
    parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
    let moveVnode = oldChildren[moveIndex]; 
    oldChildren[moveIndex] = undefined;
    parent.insertBefore(moveVnode.el, oldStartVnode.el);
    patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
// 用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入
if(oldStartIndex <= oldEndIndex){
    for(let i = oldStartIndex; i<=oldEndIndex;i++){
        let child = oldChildren[i];
        if(child != undefined){
            parent.removeChild(child.el)
        }
    }
}
// 如果有剩余则直接删除
if(!oldStartVnode){
    oldStartVnode = oldChildren[++oldStartIndex];
}else if(!oldEndVnode){
    oldEndVnode = oldChildren[--oldEndIndex]
}

文章作者: Jia
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jia !
  目录